@hasna/economy 0.2.31 → 0.2.32
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/README.md +6 -6
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/index.js +184 -832
- package/dist/db/database.d.ts +1 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -1049
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +1 -1
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/cloud-sync.d.ts +2 -9
- package/dist/lib/cloud-sync.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +2 -3
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/peer-sync.d.ts +1 -1
- package/dist/lib/peer-sync.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +1 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +34 -514
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +15 -442
- package/dist/server/index.js +46 -492
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/db/storage-adapter.d.ts +0 -34
- package/dist/db/storage-adapter.d.ts.map +0 -1
- package/dist/lib/remote-storage.d.ts +0 -15
- package/dist/lib/remote-storage.d.ts.map +0 -1
- package/dist/lib/storage-sync.d.ts +0 -27
- package/dist/lib/storage-sync.d.ts.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -17,60 +17,6 @@ var __export = (target, all) => {
|
|
|
17
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
18
18
|
var __require = import.meta.require;
|
|
19
19
|
|
|
20
|
-
// src/db/storage-adapter.ts
|
|
21
|
-
import { Database as BunDatabase } from "bun:sqlite";
|
|
22
|
-
|
|
23
|
-
class SqliteAdapter {
|
|
24
|
-
db;
|
|
25
|
-
constructor(path) {
|
|
26
|
-
this.db = new BunDatabase(path, { create: true });
|
|
27
|
-
}
|
|
28
|
-
run(sql, ...params) {
|
|
29
|
-
const result = this.db.prepare(sql).run(...params);
|
|
30
|
-
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
31
|
-
}
|
|
32
|
-
get(sql, ...params) {
|
|
33
|
-
return this.db.prepare(sql).get(...params);
|
|
34
|
-
}
|
|
35
|
-
all(sql, ...params) {
|
|
36
|
-
return this.db.prepare(sql).all(...params);
|
|
37
|
-
}
|
|
38
|
-
exec(sql) {
|
|
39
|
-
this.db.exec(sql);
|
|
40
|
-
}
|
|
41
|
-
query(sql) {
|
|
42
|
-
return this.db.query(sql);
|
|
43
|
-
}
|
|
44
|
-
prepare(sql) {
|
|
45
|
-
const statement = this.db.prepare(sql);
|
|
46
|
-
return {
|
|
47
|
-
run(...params) {
|
|
48
|
-
const result = statement.run(...params);
|
|
49
|
-
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
50
|
-
},
|
|
51
|
-
get(...params) {
|
|
52
|
-
return statement.get(...params);
|
|
53
|
-
},
|
|
54
|
-
all(...params) {
|
|
55
|
-
return statement.all(...params);
|
|
56
|
-
},
|
|
57
|
-
finalize() {
|
|
58
|
-
statement.finalize();
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
close() {
|
|
63
|
-
this.db.close();
|
|
64
|
-
}
|
|
65
|
-
transaction(fn) {
|
|
66
|
-
return this.db.transaction(fn)();
|
|
67
|
-
}
|
|
68
|
-
get raw() {
|
|
69
|
-
return this.db;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
var init_storage_adapter = () => {};
|
|
73
|
-
|
|
74
20
|
// src/lib/pricing.ts
|
|
75
21
|
var exports_pricing = {};
|
|
76
22
|
__export(exports_pricing, {
|
|
@@ -567,6 +513,7 @@ var init_pricing = __esm(() => {
|
|
|
567
513
|
});
|
|
568
514
|
|
|
569
515
|
// src/db/database.ts
|
|
516
|
+
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
570
517
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
571
518
|
import { hostname } from "os";
|
|
572
519
|
import { homedir } from "os";
|
|
@@ -609,7 +556,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
609
556
|
if (dir && !existsSync(dir))
|
|
610
557
|
mkdirSync(dir, { recursive: true });
|
|
611
558
|
}
|
|
612
|
-
const db = new
|
|
559
|
+
const db = new Database(path);
|
|
613
560
|
db.exec("PRAGMA journal_mode = WAL");
|
|
614
561
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
615
562
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1636,10 +1583,7 @@ function dedupeRequests(db) {
|
|
|
1636
1583
|
}
|
|
1637
1584
|
return removed;
|
|
1638
1585
|
}
|
|
1639
|
-
var init_database =
|
|
1640
|
-
init_storage_adapter();
|
|
1641
|
-
init_storage_adapter();
|
|
1642
|
-
});
|
|
1586
|
+
var init_database = () => {};
|
|
1643
1587
|
|
|
1644
1588
|
// src/db/pg-migrations.ts
|
|
1645
1589
|
var exports_pg_migrations = {};
|
|
@@ -1838,13 +1782,13 @@ __export(exports_billing, {
|
|
|
1838
1782
|
});
|
|
1839
1783
|
import { readFileSync as readFileSync9 } from "fs";
|
|
1840
1784
|
function getAnthropicAdminKey() {
|
|
1841
|
-
return process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
1785
|
+
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
1842
1786
|
}
|
|
1843
1787
|
function getOpenAIAdminKey() {
|
|
1844
|
-
return process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
1788
|
+
return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
1845
1789
|
}
|
|
1846
1790
|
function getGeminiBillingExportPath() {
|
|
1847
|
-
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
|
|
1791
|
+
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
|
|
1848
1792
|
}
|
|
1849
1793
|
function toISODate(d) {
|
|
1850
1794
|
return d.toISOString().substring(0, 10);
|
|
@@ -1920,7 +1864,7 @@ function parseBillingRows(content) {
|
|
|
1920
1864
|
async function syncAnthropicBilling(db, opts = {}) {
|
|
1921
1865
|
const key = getAnthropicAdminKey();
|
|
1922
1866
|
if (!key)
|
|
1923
|
-
throw new Error("Missing Anthropic admin key (
|
|
1867
|
+
throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
|
|
1924
1868
|
const now = new Date;
|
|
1925
1869
|
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
1926
1870
|
const days = opts.days ?? 31;
|
|
@@ -1969,7 +1913,7 @@ async function syncAnthropicBilling(db, opts = {}) {
|
|
|
1969
1913
|
async function syncOpenAIBilling(db, opts = {}) {
|
|
1970
1914
|
const key = getOpenAIAdminKey();
|
|
1971
1915
|
if (!key)
|
|
1972
|
-
throw new Error("Missing OpenAI admin key (
|
|
1916
|
+
throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
|
|
1973
1917
|
const now = new Date;
|
|
1974
1918
|
const end = opts.toDate ? new Date(opts.toDate) : now;
|
|
1975
1919
|
const days = opts.days ?? 31;
|
|
@@ -2021,7 +1965,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
2021
1965
|
return {
|
|
2022
1966
|
days: 0,
|
|
2023
1967
|
totalUsd: 0,
|
|
2024
|
-
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH or GEMINI_BILLING_EXPORT_PATH)"
|
|
1968
|
+
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH, HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH, or GEMINI_BILLING_EXPORT_PATH)"
|
|
2025
1969
|
};
|
|
2026
1970
|
}
|
|
2027
1971
|
const now = new Date;
|
|
@@ -2062,26 +2006,22 @@ __export(exports_open_projects, {
|
|
|
2062
2006
|
syncOpenProjectsRegistry: () => syncOpenProjectsRegistry
|
|
2063
2007
|
});
|
|
2064
2008
|
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
2065
|
-
let
|
|
2066
|
-
if (!
|
|
2009
|
+
let listProjects2 = listActiveProjects;
|
|
2010
|
+
if (!listProjects2) {
|
|
2067
2011
|
const projectsApi = await import("@hasna/projects");
|
|
2068
|
-
|
|
2069
|
-
}
|
|
2070
|
-
if (!listOpenProjects) {
|
|
2071
|
-
throw new Error("@hasna/projects does not expose listWorkspaces or listProjects");
|
|
2012
|
+
listProjects2 = projectsApi.listProjects;
|
|
2072
2013
|
}
|
|
2073
|
-
const projects =
|
|
2014
|
+
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
2074
2015
|
let imported = 0;
|
|
2075
2016
|
let skipped = 0;
|
|
2076
2017
|
for (const project of projects) {
|
|
2077
|
-
|
|
2078
|
-
if (!path) {
|
|
2018
|
+
if (!project.path) {
|
|
2079
2019
|
skipped++;
|
|
2080
2020
|
continue;
|
|
2081
2021
|
}
|
|
2082
2022
|
upsertProject(db, {
|
|
2083
2023
|
id: project.id,
|
|
2084
|
-
path,
|
|
2024
|
+
path: project.path,
|
|
2085
2025
|
name: project.name,
|
|
2086
2026
|
description: project.description,
|
|
2087
2027
|
tags: project.tags ?? [],
|
|
@@ -2097,9 +2037,9 @@ var init_open_projects = __esm(() => {
|
|
|
2097
2037
|
|
|
2098
2038
|
// src/lib/config.ts
|
|
2099
2039
|
import { existsSync as existsSync10, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
2100
|
-
import { dirname
|
|
2040
|
+
import { dirname, join as join9 } from "path";
|
|
2101
2041
|
function getConfigPath() {
|
|
2102
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
2042
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join9(getDataDir(), "config.json");
|
|
2103
2043
|
}
|
|
2104
2044
|
function loadConfig() {
|
|
2105
2045
|
try {
|
|
@@ -2721,7 +2661,7 @@ init_pricing();
|
|
|
2721
2661
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
2722
2662
|
import { homedir as homedir3 } from "os";
|
|
2723
2663
|
import { join as join3, basename as basename2 } from "path";
|
|
2724
|
-
import { Database as
|
|
2664
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2725
2665
|
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2726
2666
|
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2727
2667
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
@@ -2759,7 +2699,7 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2759
2699
|
for (const readonly of [true, false]) {
|
|
2760
2700
|
let codexDb = null;
|
|
2761
2701
|
try {
|
|
2762
|
-
codexDb = readonly ? new
|
|
2702
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
2763
2703
|
codexDb.prepare("PRAGMA schema_version").get();
|
|
2764
2704
|
return codexDb;
|
|
2765
2705
|
} catch (error) {
|
|
@@ -3825,8 +3765,6 @@ init_database();
|
|
|
3825
3765
|
|
|
3826
3766
|
// src/lib/cloud-sync.ts
|
|
3827
3767
|
init_database();
|
|
3828
|
-
import { homedir as homedir9, platform } from "os";
|
|
3829
|
-
import { dirname, join as join9 } from "path";
|
|
3830
3768
|
|
|
3831
3769
|
// src/lib/package-metadata.ts
|
|
3832
3770
|
import { readFileSync as readFileSync8 } from "fs";
|
|
@@ -3844,366 +3782,6 @@ function getPackageMetadata() {
|
|
|
3844
3782
|
}
|
|
3845
3783
|
var packageMetadata = getPackageMetadata();
|
|
3846
3784
|
|
|
3847
|
-
// src/lib/remote-storage.ts
|
|
3848
|
-
import pg from "pg";
|
|
3849
|
-
function translatePlaceholders(sql) {
|
|
3850
|
-
let index = 0;
|
|
3851
|
-
return sql.replace(/\?/g, () => `$${++index}`);
|
|
3852
|
-
}
|
|
3853
|
-
function normalizeParams(params) {
|
|
3854
|
-
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
3855
|
-
return flat.map((value) => value === undefined ? null : value);
|
|
3856
|
-
}
|
|
3857
|
-
function sslConfigFor(connectionString) {
|
|
3858
|
-
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
3859
|
-
}
|
|
3860
|
-
|
|
3861
|
-
class PgAdapterAsync {
|
|
3862
|
-
pool;
|
|
3863
|
-
constructor(source) {
|
|
3864
|
-
this.pool = typeof source === "string" ? new pg.Pool({ connectionString: source, ssl: sslConfigFor(source) }) : source;
|
|
3865
|
-
}
|
|
3866
|
-
async run(sql, ...params) {
|
|
3867
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
3868
|
-
return { changes: result.rowCount ?? 0, lastInsertRowid: 0 };
|
|
3869
|
-
}
|
|
3870
|
-
async get(sql, ...params) {
|
|
3871
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
3872
|
-
return result.rows[0] ?? null;
|
|
3873
|
-
}
|
|
3874
|
-
async all(sql, ...params) {
|
|
3875
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
3876
|
-
return result.rows;
|
|
3877
|
-
}
|
|
3878
|
-
async exec(sql) {
|
|
3879
|
-
await this.pool.query(translatePlaceholders(sql));
|
|
3880
|
-
}
|
|
3881
|
-
async close() {
|
|
3882
|
-
await this.pool.end();
|
|
3883
|
-
}
|
|
3884
|
-
async transaction(fn) {
|
|
3885
|
-
const client = await this.pool.connect();
|
|
3886
|
-
try {
|
|
3887
|
-
await client.query("BEGIN");
|
|
3888
|
-
const result = await fn(client);
|
|
3889
|
-
await client.query("COMMIT");
|
|
3890
|
-
return result;
|
|
3891
|
-
} catch (error) {
|
|
3892
|
-
await client.query("ROLLBACK");
|
|
3893
|
-
throw error;
|
|
3894
|
-
} finally {
|
|
3895
|
-
client.release();
|
|
3896
|
-
}
|
|
3897
|
-
}
|
|
3898
|
-
get raw() {
|
|
3899
|
-
return this.pool;
|
|
3900
|
-
}
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
// src/lib/storage-sync.ts
|
|
3904
|
-
async function syncPush(local, remote, options) {
|
|
3905
|
-
const tables = await getTableOrder(remote, options.tables);
|
|
3906
|
-
return syncTransfer(local, remote, { ...options, tables }, "push");
|
|
3907
|
-
}
|
|
3908
|
-
async function syncPull(remote, local, options) {
|
|
3909
|
-
const tables = await getTableOrder(remote, options.tables);
|
|
3910
|
-
return syncTransfer(remote, local, { ...options, tables }, "pull");
|
|
3911
|
-
}
|
|
3912
|
-
function quoteIdent(identifier) {
|
|
3913
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
3914
|
-
}
|
|
3915
|
-
async function getTableOrder(remote, tables) {
|
|
3916
|
-
if (tables.length <= 1)
|
|
3917
|
-
return tables;
|
|
3918
|
-
try {
|
|
3919
|
-
const rows = await remote.all(`
|
|
3920
|
-
SELECT DISTINCT
|
|
3921
|
-
tc.table_name AS source_table,
|
|
3922
|
-
ccu.table_name AS referenced_table
|
|
3923
|
-
FROM information_schema.table_constraints tc
|
|
3924
|
-
JOIN information_schema.constraint_column_usage ccu
|
|
3925
|
-
ON tc.constraint_name = ccu.constraint_name
|
|
3926
|
-
AND tc.table_schema = ccu.table_schema
|
|
3927
|
-
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
3928
|
-
AND tc.table_schema = 'public'
|
|
3929
|
-
`);
|
|
3930
|
-
if (rows.length > 0)
|
|
3931
|
-
return topoSort(tables, rows);
|
|
3932
|
-
} catch {}
|
|
3933
|
-
return tables;
|
|
3934
|
-
}
|
|
3935
|
-
function topoSort(tables, foreignKeys) {
|
|
3936
|
-
const allowed = new Set(tables);
|
|
3937
|
-
const deps = new Map;
|
|
3938
|
-
for (const table of tables)
|
|
3939
|
-
deps.set(table, new Set);
|
|
3940
|
-
for (const fk of foreignKeys) {
|
|
3941
|
-
if (allowed.has(fk.source_table) && allowed.has(fk.referenced_table)) {
|
|
3942
|
-
deps.get(fk.source_table)?.add(fk.referenced_table);
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
const sorted = [];
|
|
3946
|
-
const visited = new Set;
|
|
3947
|
-
const visiting = new Set;
|
|
3948
|
-
function visit(table) {
|
|
3949
|
-
if (visited.has(table))
|
|
3950
|
-
return;
|
|
3951
|
-
if (visiting.has(table)) {
|
|
3952
|
-
visited.add(table);
|
|
3953
|
-
sorted.push(table);
|
|
3954
|
-
return;
|
|
3955
|
-
}
|
|
3956
|
-
visiting.add(table);
|
|
3957
|
-
for (const dep of deps.get(table) ?? [])
|
|
3958
|
-
visit(dep);
|
|
3959
|
-
visiting.delete(table);
|
|
3960
|
-
visited.add(table);
|
|
3961
|
-
sorted.push(table);
|
|
3962
|
-
}
|
|
3963
|
-
for (const table of tables)
|
|
3964
|
-
visit(table);
|
|
3965
|
-
return sorted;
|
|
3966
|
-
}
|
|
3967
|
-
async function resolvePrimaryKeys(source, target, table, option) {
|
|
3968
|
-
if (option)
|
|
3969
|
-
return Array.isArray(option) ? option : [option];
|
|
3970
|
-
const sourceKeys = await detectPrimaryKeys(source, table);
|
|
3971
|
-
if (sourceKeys.length > 0)
|
|
3972
|
-
return sourceKeys;
|
|
3973
|
-
return detectPrimaryKeys(target, table);
|
|
3974
|
-
}
|
|
3975
|
-
async function detectPrimaryKeys(adapter, table) {
|
|
3976
|
-
if (isAsyncAdapter(adapter)) {
|
|
3977
|
-
try {
|
|
3978
|
-
const rows = await adapter.all(`
|
|
3979
|
-
SELECT kcu.column_name, kcu.ordinal_position
|
|
3980
|
-
FROM information_schema.table_constraints tc
|
|
3981
|
-
JOIN information_schema.key_column_usage kcu
|
|
3982
|
-
ON tc.constraint_name = kcu.constraint_name
|
|
3983
|
-
AND tc.table_schema = kcu.table_schema
|
|
3984
|
-
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
3985
|
-
AND tc.table_schema = 'public'
|
|
3986
|
-
AND tc.table_name = ?
|
|
3987
|
-
ORDER BY kcu.ordinal_position
|
|
3988
|
-
`, table);
|
|
3989
|
-
return rows.map((row) => row.column_name);
|
|
3990
|
-
} catch {
|
|
3991
|
-
return [];
|
|
3992
|
-
}
|
|
3993
|
-
}
|
|
3994
|
-
try {
|
|
3995
|
-
const rows = adapter.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
3996
|
-
return rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
|
|
3997
|
-
} catch {
|
|
3998
|
-
return [];
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
async function ensureTablesExist(source, target, tables) {
|
|
4002
|
-
if (!isAsyncAdapter(source) || isAsyncAdapter(target))
|
|
4003
|
-
return;
|
|
4004
|
-
for (const table of tables)
|
|
4005
|
-
await ensureTableInSqliteFromPg(target, source, table);
|
|
4006
|
-
}
|
|
4007
|
-
async function ensureTableInSqliteFromPg(target, source, table) {
|
|
4008
|
-
const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
|
|
4009
|
-
if (existing.length > 0)
|
|
4010
|
-
return;
|
|
4011
|
-
const columns = await source.all(`
|
|
4012
|
-
SELECT column_name, data_type, is_nullable
|
|
4013
|
-
FROM information_schema.columns
|
|
4014
|
-
WHERE table_schema = 'public' AND table_name = ?
|
|
4015
|
-
ORDER BY ordinal_position
|
|
4016
|
-
`, table);
|
|
4017
|
-
if (columns.length === 0)
|
|
4018
|
-
return;
|
|
4019
|
-
const primaryKeys = new Set(await detectPrimaryKeys(source, table));
|
|
4020
|
-
const definitions = columns.filter((column) => !["tsvector", "tsquery"].includes(column.data_type.toLowerCase())).map((column) => {
|
|
4021
|
-
const type = pgTypeToSqlite(column.data_type);
|
|
4022
|
-
const notNull = column.is_nullable === "NO" && !primaryKeys.has(column.column_name) ? " NOT NULL" : "";
|
|
4023
|
-
return `${quoteIdent(column.column_name)} ${type}${notNull}`;
|
|
4024
|
-
});
|
|
4025
|
-
if (primaryKeys.size > 0) {
|
|
4026
|
-
definitions.push(`PRIMARY KEY (${[...primaryKeys].map(quoteIdent).join(", ")})`);
|
|
4027
|
-
}
|
|
4028
|
-
target.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (${definitions.join(", ")})`);
|
|
4029
|
-
}
|
|
4030
|
-
function pgTypeToSqlite(pgType) {
|
|
4031
|
-
const type = pgType.toLowerCase();
|
|
4032
|
-
if (type.includes("int") || ["bigint", "smallint", "serial", "bigserial"].includes(type))
|
|
4033
|
-
return "INTEGER";
|
|
4034
|
-
if (type.includes("bool"))
|
|
4035
|
-
return "INTEGER";
|
|
4036
|
-
if (type.includes("float") || type.includes("double") || ["real", "numeric", "decimal"].includes(type))
|
|
4037
|
-
return "REAL";
|
|
4038
|
-
if (type === "bytea")
|
|
4039
|
-
return "BLOB";
|
|
4040
|
-
return "TEXT";
|
|
4041
|
-
}
|
|
4042
|
-
async function filterColumnsForTarget(target, table, columns) {
|
|
4043
|
-
if (columns.includes("machine_id") && table !== "machines")
|
|
4044
|
-
await ensureMachineIdColumnInTarget(target, table);
|
|
4045
|
-
try {
|
|
4046
|
-
if (isAsyncAdapter(target)) {
|
|
4047
|
-
const rows2 = await target.all(`
|
|
4048
|
-
SELECT column_name
|
|
4049
|
-
FROM information_schema.columns
|
|
4050
|
-
WHERE table_schema = 'public' AND table_name = ?
|
|
4051
|
-
`, table);
|
|
4052
|
-
if (rows2.length === 0)
|
|
4053
|
-
return columns;
|
|
4054
|
-
const targetColumns2 = new Set(rows2.map((row) => row.column_name));
|
|
4055
|
-
return columns.filter((column) => targetColumns2.has(column));
|
|
4056
|
-
}
|
|
4057
|
-
const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
4058
|
-
if (rows.length === 0)
|
|
4059
|
-
return columns;
|
|
4060
|
-
const targetColumns = new Set(rows.map((row) => row.name));
|
|
4061
|
-
return columns.filter((column) => targetColumns.has(column));
|
|
4062
|
-
} catch {
|
|
4063
|
-
return columns;
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
async function ensureMachineIdColumnInTarget(target, table) {
|
|
4067
|
-
if (isAsyncAdapter(target)) {
|
|
4068
|
-
const rows2 = await target.all(`
|
|
4069
|
-
SELECT column_name
|
|
4070
|
-
FROM information_schema.columns
|
|
4071
|
-
WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'
|
|
4072
|
-
`, table);
|
|
4073
|
-
if (rows2.length === 0)
|
|
4074
|
-
await target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
4075
|
-
return;
|
|
4076
|
-
}
|
|
4077
|
-
const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
4078
|
-
if (!rows.some((row) => row.name === "machine_id")) {
|
|
4079
|
-
target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
4080
|
-
}
|
|
4081
|
-
}
|
|
4082
|
-
async function syncTransfer(source, target, options, _direction) {
|
|
4083
|
-
const { tables, onProgress, batchSize = 100, conflictColumn = "updated_at", primaryKey } = options;
|
|
4084
|
-
const results = [];
|
|
4085
|
-
const sqliteTarget = isAsyncAdapter(target) ? null : target;
|
|
4086
|
-
await ensureTablesExist(source, target, tables);
|
|
4087
|
-
if (sqliteTarget) {
|
|
4088
|
-
try {
|
|
4089
|
-
sqliteTarget.exec("PRAGMA foreign_keys = OFF");
|
|
4090
|
-
} catch {}
|
|
4091
|
-
}
|
|
4092
|
-
try {
|
|
4093
|
-
for (let i = 0;i < tables.length; i++) {
|
|
4094
|
-
const table = tables[i];
|
|
4095
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, rowsSkipped: 0, errors: [] };
|
|
4096
|
-
try {
|
|
4097
|
-
onProgress?.({ table, phase: "reading", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
4098
|
-
const rows = await readAll(source, `SELECT * FROM ${quoteIdent(table)}`);
|
|
4099
|
-
result.rowsRead = rows.length;
|
|
4100
|
-
if (rows.length === 0) {
|
|
4101
|
-
onProgress?.({ table, phase: "done", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
4102
|
-
results.push(result);
|
|
4103
|
-
continue;
|
|
4104
|
-
}
|
|
4105
|
-
const sourceColumns = Object.keys(rows[0]);
|
|
4106
|
-
const columns = await filterColumnsForTarget(target, table, sourceColumns);
|
|
4107
|
-
const primaryKeys = await resolvePrimaryKeys(source, target, table, primaryKey);
|
|
4108
|
-
if (primaryKeys.length === 0) {
|
|
4109
|
-
result.errors.push(`Table "${table}" has no primary key; inserted without conflict handling`);
|
|
4110
|
-
for (const batch of batches(rows, batchSize)) {
|
|
4111
|
-
await insertBatch(target, table, columns, batch);
|
|
4112
|
-
result.rowsWritten += batch.length;
|
|
4113
|
-
}
|
|
4114
|
-
results.push(result);
|
|
4115
|
-
continue;
|
|
4116
|
-
}
|
|
4117
|
-
const missingKeys = primaryKeys.filter((key) => !columns.includes(key));
|
|
4118
|
-
if (missingKeys.length > 0) {
|
|
4119
|
-
result.errors.push(`Table "${table}" missing primary key column(s): ${missingKeys.join(", ")}`);
|
|
4120
|
-
results.push(result);
|
|
4121
|
-
continue;
|
|
4122
|
-
}
|
|
4123
|
-
onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
4124
|
-
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
4125
|
-
const newestWinsColumn = columns.includes(conflictColumn) ? conflictColumn : undefined;
|
|
4126
|
-
for (const batch of batches(rows, batchSize)) {
|
|
4127
|
-
await upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, newestWinsColumn);
|
|
4128
|
-
result.rowsWritten += batch.length;
|
|
4129
|
-
onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
|
|
4130
|
-
}
|
|
4131
|
-
onProgress?.({ table, phase: "done", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
|
|
4132
|
-
} catch (error) {
|
|
4133
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
4134
|
-
}
|
|
4135
|
-
results.push(result);
|
|
4136
|
-
}
|
|
4137
|
-
} finally {
|
|
4138
|
-
if (sqliteTarget) {
|
|
4139
|
-
try {
|
|
4140
|
-
sqliteTarget.exec("PRAGMA foreign_keys = ON");
|
|
4141
|
-
} catch {}
|
|
4142
|
-
}
|
|
4143
|
-
}
|
|
4144
|
-
return results;
|
|
4145
|
-
}
|
|
4146
|
-
function batches(rows, size) {
|
|
4147
|
-
const result = [];
|
|
4148
|
-
for (let offset = 0;offset < rows.length; offset += size)
|
|
4149
|
-
result.push(rows.slice(offset, offset + size));
|
|
4150
|
-
return result;
|
|
4151
|
-
}
|
|
4152
|
-
async function upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, conflictColumn) {
|
|
4153
|
-
if (batch.length === 0 || columns.length === 0)
|
|
4154
|
-
return;
|
|
4155
|
-
const fallbackKey = primaryKeys[0] ?? columns[0] ?? "id";
|
|
4156
|
-
const columnList = columns.map(quoteIdent).join(", ");
|
|
4157
|
-
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
4158
|
-
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
4159
|
-
const whereClause = conflictColumn && updateColumns.includes(conflictColumn) ? ` WHERE ${quoteIdent(table)}.${quoteIdent(conflictColumn)} IS NULL OR EXCLUDED.${quoteIdent(conflictColumn)} >= ${quoteIdent(table)}.${quoteIdent(conflictColumn)}` : "";
|
|
4160
|
-
if (isAsyncAdapter(target)) {
|
|
4161
|
-
const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
|
|
4162
|
-
const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
|
|
4163
|
-
await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}
|
|
4164
|
-
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params2);
|
|
4165
|
-
return;
|
|
4166
|
-
}
|
|
4167
|
-
const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
4168
|
-
const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
|
|
4169
|
-
target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}
|
|
4170
|
-
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params);
|
|
4171
|
-
}
|
|
4172
|
-
async function insertBatch(target, table, columns, batch) {
|
|
4173
|
-
if (batch.length === 0 || columns.length === 0)
|
|
4174
|
-
return;
|
|
4175
|
-
const columnList = columns.map(quoteIdent).join(", ");
|
|
4176
|
-
if (isAsyncAdapter(target)) {
|
|
4177
|
-
const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
|
|
4178
|
-
const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
|
|
4179
|
-
await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}`, ...params2);
|
|
4180
|
-
return;
|
|
4181
|
-
}
|
|
4182
|
-
const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
4183
|
-
const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
|
|
4184
|
-
target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}`, ...params);
|
|
4185
|
-
}
|
|
4186
|
-
function coerceForSqlite(value) {
|
|
4187
|
-
if (value === null || value === undefined)
|
|
4188
|
-
return null;
|
|
4189
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
4190
|
-
return value;
|
|
4191
|
-
if (value instanceof Date)
|
|
4192
|
-
return value.toISOString();
|
|
4193
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
4194
|
-
return value;
|
|
4195
|
-
if (typeof value === "object")
|
|
4196
|
-
return JSON.stringify(value);
|
|
4197
|
-
return String(value);
|
|
4198
|
-
}
|
|
4199
|
-
function isAsyncAdapter(adapter) {
|
|
4200
|
-
return adapter instanceof PgAdapterAsync;
|
|
4201
|
-
}
|
|
4202
|
-
async function readAll(adapter, sql) {
|
|
4203
|
-
const rows = adapter.all(sql);
|
|
4204
|
-
return rows instanceof Promise ? await rows : rows;
|
|
4205
|
-
}
|
|
4206
|
-
|
|
4207
3785
|
// src/lib/cloud-sync.ts
|
|
4208
3786
|
var CLOUD_TABLES = [
|
|
4209
3787
|
"requests",
|
|
@@ -4237,6 +3815,7 @@ async function getCloudPg() {
|
|
|
4237
3815
|
if (!url) {
|
|
4238
3816
|
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
4239
3817
|
}
|
|
3818
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
4240
3819
|
return new PgAdapterAsync(url);
|
|
4241
3820
|
}
|
|
4242
3821
|
async function runCloudMigrations(cloud) {
|
|
@@ -4246,52 +3825,40 @@ async function runCloudMigrations(cloud) {
|
|
|
4246
3825
|
}
|
|
4247
3826
|
}
|
|
4248
3827
|
async function cloudPush(opts) {
|
|
3828
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
4249
3829
|
const cloud = await getCloudPg();
|
|
4250
|
-
const local =
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
local.close();
|
|
4260
|
-
await cloud.close();
|
|
4261
|
-
}
|
|
3830
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3831
|
+
await runCloudMigrations(cloud);
|
|
3832
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3833
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3834
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3835
|
+
touchMachineRegistry(local, "push");
|
|
3836
|
+
local.close();
|
|
3837
|
+
await cloud.close();
|
|
3838
|
+
return { rows, machine: getMachineId() };
|
|
4262
3839
|
}
|
|
4263
3840
|
async function cloudPull(opts) {
|
|
3841
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
4264
3842
|
const cloud = await getCloudPg();
|
|
4265
|
-
const local =
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
local.close();
|
|
4276
|
-
await cloud.close();
|
|
4277
|
-
}
|
|
3843
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3844
|
+
await runCloudMigrations(cloud);
|
|
3845
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3846
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3847
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3848
|
+
touchMachineRegistry(local, "pull");
|
|
3849
|
+
local.close();
|
|
3850
|
+
await cloud.close();
|
|
3851
|
+
setLastCloudPull();
|
|
3852
|
+
return { rows, machine: getMachineId() };
|
|
4278
3853
|
}
|
|
4279
3854
|
function setLastCloudPull(at = new Date().toISOString()) {
|
|
4280
|
-
const db = openDatabase(
|
|
4281
|
-
|
|
4282
|
-
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
4283
|
-
} finally {
|
|
4284
|
-
db.close();
|
|
4285
|
-
}
|
|
3855
|
+
const db = openDatabase();
|
|
3856
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
4286
3857
|
}
|
|
4287
3858
|
function getLastCloudPull() {
|
|
4288
|
-
const db = openDatabase(
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
return row?.value ?? null;
|
|
4292
|
-
} finally {
|
|
4293
|
-
db.close();
|
|
4294
|
-
}
|
|
3859
|
+
const db = openDatabase();
|
|
3860
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
3861
|
+
return row?.value ?? null;
|
|
4295
3862
|
}
|
|
4296
3863
|
function shouldPullFromCloud() {
|
|
4297
3864
|
if (!getCloudDatabaseUrl())
|
|
@@ -4337,19 +3904,6 @@ function touchMachineRegistry(db, direction) {
|
|
|
4337
3904
|
updated_at = excluded.updated_at
|
|
4338
3905
|
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
4339
3906
|
}
|
|
4340
|
-
function resolveCloudTables(tables) {
|
|
4341
|
-
if (!tables || tables.length === 0)
|
|
4342
|
-
return [...CLOUD_TABLES];
|
|
4343
|
-
const allowed = new Set(CLOUD_TABLES);
|
|
4344
|
-
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
4345
|
-
const invalid = requested.filter((table) => !allowed.has(table));
|
|
4346
|
-
if (invalid.length > 0) {
|
|
4347
|
-
throw new Error(`Unknown economy sync table(s): ${invalid.join(", ")}`);
|
|
4348
|
-
}
|
|
4349
|
-
return requested;
|
|
4350
|
-
}
|
|
4351
|
-
var SCHEDULE_CONFIG_DIR = join9(homedir9(), ".hasna", "economy");
|
|
4352
|
-
var SCHEDULE_CONFIG_PATH = join9(SCHEDULE_CONFIG_DIR, "cloud-sync-schedule.json");
|
|
4353
3907
|
|
|
4354
3908
|
// src/lib/sync-all.ts
|
|
4355
3909
|
async function syncAll(db, opts = {}) {
|
package/dist/server/serve.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAuC7D,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AAqED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAqX/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAcvG"}
|