@hasna/economy 0.2.31 → 0.2.33

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 (64) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/commands/tui.d.ts +1 -1
  3. package/dist/cli/commands/tui.d.ts.map +1 -1
  4. package/dist/cli/index.js +184 -832
  5. package/dist/db/database.d.ts +1 -3
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/index.d.ts +0 -4
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +50 -1049
  10. package/dist/ingest/billing.d.ts +1 -1
  11. package/dist/ingest/billing.d.ts.map +1 -1
  12. package/dist/ingest/claude-quota.d.ts +1 -1
  13. package/dist/ingest/claude-quota.d.ts.map +1 -1
  14. package/dist/ingest/claude.d.ts +1 -1
  15. package/dist/ingest/claude.d.ts.map +1 -1
  16. package/dist/ingest/codex-quota.d.ts +1 -1
  17. package/dist/ingest/codex-quota.d.ts.map +1 -1
  18. package/dist/ingest/codex.d.ts +1 -1
  19. package/dist/ingest/codex.d.ts.map +1 -1
  20. package/dist/ingest/cursor.d.ts +1 -1
  21. package/dist/ingest/cursor.d.ts.map +1 -1
  22. package/dist/ingest/gemini.d.ts +1 -1
  23. package/dist/ingest/gemini.d.ts.map +1 -1
  24. package/dist/ingest/hermes.d.ts +1 -1
  25. package/dist/ingest/hermes.d.ts.map +1 -1
  26. package/dist/ingest/opencode.d.ts +1 -1
  27. package/dist/ingest/opencode.d.ts.map +1 -1
  28. package/dist/ingest/otel.d.ts +1 -1
  29. package/dist/ingest/otel.d.ts.map +1 -1
  30. package/dist/ingest/pi.d.ts +1 -1
  31. package/dist/ingest/pi.d.ts.map +1 -1
  32. package/dist/ingest/plugin.d.ts +1 -1
  33. package/dist/ingest/plugin.d.ts.map +1 -1
  34. package/dist/lib/billing-diff.d.ts +1 -1
  35. package/dist/lib/billing-diff.d.ts.map +1 -1
  36. package/dist/lib/cloud-sync.d.ts +2 -9
  37. package/dist/lib/cloud-sync.d.ts.map +1 -1
  38. package/dist/lib/open-projects.d.ts +2 -3
  39. package/dist/lib/open-projects.d.ts.map +1 -1
  40. package/dist/lib/peer-sync.d.ts +1 -1
  41. package/dist/lib/peer-sync.d.ts.map +1 -1
  42. package/dist/lib/pricing.d.ts +1 -1
  43. package/dist/lib/pricing.d.ts.map +1 -1
  44. package/dist/lib/savings.d.ts +1 -1
  45. package/dist/lib/savings.d.ts.map +1 -1
  46. package/dist/lib/spikes.d.ts +1 -1
  47. package/dist/lib/spikes.d.ts.map +1 -1
  48. package/dist/lib/sync-all.d.ts +1 -1
  49. package/dist/lib/sync-all.d.ts.map +1 -1
  50. package/dist/lib/webhooks.d.ts +1 -1
  51. package/dist/lib/webhooks.d.ts.map +1 -1
  52. package/dist/mcp/index.js +34 -514
  53. package/dist/mcp/server.d.ts.map +1 -1
  54. package/dist/otel/index.js +15 -442
  55. package/dist/server/index.js +46 -492
  56. package/dist/server/serve.d.ts +1 -1
  57. package/dist/server/serve.d.ts.map +1 -1
  58. package/package.json +6 -5
  59. package/dist/db/storage-adapter.d.ts +0 -34
  60. package/dist/db/storage-adapter.d.ts.map +0 -1
  61. package/dist/lib/remote-storage.d.ts +0 -15
  62. package/dist/lib/remote-storage.d.ts.map +0 -1
  63. package/dist/lib/storage-sync.d.ts +0 -27
  64. package/dist/lib/storage-sync.d.ts.map +0 -1
@@ -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 SqliteAdapter(path);
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 = __esm(() => {
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 (ANTHROPIC_ADMIN_API_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 (OPENAI_ADMIN_API_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 listOpenProjects = listActiveProjects;
2066
- if (!listOpenProjects) {
2009
+ let listProjects2 = listActiveProjects;
2010
+ if (!listProjects2) {
2067
2011
  const projectsApi = await import("@hasna/projects");
2068
- listOpenProjects = projectsApi.listProjects ?? projectsApi.listWorkspaces;
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 = listOpenProjects({ status: "active", limit: 5000 });
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
- const path = project.path ?? project.primary_path ?? "";
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 as dirname2, join as join10 } from "path";
2040
+ import { dirname, join as join9 } from "path";
2101
2041
  function getConfigPath() {
2102
- return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join10(getDataDir(), "config.json");
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 BunDatabase2 } from "bun:sqlite";
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 BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
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 = openDatabase(getDbPath(), true);
4251
- try {
4252
- await runCloudMigrations(cloud);
4253
- touchMachineRegistry(local, "push");
4254
- const tables = resolveCloudTables(opts?.tables);
4255
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
4256
- const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
4257
- return { rows, machine: getMachineId() };
4258
- } finally {
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 = openDatabase(getDbPath(), true);
4266
- try {
4267
- await runCloudMigrations(cloud);
4268
- const tables = resolveCloudTables(opts?.tables);
4269
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
4270
- const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
4271
- touchMachineRegistry(local, "pull");
4272
- setLastCloudPull();
4273
- return { rows, machine: getMachineId() };
4274
- } finally {
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(undefined, true);
4281
- try {
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(undefined, true);
4289
- try {
4290
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
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 = {}) {
@@ -1,4 +1,4 @@
1
- import type { Database } from '../db/database.js';
1
+ import type { SqliteAdapter as Database } from '@hasna/cloud';
2
2
  interface StartServerOptions {
3
3
  db?: Database;
4
4
  dashboardDir?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAuCjD,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"}
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"}