@hasna/instructions 0.3.1 → 0.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 (63) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1305 -837
  4. package/dist/data/config-store.d.ts +134 -0
  5. package/dist/data/config-store.d.ts.map +1 -0
  6. package/dist/data/config-store.test.d.ts +2 -0
  7. package/dist/data/config-store.test.d.ts.map +1 -0
  8. package/dist/db/database.d.ts +15 -0
  9. package/dist/db/database.d.ts.map +1 -1
  10. package/dist/index.d.ts +6 -8
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +908 -492
  13. package/dist/lib/apply.d.ts +2 -2
  14. package/dist/lib/apply.d.ts.map +1 -1
  15. package/dist/lib/export.d.ts +2 -2
  16. package/dist/lib/export.d.ts.map +1 -1
  17. package/dist/lib/import.d.ts +2 -2
  18. package/dist/lib/import.d.ts.map +1 -1
  19. package/dist/lib/package-manager-guard.d.ts +24 -0
  20. package/dist/lib/package-manager-guard.d.ts.map +1 -0
  21. package/dist/lib/package-manager-guard.test.d.ts +2 -0
  22. package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
  23. package/dist/lib/platform-profiles.d.ts +2 -2
  24. package/dist/lib/platform-profiles.d.ts.map +1 -1
  25. package/dist/lib/project-dashboard-standard.d.ts +2 -2
  26. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  27. package/dist/lib/redact.d.ts.map +1 -1
  28. package/dist/lib/sync-dir.d.ts +3 -3
  29. package/dist/lib/sync-dir.d.ts.map +1 -1
  30. package/dist/lib/sync.d.ts +6 -6
  31. package/dist/lib/sync.d.ts.map +1 -1
  32. package/dist/mcp/http.d.ts +0 -13
  33. package/dist/mcp/http.d.ts.map +1 -1
  34. package/dist/mcp/index.js +661 -574
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +1757 -17541
  38. package/dist/server/v1.d.ts.map +1 -1
  39. package/dist/status.d.ts +2 -2
  40. package/dist/status.d.ts.map +1 -1
  41. package/dist/storage/cloud-store.d.ts +21 -1
  42. package/dist/storage/cloud-store.d.ts.map +1 -1
  43. package/dist/storage/schema.d.ts.map +1 -1
  44. package/package.json +4 -7
  45. package/dashboard/dist/assets/index-D7p6fFQw.js +0 -11
  46. package/dashboard/dist/assets/index-DQ3P1g1z.css +0 -1
  47. package/dashboard/dist/index.html +0 -14
  48. package/dashboard/dist/vite.svg +0 -1
  49. package/dist/cli/storage.d.ts +0 -3
  50. package/dist/cli/storage.d.ts.map +0 -1
  51. package/dist/cli/storage.test.d.ts +0 -2
  52. package/dist/cli/storage.test.d.ts.map +0 -1
  53. package/dist/db/remote-storage.d.ts +0 -13
  54. package/dist/db/remote-storage.d.ts.map +0 -1
  55. package/dist/db/storage-sync.d.ts +0 -53
  56. package/dist/db/storage-sync.d.ts.map +0 -1
  57. package/dist/db/storage-sync.test.d.ts +0 -2
  58. package/dist/db/storage-sync.test.d.ts.map +0 -1
  59. package/dist/server/server.test.d.ts +0 -2
  60. package/dist/server/server.test.d.ts.map +0 -1
  61. package/dist/storage.d.ts +0 -5
  62. package/dist/storage.d.ts.map +0 -1
  63. package/dist/storage.js +0 -537
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
@@ -1,3 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare function registerStorageCommands(program: Command): void;
3
- //# sourceMappingURL=storage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/cli/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6C9D"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=storage.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage.test.d.ts","sourceRoot":"","sources":["../../src/cli/storage.test.ts"],"names":[],"mappings":""}
@@ -1,13 +0,0 @@
1
- import type { PoolConfig } from "pg";
2
- export declare function isLocalPostgresHost(hostname: string): boolean;
3
- export declare function buildPgPoolConfig(connectionString: string): PoolConfig;
4
- export declare class PgAdapterAsync {
5
- private readonly pool;
6
- constructor(connectionString: string);
7
- run(sql: string, ...params: unknown[]): Promise<{
8
- changes: number;
9
- }>;
10
- all(sql: string, ...params: unknown[]): Promise<unknown[]>;
11
- close(): Promise<void>;
12
- }
13
- //# sourceMappingURL=remote-storage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"remote-storage.d.ts","sourceRoot":"","sources":["../../src/db/remote-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAQ,UAAU,EAAE,MAAM,IAAI,CAAC;AAuB3C,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAG7D;AAQD,wBAAgB,iBAAiB,CAAC,gBAAgB,EAAE,MAAM,GAAG,UAAU,CAyBtE;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;gBAEhB,gBAAgB,EAAE,MAAM;IAI9B,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAKpE,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAK1D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
@@ -1,53 +0,0 @@
1
- import { PgAdapterAsync } from "./remote-storage.js";
2
- export declare const STORAGE_TABLES: readonly ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
3
- export declare const CONFIGS_STORAGE_TABLES: readonly ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
4
- type StorageTable = (typeof STORAGE_TABLES)[number];
5
- export type StorageMode = "local" | "hybrid" | "remote";
6
- export interface SyncResult {
7
- table: string;
8
- rowsRead: number;
9
- rowsWritten: number;
10
- errors: string[];
11
- }
12
- export interface SyncMeta {
13
- table_name: string;
14
- last_synced_at: string | null;
15
- direction: "push" | "pull";
16
- }
17
- export interface StorageStatus {
18
- configured: boolean;
19
- mode: StorageMode;
20
- env: typeof STORAGE_DATABASE_ENV;
21
- service: "configs";
22
- tables: typeof STORAGE_TABLES;
23
- sync: SyncMeta[];
24
- }
25
- export declare const CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
26
- export declare const CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
27
- export declare const CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
28
- export declare const CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
29
- export declare const STORAGE_DATABASE_ENV: readonly ["HASNA_CONFIGS_DATABASE_URL", "CONFIGS_DATABASE_URL"];
30
- export declare const STORAGE_MODE_ENV: readonly ["HASNA_CONFIGS_STORAGE_MODE", "CONFIGS_STORAGE_MODE"];
31
- export declare function getStorageDatabaseUrl(): string | null;
32
- export declare function getStorageDatabaseEnvName(): (typeof STORAGE_DATABASE_ENV)[number] | null;
33
- export declare function getStorageMode(): StorageMode;
34
- export declare function getStoragePg(): Promise<PgAdapterAsync>;
35
- export declare function runStorageMigrations(remote: PgAdapterAsync): Promise<void>;
36
- export declare function storagePush(options?: {
37
- tables?: string[];
38
- }): Promise<SyncResult[]>;
39
- export declare function storagePull(options?: {
40
- tables?: string[];
41
- }): Promise<SyncResult[]>;
42
- export declare function storageSync(options?: {
43
- tables?: string[];
44
- }): Promise<{
45
- pull: SyncResult[];
46
- push: SyncResult[];
47
- }>;
48
- export declare function getStorageSyncMetaAll(): SyncMeta[];
49
- export declare function getSyncMetaAll(): SyncMeta[];
50
- export declare function getStorageStatus(): StorageStatus;
51
- export declare function resolveTables(tables?: string[]): StorageTable[];
52
- export {};
53
- //# sourceMappingURL=storage-sync.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage-sync.d.ts","sourceRoot":"","sources":["../../src/db/storage-sync.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,eAAO,MAAM,cAAc,iGAAkG,CAAC;AAC9H,eAAO,MAAM,sBAAsB,iGAAiB,CAAC;AAErD,KAAK,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAWxD,MAAM,WAAW,UAAU;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAC;CAAE;AACvG,MAAM,WAAW,QAAQ;IAAG,UAAU,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAAE;AAC5G,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,WAAW,CAAC;IAClB,GAAG,EAAE,OAAO,oBAAoB,CAAC;IACjC,OAAO,EAAE,SAAS,CAAC;IACnB,MAAM,EAAE,OAAO,cAAc,CAAC;IAC9B,IAAI,EAAE,QAAQ,EAAE,CAAC;CAClB;AAED,eAAO,MAAM,mBAAmB,+BAA+B,CAAC;AAChE,eAAO,MAAM,4BAA4B,yBAAyB,CAAC;AACnE,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AACrE,eAAO,MAAM,iCAAiC,yBAAyB,CAAC;AACxE,eAAO,MAAM,oBAAoB,iEAA+D,CAAC;AACjG,eAAO,MAAM,gBAAgB,iEAAyE,CAAC;AAgBvG,wBAAgB,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAErD;AAED,wBAAgB,yBAAyB,IAAI,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAKxF;AAED,wBAAgB,cAAc,IAAI,WAAW,CAI5C;AAED,wBAAsB,YAAY,IAAI,OAAO,CAAC,cAAc,CAAC,CAI5D;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAGhF;AAED,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAYxF;AAED,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAYxF;AAED,wBAAsB,WAAW,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC;IAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAAC,IAAI,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAItH;AAED,wBAAgB,qBAAqB,IAAI,QAAQ,EAAE,CAIlD;AAED,wBAAgB,cAAc,IAAI,QAAQ,EAAE,CAE3C;AAED,wBAAgB,gBAAgB,IAAI,aAAa,CAShD;AAED,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,YAAY,EAAE,CAO/D"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=storage-sync.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage-sync.test.d.ts","sourceRoot":"","sources":["../../src/db/storage-sync.test.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=server.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server.test.d.ts","sourceRoot":"","sources":["../../src/server/server.test.ts"],"names":[],"mappings":""}
package/dist/storage.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export { PG_MIGRATIONS } from "./db/pg-migrations.js";
2
- export { PgAdapterAsync } from "./db/remote-storage.js";
3
- export { CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV, CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV, CONFIGS_STORAGE_TABLES, STORAGE_DATABASE_ENV, STORAGE_MODE_ENV, STORAGE_TABLES, getStorageDatabaseEnvName, getStorageDatabaseUrl, getStorageMode, getStoragePg, getStorageStatus, getStorageSyncMetaAll, getSyncMetaAll, resolveTables, runStorageMigrations, storagePull, storagePush, storageSync, } from "./db/storage-sync.js";
4
- export type { StorageMode, StorageStatus, SyncMeta, SyncResult, } from "./db/storage-sync.js";
5
- //# sourceMappingURL=storage.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EACL,mBAAmB,EACnB,4BAA4B,EAC5B,wBAAwB,EACxB,iCAAiC,EACjC,sBAAsB,EACtB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,yBAAyB,EACzB,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,gBAAgB,EAChB,qBAAqB,EACrB,cAAc,EACd,aAAa,EACb,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,WAAW,GACZ,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,WAAW,EACX,aAAa,EACb,QAAQ,EACR,UAAU,GACX,MAAM,sBAAsB,CAAC"}
package/dist/storage.js DELETED
@@ -1,537 +0,0 @@
1
- // @bun
2
- // src/db/pg-migrations.ts
3
- var PG_MIGRATIONS = [
4
- `CREATE TABLE IF NOT EXISTS configs (
5
- id TEXT PRIMARY KEY,
6
- name TEXT NOT NULL,
7
- slug TEXT NOT NULL UNIQUE,
8
- kind TEXT NOT NULL DEFAULT 'file',
9
- category TEXT NOT NULL,
10
- agent TEXT NOT NULL DEFAULT 'global',
11
- target_path TEXT,
12
- outputs TEXT NOT NULL DEFAULT '[]',
13
- format TEXT NOT NULL DEFAULT 'text',
14
- content TEXT NOT NULL DEFAULT '',
15
- description TEXT,
16
- tags TEXT NOT NULL DEFAULT '[]',
17
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
18
- version INTEGER NOT NULL DEFAULT 1,
19
- created_at TEXT NOT NULL,
20
- updated_at TEXT NOT NULL,
21
- synced_at TEXT
22
- )`,
23
- `CREATE TABLE IF NOT EXISTS config_snapshots (
24
- id TEXT PRIMARY KEY,
25
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
26
- content TEXT NOT NULL,
27
- version INTEGER NOT NULL,
28
- created_at TEXT NOT NULL
29
- )`,
30
- `CREATE TABLE IF NOT EXISTS profiles (
31
- id TEXT PRIMARY KEY,
32
- name TEXT NOT NULL,
33
- slug TEXT NOT NULL UNIQUE,
34
- description TEXT,
35
- selectors TEXT NOT NULL DEFAULT '{}',
36
- variables TEXT NOT NULL DEFAULT '{}',
37
- created_at TEXT NOT NULL,
38
- updated_at TEXT NOT NULL
39
- )`,
40
- `CREATE TABLE IF NOT EXISTS profile_configs (
41
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
42
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
43
- sort_order INTEGER NOT NULL DEFAULT 0,
44
- PRIMARY KEY (profile_id, config_id)
45
- )`,
46
- `CREATE TABLE IF NOT EXISTS machines (
47
- id TEXT PRIMARY KEY,
48
- hostname TEXT NOT NULL UNIQUE,
49
- os TEXT,
50
- arch TEXT,
51
- last_applied_at TEXT,
52
- created_at TEXT NOT NULL
53
- )`,
54
- `CREATE TABLE IF NOT EXISTS feedback (
55
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
56
- message TEXT NOT NULL,
57
- email TEXT,
58
- category TEXT DEFAULT 'general',
59
- version TEXT,
60
- machine_id TEXT,
61
- created_at TEXT NOT NULL DEFAULT NOW()::text
62
- )`,
63
- `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
64
- ];
65
- // src/db/remote-storage.ts
66
- import pg from "pg";
67
- var DISABLED_SSL_MODE = "disable";
68
- function translatePlaceholders(sql) {
69
- let index = 0;
70
- return sql.replace(/\?/g, () => `$${++index}`);
71
- }
72
- function normalizeParams(params) {
73
- const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
74
- return flat.map((value) => value === undefined ? null : value);
75
- }
76
- function normalizeHost(hostname) {
77
- const stripped = hostname.replace(/^\[/, "").replace(/\]$/, "");
78
- try {
79
- return decodeURIComponent(stripped).toLowerCase();
80
- } catch {
81
- return stripped.toLowerCase();
82
- }
83
- }
84
- function isLocalPostgresHost(hostname) {
85
- const host = normalizeHost(hostname);
86
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
87
- }
88
- function effectivePgHost(url) {
89
- const hosts = url.searchParams.getAll("host");
90
- const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
91
- return finalHost?.trim() ? finalHost : url.hostname;
92
- }
93
- function buildPgPoolConfig(connectionString) {
94
- let url;
95
- try {
96
- url = new URL(connectionString);
97
- } catch {
98
- throw new Error("Invalid PostgreSQL connection string");
99
- }
100
- const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
101
- const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
102
- const isLocal = isLocalPostgresHost(effectivePgHost(url));
103
- const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
104
- if (!isLocal && hasDisabledSsl) {
105
- throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
106
- }
107
- const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
108
- url.searchParams.delete("sslmode");
109
- url.searchParams.delete("ssl");
110
- return {
111
- connectionString: url.toString(),
112
- ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
113
- };
114
- }
115
-
116
- class PgAdapterAsync {
117
- pool;
118
- constructor(connectionString) {
119
- this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
120
- }
121
- async run(sql, ...params) {
122
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
123
- return { changes: result.rowCount ?? 0 };
124
- }
125
- async all(sql, ...params) {
126
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
127
- return result.rows;
128
- }
129
- async close() {
130
- await this.pool.end();
131
- }
132
- }
133
- // src/db/database.ts
134
- import { Database } from "bun:sqlite";
135
- import { cpSync, existsSync, mkdirSync, statSync } from "fs";
136
- import { join } from "path";
137
- import { randomUUID } from "crypto";
138
- function getDbPath() {
139
- if (process.env["HASNA_CONFIGS_DB_PATH"]) {
140
- return process.env["HASNA_CONFIGS_DB_PATH"];
141
- }
142
- if (process.env["CONFIGS_DB_PATH"]) {
143
- return process.env["CONFIGS_DB_PATH"];
144
- }
145
- migrateDotfile();
146
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
147
- const dir = join(home, ".hasna", "configs");
148
- mkdirSync(dir, { recursive: true });
149
- return join(dir, "configs.db");
150
- }
151
- function uuid() {
152
- return randomUUID();
153
- }
154
- function now() {
155
- return new Date().toISOString();
156
- }
157
- function slugify(name) {
158
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
159
- }
160
- var MIGRATIONS = [
161
- `
162
- CREATE TABLE IF NOT EXISTS configs (
163
- id TEXT PRIMARY KEY,
164
- name TEXT NOT NULL,
165
- slug TEXT NOT NULL UNIQUE,
166
- kind TEXT NOT NULL DEFAULT 'file',
167
- category TEXT NOT NULL,
168
- agent TEXT NOT NULL DEFAULT 'global',
169
- target_path TEXT,
170
- format TEXT NOT NULL DEFAULT 'text',
171
- content TEXT NOT NULL DEFAULT '',
172
- description TEXT,
173
- tags TEXT NOT NULL DEFAULT '[]',
174
- is_template INTEGER NOT NULL DEFAULT 0,
175
- version INTEGER NOT NULL DEFAULT 1,
176
- created_at TEXT NOT NULL,
177
- updated_at TEXT NOT NULL,
178
- synced_at TEXT
179
- );
180
-
181
- CREATE TABLE IF NOT EXISTS config_snapshots (
182
- id TEXT PRIMARY KEY,
183
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
184
- content TEXT NOT NULL,
185
- version INTEGER NOT NULL,
186
- created_at TEXT NOT NULL
187
- );
188
-
189
- CREATE TABLE IF NOT EXISTS profiles (
190
- id TEXT PRIMARY KEY,
191
- name TEXT NOT NULL,
192
- slug TEXT NOT NULL UNIQUE,
193
- description TEXT,
194
- created_at TEXT NOT NULL,
195
- updated_at TEXT NOT NULL
196
- );
197
-
198
- CREATE TABLE IF NOT EXISTS profile_configs (
199
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
200
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
201
- sort_order INTEGER NOT NULL DEFAULT 0,
202
- PRIMARY KEY (profile_id, config_id)
203
- );
204
-
205
- CREATE TABLE IF NOT EXISTS machines (
206
- id TEXT PRIMARY KEY,
207
- hostname TEXT NOT NULL UNIQUE,
208
- os TEXT,
209
- last_applied_at TEXT,
210
- created_at TEXT NOT NULL
211
- );
212
-
213
- CREATE TABLE IF NOT EXISTS schema_version (
214
- version INTEGER PRIMARY KEY
215
- );
216
-
217
- INSERT OR IGNORE INTO schema_version (version) VALUES (1);
218
- `,
219
- `
220
- ALTER TABLE profiles ADD COLUMN selectors TEXT NOT NULL DEFAULT '{}';
221
- ALTER TABLE profiles ADD COLUMN variables TEXT NOT NULL DEFAULT '{}';
222
- ALTER TABLE machines ADD COLUMN arch TEXT;
223
- `,
224
- `
225
- ALTER TABLE configs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]';
226
- `
227
- ];
228
- var _db = null;
229
- function getDatabase(path) {
230
- if (_db)
231
- return _db;
232
- const dbPath = path || getDbPath();
233
- const db = new Database(dbPath);
234
- db.run("PRAGMA journal_mode = WAL");
235
- db.run("PRAGMA foreign_keys = ON");
236
- applyMigrations(db);
237
- ensureFeedbackTable(db);
238
- _db = db;
239
- return db;
240
- }
241
- function resetDatabase() {
242
- if (_db) {
243
- try {
244
- _db.close();
245
- } catch {}
246
- }
247
- _db = null;
248
- }
249
- function applyMigrations(db) {
250
- let currentVersion = 0;
251
- try {
252
- const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
253
- currentVersion = row?.version ?? 0;
254
- } catch {
255
- currentVersion = 0;
256
- }
257
- for (let i = currentVersion;i < MIGRATIONS.length; i++) {
258
- db.exec(MIGRATIONS[i]);
259
- db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${i + 1})`);
260
- }
261
- }
262
- function ensureFeedbackTable(db) {
263
- db.exec(`
264
- CREATE TABLE IF NOT EXISTS feedback (
265
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
266
- message TEXT NOT NULL,
267
- email TEXT,
268
- category TEXT DEFAULT 'general',
269
- version TEXT,
270
- machine_id TEXT,
271
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
272
- )
273
- `);
274
- }
275
- function migrateDotfile() {
276
- const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
277
- const oldDirs = [join(home, ".open-configs"), join(home, ".configs")];
278
- const newDir = join(home, ".hasna", "configs");
279
- if (existsSync(newDir))
280
- return;
281
- for (const oldDir of oldDirs) {
282
- if (!existsSync(oldDir))
283
- continue;
284
- try {
285
- if (!statSync(oldDir).isDirectory())
286
- continue;
287
- mkdirSync(join(home, ".hasna"), { recursive: true });
288
- cpSync(oldDir, newDir, { recursive: true, force: false });
289
- return;
290
- } catch {}
291
- }
292
- }
293
-
294
- // src/db/storage-sync.ts
295
- var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
296
- var CONFIGS_STORAGE_TABLES = STORAGE_TABLES;
297
- var PRIMARY_KEYS = {
298
- configs: ["id"],
299
- config_snapshots: ["id"],
300
- profiles: ["id"],
301
- profile_configs: ["profile_id", "config_id"],
302
- machines: ["id"],
303
- feedback: ["id"]
304
- };
305
- var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
306
- var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
307
- var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
308
- var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
309
- var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
310
- var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
311
- function firstEnv(names) {
312
- for (const name of names) {
313
- const value = process.env[name];
314
- if (value)
315
- return value;
316
- }
317
- return null;
318
- }
319
- function normalizeStorageMode(value) {
320
- const normalized = value?.trim().toLowerCase();
321
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
322
- return normalized;
323
- return;
324
- }
325
- function getStorageDatabaseUrl() {
326
- return firstEnv(STORAGE_DATABASE_ENV);
327
- }
328
- function getStorageDatabaseEnvName() {
329
- for (const name of STORAGE_DATABASE_ENV) {
330
- if (process.env[name])
331
- return name;
332
- }
333
- return null;
334
- }
335
- function getStorageMode() {
336
- const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
337
- if (mode)
338
- return mode;
339
- return getStorageDatabaseUrl() ? "hybrid" : "local";
340
- }
341
- async function getStoragePg() {
342
- const url = getStorageDatabaseUrl();
343
- if (!url)
344
- throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
345
- return new PgAdapterAsync(url);
346
- }
347
- async function runStorageMigrations(remote) {
348
- await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
349
- for (const sql of PG_MIGRATIONS)
350
- await remote.run(sql);
351
- }
352
- async function storagePush(options) {
353
- const remote = await getStoragePg();
354
- const db = getDatabase();
355
- try {
356
- await runStorageMigrations(remote);
357
- const results = [];
358
- for (const table of resolveTables(options?.tables))
359
- results.push(await pushTable(db, remote, table));
360
- recordSyncMeta(db, "push", results);
361
- return results;
362
- } finally {
363
- await remote.close();
364
- }
365
- }
366
- async function storagePull(options) {
367
- const remote = await getStoragePg();
368
- const db = getDatabase();
369
- try {
370
- await runStorageMigrations(remote);
371
- const results = [];
372
- for (const table of resolveTables(options?.tables))
373
- results.push(await pullTable(remote, db, table));
374
- recordSyncMeta(db, "pull", results);
375
- return results;
376
- } finally {
377
- await remote.close();
378
- }
379
- }
380
- async function storageSync(options) {
381
- const pull = await storagePull(options);
382
- const push = await storagePush(options);
383
- return { pull, push };
384
- }
385
- function getStorageSyncMetaAll() {
386
- const db = getDatabase();
387
- ensureSyncMetaTable(db);
388
- return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
389
- }
390
- function getSyncMetaAll() {
391
- return getStorageSyncMetaAll();
392
- }
393
- function getStorageStatus() {
394
- return {
395
- configured: Boolean(getStorageDatabaseUrl()),
396
- mode: getStorageMode(),
397
- env: STORAGE_DATABASE_ENV,
398
- service: "configs",
399
- tables: STORAGE_TABLES,
400
- sync: getStorageSyncMetaAll()
401
- };
402
- }
403
- function resolveTables(tables) {
404
- if (!tables || tables.length === 0)
405
- return [...STORAGE_TABLES];
406
- const allowed = new Set(STORAGE_TABLES);
407
- const requested = tables.map((table) => table.trim()).filter(Boolean);
408
- const invalid = requested.filter((table) => !allowed.has(table));
409
- if (invalid.length > 0)
410
- throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
411
- return requested;
412
- }
413
- async function pushTable(db, remote, table) {
414
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
415
- try {
416
- const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
417
- result.rowsRead = rows.length;
418
- if (rows.length === 0)
419
- return result;
420
- const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
421
- result.rowsWritten = await upsertPg(remote, table, columns, rows);
422
- } catch (error) {
423
- result.errors.push(error instanceof Error ? error.message : String(error));
424
- }
425
- return result;
426
- }
427
- async function pullTable(remote, db, table) {
428
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
429
- try {
430
- const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
431
- result.rowsRead = rows.length;
432
- if (rows.length === 0)
433
- return result;
434
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
435
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
436
- } catch (error) {
437
- result.errors.push(error instanceof Error ? error.message : String(error));
438
- }
439
- return result;
440
- }
441
- async function filterRemoteColumns(remote, table, columns) {
442
- const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
443
- if (rows.length === 0)
444
- return columns;
445
- const allowed = new Set(rows.map((row) => row.column_name));
446
- return columns.filter((column) => allowed.has(column));
447
- }
448
- function filterLocalColumns(db, table, columns) {
449
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
450
- const allowed = new Set(rows.map((row) => row.name));
451
- return columns.filter((column) => allowed.has(column));
452
- }
453
- async function upsertPg(remote, table, columns, rows) {
454
- if (columns.length === 0)
455
- return 0;
456
- const primaryKeys = PRIMARY_KEYS[table];
457
- const columnList = columns.map(quoteIdent).join(", ");
458
- const placeholders = columns.map(() => "?").join(", ");
459
- const keyList = primaryKeys.map(quoteIdent).join(", ");
460
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
461
- const fallbackKey = primaryKeys[0];
462
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
463
- for (const row of rows) {
464
- await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
465
- }
466
- return rows.length;
467
- }
468
- function upsertSqlite(db, table, columns, rows) {
469
- if (columns.length === 0)
470
- return 0;
471
- const primaryKeys = PRIMARY_KEYS[table];
472
- const columnList = columns.map(quoteIdent).join(", ");
473
- const placeholders = columns.map(() => "?").join(", ");
474
- const keyList = primaryKeys.map(quoteIdent).join(", ");
475
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
476
- const fallbackKey = primaryKeys[0];
477
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
478
- const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
479
- db.transaction((batch) => {
480
- for (const row of batch)
481
- statement.run(...columns.map((column) => coerceForSqlite(row[column])));
482
- })(rows);
483
- return rows.length;
484
- }
485
- function recordSyncMeta(db, direction, results) {
486
- ensureSyncMetaTable(db);
487
- const now2 = new Date().toISOString();
488
- const statement = db.prepare("INSERT INTO _configs_sync_meta (table_name, last_synced_at, direction) VALUES (?, ?, ?) ON CONFLICT(table_name, direction) DO UPDATE SET last_synced_at = excluded.last_synced_at");
489
- for (const result of results) {
490
- if (result.errors.length > 0)
491
- continue;
492
- statement.run(result.table, now2, direction);
493
- }
494
- }
495
- function ensureSyncMetaTable(db) {
496
- db.exec("CREATE TABLE IF NOT EXISTS _configs_sync_meta (table_name TEXT NOT NULL, last_synced_at TEXT, direction TEXT NOT NULL CHECK(direction IN ('push', 'pull')), PRIMARY KEY (table_name, direction))");
497
- }
498
- function quoteIdent(identifier) {
499
- return `"${identifier.replace(/"/g, '""')}"`;
500
- }
501
- function coerceForSqlite(value) {
502
- if (value === undefined || value === null)
503
- return null;
504
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
505
- return value;
506
- if (value instanceof Date)
507
- return value.toISOString();
508
- if (Buffer.isBuffer(value) || value instanceof Uint8Array)
509
- return value;
510
- if (typeof value === "object")
511
- return JSON.stringify(value);
512
- return String(value);
513
- }
514
- export {
515
- storageSync,
516
- storagePush,
517
- storagePull,
518
- runStorageMigrations,
519
- resolveTables,
520
- getSyncMetaAll,
521
- getStorageSyncMetaAll,
522
- getStorageStatus,
523
- getStoragePg,
524
- getStorageMode,
525
- getStorageDatabaseUrl,
526
- getStorageDatabaseEnvName,
527
- STORAGE_TABLES,
528
- STORAGE_MODE_ENV,
529
- STORAGE_DATABASE_ENV,
530
- PgAdapterAsync,
531
- PG_MIGRATIONS,
532
- CONFIGS_STORAGE_TABLES,
533
- CONFIGS_STORAGE_MODE_FALLBACK_ENV,
534
- CONFIGS_STORAGE_MODE_ENV,
535
- CONFIGS_STORAGE_FALLBACK_ENV,
536
- CONFIGS_STORAGE_ENV
537
- };