@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
package/dist/index.js CHANGED
@@ -68,9 +68,12 @@ class TemplateRenderError extends Error {
68
68
  this.name = "TemplateRenderError";
69
69
  }
70
70
  }
71
+ // src/data/config-store.ts
72
+ import { randomUUID as randomUUID2 } from "crypto";
73
+
71
74
  // src/db/database.ts
72
75
  import { Database } from "bun:sqlite";
73
- import { cpSync, existsSync, mkdirSync, statSync } from "fs";
76
+ import { cpSync, existsSync, mkdirSync, rmSync, statSync } from "fs";
74
77
  import { join } from "path";
75
78
  import { randomUUID } from "crypto";
76
79
  function getDbPath() {
@@ -167,6 +170,9 @@ var _db = null;
167
170
  function getDatabase(path) {
168
171
  if (_db)
169
172
  return _db;
173
+ if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
174
+ throw new Error("instructions is in self_hosted (cloud) mode: this command is not wired to the cloud API yet. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to use it against the local store.");
175
+ }
170
176
  const dbPath = path || getDbPath();
171
177
  const db = new Database(dbPath);
172
178
  db.run("PRAGMA journal_mode = WAL");
@@ -184,6 +190,16 @@ function resetDatabase() {
184
190
  }
185
191
  _db = null;
186
192
  }
193
+ function resetLocalDatabase() {
194
+ resetDatabase();
195
+ const dbPath = getDbPath();
196
+ if (dbPath === ":memory:")
197
+ return;
198
+ for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
199
+ if (existsSync(p))
200
+ rmSync(p);
201
+ }
202
+ }
187
203
  function applyMigrations(db) {
188
204
  let currentVersion = 0;
189
205
  try {
@@ -209,6 +225,22 @@ function ensureFeedbackTable(db) {
209
225
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
210
226
  )
211
227
  `);
228
+ const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
229
+ const required = [
230
+ ["email", "TEXT"],
231
+ ["category", "TEXT DEFAULT 'general'"],
232
+ ["version", "TEXT"],
233
+ ["machine_id", "TEXT"],
234
+ ["created_at", "TEXT"]
235
+ ];
236
+ for (const [name, def] of required) {
237
+ if (!existing.has(name))
238
+ db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
239
+ }
240
+ }
241
+ function insertFeedback(input, db) {
242
+ const d = db || getDatabase();
243
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
212
244
  }
213
245
  function migrateDotfile() {
214
246
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
@@ -409,33 +441,7 @@ function getConfigStats(db) {
409
441
  }
410
442
  return stats;
411
443
  }
412
- // src/db/snapshots.ts
413
- function createSnapshot(configId, content, version, db) {
414
- const d = db || getDatabase();
415
- const id = uuid();
416
- const ts = now();
417
- d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
418
- return { id, config_id: configId, content, version, created_at: ts };
419
- }
420
- function listSnapshots(configId, db) {
421
- const d = db || getDatabase();
422
- return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
423
- }
424
- function getSnapshot(id, db) {
425
- const d = db || getDatabase();
426
- return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
427
- }
428
- function getSnapshotByVersion(configId, version, db) {
429
- const d = db || getDatabase();
430
- return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
431
- }
432
- function pruneSnapshots(configId, keep = 10, db) {
433
- const d = db || getDatabase();
434
- const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
435
- SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
436
- )`, [configId, configId, keep]);
437
- return result.changes;
438
- }
444
+
439
445
  // src/lib/machine.ts
440
446
  import { arch as currentArch, homedir, hostname as currentHostname, type as currentOsType } from "os";
441
447
  import { existsSync as existsSync2 } from "fs";
@@ -701,6 +707,35 @@ function resolveProfileForMachine(machine = detectMachineContext(), db) {
701
707
  }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
702
708
  return matches[0]?.profile ?? null;
703
709
  }
710
+
711
+ // src/db/snapshots.ts
712
+ function createSnapshot(configId, content, version, db) {
713
+ const d = db || getDatabase();
714
+ const id = uuid();
715
+ const ts = now();
716
+ d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
717
+ return { id, config_id: configId, content, version, created_at: ts };
718
+ }
719
+ function listSnapshots(configId, db) {
720
+ const d = db || getDatabase();
721
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
722
+ }
723
+ function getSnapshot(id, db) {
724
+ const d = db || getDatabase();
725
+ return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
726
+ }
727
+ function getSnapshotByVersion(configId, version, db) {
728
+ const d = db || getDatabase();
729
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
730
+ }
731
+ function pruneSnapshots(configId, keep = 10, db) {
732
+ const d = db || getDatabase();
733
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
734
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
735
+ )`, [configId, configId, keep]);
736
+ return result.changes;
737
+ }
738
+
704
739
  // src/db/machines.ts
705
740
  import { arch, hostname, type } from "os";
706
741
  function currentHostname2() {
@@ -739,358 +774,319 @@ function listMachines(db) {
739
774
  const d = db || getDatabase();
740
775
  return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
741
776
  }
742
- // src/db/pg-migrations.ts
743
- var PG_MIGRATIONS = [
744
- `CREATE TABLE IF NOT EXISTS configs (
745
- id TEXT PRIMARY KEY,
746
- name TEXT NOT NULL,
747
- slug TEXT NOT NULL UNIQUE,
748
- kind TEXT NOT NULL DEFAULT 'file',
749
- category TEXT NOT NULL,
750
- agent TEXT NOT NULL DEFAULT 'global',
751
- target_path TEXT,
752
- outputs TEXT NOT NULL DEFAULT '[]',
753
- format TEXT NOT NULL DEFAULT 'text',
754
- content TEXT NOT NULL DEFAULT '',
755
- description TEXT,
756
- tags TEXT NOT NULL DEFAULT '[]',
757
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
758
- version INTEGER NOT NULL DEFAULT 1,
759
- created_at TEXT NOT NULL,
760
- updated_at TEXT NOT NULL,
761
- synced_at TEXT
762
- )`,
763
- `CREATE TABLE IF NOT EXISTS config_snapshots (
764
- id TEXT PRIMARY KEY,
765
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
766
- content TEXT NOT NULL,
767
- version INTEGER NOT NULL,
768
- created_at TEXT NOT NULL
769
- )`,
770
- `CREATE TABLE IF NOT EXISTS profiles (
771
- id TEXT PRIMARY KEY,
772
- name TEXT NOT NULL,
773
- slug TEXT NOT NULL UNIQUE,
774
- description TEXT,
775
- selectors TEXT NOT NULL DEFAULT '{}',
776
- variables TEXT NOT NULL DEFAULT '{}',
777
- created_at TEXT NOT NULL,
778
- updated_at TEXT NOT NULL
779
- )`,
780
- `CREATE TABLE IF NOT EXISTS profile_configs (
781
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
782
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
783
- sort_order INTEGER NOT NULL DEFAULT 0,
784
- PRIMARY KEY (profile_id, config_id)
785
- )`,
786
- `CREATE TABLE IF NOT EXISTS machines (
787
- id TEXT PRIMARY KEY,
788
- hostname TEXT NOT NULL UNIQUE,
789
- os TEXT,
790
- arch TEXT,
791
- last_applied_at TEXT,
792
- created_at TEXT NOT NULL
793
- )`,
794
- `CREATE TABLE IF NOT EXISTS feedback (
795
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
796
- message TEXT NOT NULL,
797
- email TEXT,
798
- category TEXT DEFAULT 'general',
799
- version TEXT,
800
- machine_id TEXT,
801
- created_at TEXT NOT NULL DEFAULT NOW()::text
802
- )`,
803
- `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
804
- ];
805
777
 
806
- // src/db/remote-storage.ts
807
- import pg from "pg";
808
- var DISABLED_SSL_MODE = "disable";
809
- function translatePlaceholders(sql) {
810
- let index = 0;
811
- return sql.replace(/\?/g, () => `$${++index}`);
812
- }
813
- function normalizeParams(params) {
814
- const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
815
- return flat.map((value) => value === undefined ? null : value);
816
- }
817
- function normalizeHost(hostname2) {
818
- const stripped = hostname2.replace(/^\[/, "").replace(/\]$/, "");
819
- try {
820
- return decodeURIComponent(stripped).toLowerCase();
821
- } catch {
822
- return stripped.toLowerCase();
778
+ // src/data/config-store.ts
779
+ class CloudHttpError extends Error {
780
+ status;
781
+ body;
782
+ constructor(status, message, body) {
783
+ super(message);
784
+ this.status = status;
785
+ this.body = body;
786
+ this.name = "CloudHttpError";
823
787
  }
824
788
  }
825
- function isLocalPostgresHost(hostname2) {
826
- const host = normalizeHost(hostname2);
827
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
828
- }
829
- function effectivePgHost(url) {
830
- const hosts = url.searchParams.getAll("host");
831
- const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
832
- return finalHost?.trim() ? finalHost : url.hostname;
789
+ var API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL";
790
+ var API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
791
+ function resolveCloudConfig(env = process.env) {
792
+ const apiUrl = env[API_URL_ENV]?.trim();
793
+ const apiKey = env[API_KEY_ENV]?.trim();
794
+ if (!apiUrl && !apiKey)
795
+ return null;
796
+ if (!apiUrl || !apiKey) {
797
+ throw new Error(`API mode requires BOTH ${API_URL_ENV} and ${API_KEY_ENV}; only ` + `${apiUrl ? API_URL_ENV : API_KEY_ENV} is set. Set both to use the cloud API, ` + `or unset both to use the local store.`);
798
+ }
799
+ return { apiUrl, apiKey };
833
800
  }
834
- function buildPgPoolConfig(connectionString) {
835
- let url;
836
- try {
837
- url = new URL(connectionString);
838
- } catch {
839
- throw new Error("Invalid PostgreSQL connection string");
840
- }
841
- const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
842
- const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
843
- const isLocal = isLocalPostgresHost(effectivePgHost(url));
844
- const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
845
- if (!isLocal && hasDisabledSsl) {
846
- throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
847
- }
848
- const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
849
- url.searchParams.delete("sslmode");
850
- url.searchParams.delete("ssl");
851
- return {
852
- connectionString: url.toString(),
853
- ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
854
- };
801
+ function isCloudMode(env = process.env) {
802
+ return resolveCloudConfig(env) !== null;
855
803
  }
856
804
 
857
- class PgAdapterAsync {
858
- pool;
859
- constructor(connectionString) {
860
- this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
805
+ class LocalConfigStore {
806
+ db;
807
+ mode = "local";
808
+ constructor(db) {
809
+ this.db = db;
810
+ }
811
+ async listConfigs(filter) {
812
+ return listConfigs(filter, this.db);
813
+ }
814
+ async getConfig(idOrSlug) {
815
+ return getConfig(idOrSlug, this.db);
816
+ }
817
+ async getConfigById(id) {
818
+ return getConfigById(id, this.db);
819
+ }
820
+ async createConfig(input) {
821
+ return createConfig(input, this.db);
822
+ }
823
+ async updateConfig(idOrSlug, input) {
824
+ return updateConfig(idOrSlug, input, this.db);
825
+ }
826
+ async deleteConfig(idOrSlug) {
827
+ deleteConfig(idOrSlug, this.db);
861
828
  }
862
- async run(sql, ...params) {
863
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
864
- return { changes: result.rowCount ?? 0 };
829
+ async getConfigStats() {
830
+ return getConfigStats(this.db);
865
831
  }
866
- async all(sql, ...params) {
867
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
868
- return result.rows;
832
+ async listSnapshots(configId) {
833
+ return listSnapshots(configId, this.db);
869
834
  }
870
- async close() {
871
- await this.pool.end();
835
+ async getSnapshot(id) {
836
+ return getSnapshot(id, this.db);
837
+ }
838
+ async getSnapshotByVersion(configId, version) {
839
+ return getSnapshotByVersion(configId, version, this.db);
840
+ }
841
+ async createSnapshot(configId, content, version) {
842
+ return createSnapshot(configId, content, version, this.db);
843
+ }
844
+ async pruneSnapshots(configId, keep = 10) {
845
+ return pruneSnapshots(configId, keep, this.db);
846
+ }
847
+ async listProfiles() {
848
+ return listProfiles(this.db);
849
+ }
850
+ async getProfile(idOrSlug) {
851
+ return getProfile(idOrSlug, this.db);
852
+ }
853
+ async getProfileConfigs(idOrSlug) {
854
+ return getProfileConfigs(idOrSlug, this.db);
855
+ }
856
+ async createProfile(input) {
857
+ return createProfile(input, this.db);
858
+ }
859
+ async updateProfile(idOrSlug, input) {
860
+ return updateProfile(idOrSlug, input, this.db);
861
+ }
862
+ async deleteProfile(idOrSlug) {
863
+ deleteProfile(idOrSlug, this.db);
864
+ }
865
+ async addConfigToProfile(profileIdOrSlug, configId) {
866
+ addConfigToProfile(profileIdOrSlug, configId, this.db);
867
+ }
868
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
869
+ removeConfigFromProfile(profileIdOrSlug, configId, this.db);
870
+ }
871
+ async resolveProfileForMachine(machine) {
872
+ return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
873
+ }
874
+ async registerMachine(hostname2, os, arch2) {
875
+ return registerMachine(hostname2, os, arch2, this.db);
876
+ }
877
+ async updateMachineApplied(hostname2) {
878
+ updateMachineApplied(hostname2, this.db);
879
+ }
880
+ async listMachines() {
881
+ return listMachines(this.db);
882
+ }
883
+ async sendFeedback(input) {
884
+ insertFeedback(input, this.db);
885
+ }
886
+ async reset() {
887
+ resetLocalDatabase();
872
888
  }
873
889
  }
874
890
 
875
- // src/db/storage-sync.ts
876
- var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
877
- var CONFIGS_STORAGE_TABLES = STORAGE_TABLES;
878
- var PRIMARY_KEYS = {
879
- configs: ["id"],
880
- config_snapshots: ["id"],
881
- profiles: ["id"],
882
- profile_configs: ["profile_id", "config_id"],
883
- machines: ["id"],
884
- feedback: ["id"]
885
- };
886
- var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
887
- var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
888
- var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
889
- var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
890
- var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
891
- var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
892
- function firstEnv(names) {
893
- for (const name of names) {
894
- const value = process.env[name];
895
- if (value)
896
- return value;
891
+ class CloudConfigStore {
892
+ mode = "api";
893
+ base;
894
+ apiKey;
895
+ timeoutMs;
896
+ constructor(config) {
897
+ this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
898
+ this.apiKey = config.apiKey;
899
+ this.timeoutMs = config.timeoutMs ?? 30000;
900
+ }
901
+ async request(method, path, body, opts = {}) {
902
+ const controller = new AbortController;
903
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
904
+ const headers = {
905
+ Authorization: `Bearer ${this.apiKey}`,
906
+ Accept: "application/json"
907
+ };
908
+ if (body !== undefined)
909
+ headers["Content-Type"] = "application/json";
910
+ if (opts.idempotent)
911
+ headers["Idempotency-Key"] = randomUUID2();
912
+ try {
913
+ const res = await fetch(`${this.base}${path}`, {
914
+ method,
915
+ headers,
916
+ body: body === undefined ? undefined : JSON.stringify(body),
917
+ signal: controller.signal
918
+ });
919
+ if (res.status === 404 && opts.allow404)
920
+ return { status: 404, data: null };
921
+ const text = await res.text();
922
+ let parsed = null;
923
+ if (text) {
924
+ try {
925
+ parsed = JSON.parse(text);
926
+ } catch {
927
+ parsed = text;
928
+ }
929
+ }
930
+ if (!res.ok) {
931
+ const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
932
+ throw new CloudHttpError(res.status, message, parsed);
933
+ }
934
+ return { status: res.status, data: parsed };
935
+ } finally {
936
+ clearTimeout(timer);
937
+ }
897
938
  }
898
- return null;
899
- }
900
- function normalizeStorageMode(value) {
901
- const normalized = value?.trim().toLowerCase();
902
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
903
- return normalized;
904
- return;
905
- }
906
- function getStorageDatabaseUrl() {
907
- return firstEnv(STORAGE_DATABASE_ENV);
908
- }
909
- function getStorageDatabaseEnvName() {
910
- for (const name of STORAGE_DATABASE_ENV) {
911
- if (process.env[name])
912
- return name;
939
+ async listConfigs(filter = {}) {
940
+ const params = new URLSearchParams;
941
+ if (filter.category)
942
+ params.set("category", filter.category);
943
+ if (filter.agent)
944
+ params.set("agent", filter.agent);
945
+ if (filter.kind)
946
+ params.set("kind", filter.kind);
947
+ if (filter.search)
948
+ params.set("search", filter.search);
949
+ const qs = params.toString();
950
+ const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
951
+ let configs = data?.configs ?? [];
952
+ if (filter.tags && filter.tags.length > 0) {
953
+ configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
954
+ }
955
+ if (filter.is_template !== undefined) {
956
+ configs = configs.filter((c) => c.is_template === filter.is_template);
957
+ }
958
+ return configs;
913
959
  }
914
- return null;
915
- }
916
- function getStorageMode() {
917
- const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
918
- if (mode)
919
- return mode;
920
- return getStorageDatabaseUrl() ? "hybrid" : "local";
921
- }
922
- async function getStoragePg() {
923
- const url = getStorageDatabaseUrl();
924
- if (!url)
925
- throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
926
- return new PgAdapterAsync(url);
927
- }
928
- async function runStorageMigrations(remote) {
929
- await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
930
- for (const sql of PG_MIGRATIONS)
931
- await remote.run(sql);
932
- }
933
- async function storagePush(options) {
934
- const remote = await getStoragePg();
935
- const db = getDatabase();
936
- try {
937
- await runStorageMigrations(remote);
938
- const results = [];
939
- for (const table of resolveTables(options?.tables))
940
- results.push(await pushTable(db, remote, table));
941
- recordSyncMeta(db, "push", results);
942
- return results;
943
- } finally {
944
- await remote.close();
960
+ async getConfig(idOrSlug) {
961
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
962
+ if (status === 404 || !data?.config)
963
+ throw new ConfigNotFoundError(idOrSlug);
964
+ return data.config;
945
965
  }
946
- }
947
- async function storagePull(options) {
948
- const remote = await getStoragePg();
949
- const db = getDatabase();
950
- try {
951
- await runStorageMigrations(remote);
952
- const results = [];
953
- for (const table of resolveTables(options?.tables))
954
- results.push(await pullTable(remote, db, table));
955
- recordSyncMeta(db, "pull", results);
956
- return results;
957
- } finally {
958
- await remote.close();
966
+ async getConfigById(id) {
967
+ return this.getConfig(id);
959
968
  }
960
- }
961
- async function storageSync(options) {
962
- const pull = await storagePull(options);
963
- const push = await storagePush(options);
964
- return { pull, push };
965
- }
966
- function getStorageSyncMetaAll() {
967
- const db = getDatabase();
968
- ensureSyncMetaTable(db);
969
- return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
970
- }
971
- function getSyncMetaAll() {
972
- return getStorageSyncMetaAll();
973
- }
974
- function getStorageStatus() {
975
- return {
976
- configured: Boolean(getStorageDatabaseUrl()),
977
- mode: getStorageMode(),
978
- env: STORAGE_DATABASE_ENV,
979
- service: "configs",
980
- tables: STORAGE_TABLES,
981
- sync: getStorageSyncMetaAll()
982
- };
983
- }
984
- function resolveTables(tables) {
985
- if (!tables || tables.length === 0)
986
- return [...STORAGE_TABLES];
987
- const allowed = new Set(STORAGE_TABLES);
988
- const requested = tables.map((table) => table.trim()).filter(Boolean);
989
- const invalid = requested.filter((table) => !allowed.has(table));
990
- if (invalid.length > 0)
991
- throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
992
- return requested;
993
- }
994
- async function pushTable(db, remote, table) {
995
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
996
- try {
997
- const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
998
- result.rowsRead = rows.length;
999
- if (rows.length === 0)
1000
- return result;
1001
- const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
1002
- result.rowsWritten = await upsertPg(remote, table, columns, rows);
1003
- } catch (error) {
1004
- result.errors.push(error instanceof Error ? error.message : String(error));
969
+ async createConfig(input) {
970
+ const { data } = await this.request("POST", "/configs", input, {
971
+ idempotent: true
972
+ });
973
+ return data.config;
1005
974
  }
1006
- return result;
1007
- }
1008
- async function pullTable(remote, db, table) {
1009
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
1010
- try {
1011
- const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
1012
- result.rowsRead = rows.length;
1013
- if (rows.length === 0)
1014
- return result;
1015
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
1016
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
1017
- } catch (error) {
1018
- result.errors.push(error instanceof Error ? error.message : String(error));
975
+ async updateConfig(idOrSlug, input) {
976
+ const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
977
+ return data.config;
1019
978
  }
1020
- return result;
1021
- }
1022
- async function filterRemoteColumns(remote, table, columns) {
1023
- const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
1024
- if (rows.length === 0)
1025
- return columns;
1026
- const allowed = new Set(rows.map((row) => row.column_name));
1027
- return columns.filter((column) => allowed.has(column));
1028
- }
1029
- function filterLocalColumns(db, table, columns) {
1030
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
1031
- const allowed = new Set(rows.map((row) => row.name));
1032
- return columns.filter((column) => allowed.has(column));
1033
- }
1034
- async function upsertPg(remote, table, columns, rows) {
1035
- if (columns.length === 0)
1036
- return 0;
1037
- const primaryKeys = PRIMARY_KEYS[table];
1038
- const columnList = columns.map(quoteIdent).join(", ");
1039
- const placeholders = columns.map(() => "?").join(", ");
1040
- const keyList = primaryKeys.map(quoteIdent).join(", ");
1041
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
1042
- const fallbackKey = primaryKeys[0];
1043
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
1044
- for (const row of rows) {
1045
- await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
1046
- }
1047
- return rows.length;
1048
- }
1049
- function upsertSqlite(db, table, columns, rows) {
1050
- if (columns.length === 0)
1051
- return 0;
1052
- const primaryKeys = PRIMARY_KEYS[table];
1053
- const columnList = columns.map(quoteIdent).join(", ");
1054
- const placeholders = columns.map(() => "?").join(", ");
1055
- const keyList = primaryKeys.map(quoteIdent).join(", ");
1056
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
1057
- const fallbackKey = primaryKeys[0];
1058
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
1059
- const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
1060
- db.transaction((batch) => {
1061
- for (const row of batch)
1062
- statement.run(...columns.map((column) => coerceForSqlite(row[column])));
1063
- })(rows);
1064
- return rows.length;
1065
- }
1066
- function recordSyncMeta(db, direction, results) {
1067
- ensureSyncMetaTable(db);
1068
- const now2 = new Date().toISOString();
1069
- 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");
1070
- for (const result of results) {
1071
- if (result.errors.length > 0)
1072
- continue;
1073
- statement.run(result.table, now2, direction);
979
+ async deleteConfig(idOrSlug) {
980
+ const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
981
+ if (status === 404)
982
+ throw new ConfigNotFoundError(idOrSlug);
983
+ }
984
+ async getConfigStats() {
985
+ const { data } = await this.request("GET", "/stats");
986
+ return data ?? { total: 0 };
987
+ }
988
+ async listSnapshots(configId) {
989
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
990
+ return data?.snapshots ?? [];
991
+ }
992
+ async getSnapshot(id) {
993
+ const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
994
+ if (status === 404 || !data?.snapshot)
995
+ return null;
996
+ return data.snapshot;
997
+ }
998
+ async getSnapshotByVersion(configId, version) {
999
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
1000
+ if (status === 404 || !data?.snapshot)
1001
+ return null;
1002
+ return data.snapshot;
1003
+ }
1004
+ async createSnapshot(configId, content, version) {
1005
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
1006
+ return data.snapshot;
1007
+ }
1008
+ async pruneSnapshots(configId, keep = 10) {
1009
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
1010
+ return data?.pruned ?? 0;
1011
+ }
1012
+ async listProfiles() {
1013
+ const { data } = await this.request("GET", "/profiles");
1014
+ return data?.profiles ?? [];
1015
+ }
1016
+ async getProfile(idOrSlug) {
1017
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1018
+ if (status === 404 || !data?.profile)
1019
+ throw new ProfileNotFoundError(idOrSlug);
1020
+ const { configs: _configs, ...profile } = data.profile;
1021
+ return profile;
1022
+ }
1023
+ async getProfileConfigs(idOrSlug) {
1024
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1025
+ if (status === 404 || !data?.profile)
1026
+ throw new ProfileNotFoundError(idOrSlug);
1027
+ return data.profile.configs ?? [];
1028
+ }
1029
+ async createProfile(input) {
1030
+ const { data } = await this.request("POST", "/profiles", input, {
1031
+ idempotent: true
1032
+ });
1033
+ return data.profile;
1034
+ }
1035
+ async updateProfile(idOrSlug, input) {
1036
+ const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
1037
+ return data.profile;
1038
+ }
1039
+ async deleteProfile(idOrSlug) {
1040
+ const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1041
+ if (status === 404)
1042
+ throw new ProfileNotFoundError(idOrSlug);
1043
+ }
1044
+ async addConfigToProfile(profileIdOrSlug, configId) {
1045
+ await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
1046
+ }
1047
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
1048
+ await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
1049
+ }
1050
+ async resolveProfileForMachine(machine) {
1051
+ const params = new URLSearchParams;
1052
+ if (machine?.hostname)
1053
+ params.set("hostname", machine.hostname);
1054
+ if (machine?.os)
1055
+ params.set("os", machine.os);
1056
+ if (machine?.arch)
1057
+ params.set("arch", machine.arch);
1058
+ const qs = params.toString();
1059
+ const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
1060
+ if (status === 404 || !data?.profile)
1061
+ return null;
1062
+ return data.profile;
1063
+ }
1064
+ async registerMachine(hostname2, os, arch2) {
1065
+ const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
1066
+ return data.machine;
1067
+ }
1068
+ async updateMachineApplied(hostname2) {
1069
+ await this.request("POST", "/machines/applied", { hostname: hostname2 });
1070
+ }
1071
+ async listMachines() {
1072
+ const { data } = await this.request("GET", "/machines");
1073
+ return data?.machines ?? [];
1074
+ }
1075
+ async sendFeedback(input) {
1076
+ await this.request("POST", "/feedback", {
1077
+ message: input.message,
1078
+ email: input.email ?? undefined,
1079
+ category: input.category ?? undefined,
1080
+ version: input.version ?? undefined
1081
+ });
1082
+ }
1083
+ async reset() {
1084
+ throw new Error("`init --force` cannot wipe the shared cloud store from a client. " + "Unset HASNA_INSTRUCTIONS_API_URL / HASNA_INSTRUCTIONS_API_KEY to reset the local store instead.");
1074
1085
  }
1075
1086
  }
1076
- function ensureSyncMetaTable(db) {
1077
- 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))");
1078
- }
1079
- function quoteIdent(identifier) {
1080
- return `"${identifier.replace(/"/g, '""')}"`;
1081
- }
1082
- function coerceForSqlite(value) {
1083
- if (value === undefined || value === null)
1084
- return null;
1085
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
1086
- return value;
1087
- if (value instanceof Date)
1088
- return value.toISOString();
1089
- if (Buffer.isBuffer(value) || value instanceof Uint8Array)
1090
- return value;
1091
- if (typeof value === "object")
1092
- return JSON.stringify(value);
1093
- return String(value);
1087
+ function resolveConfigStore(env = process.env) {
1088
+ const cloud = resolveCloudConfig(env);
1089
+ return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
1094
1090
  }
1095
1091
  // src/status.ts
1096
1092
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
@@ -1255,8 +1251,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
1255
1251
  mkdirSync2(dir, { recursive: true });
1256
1252
  }
1257
1253
  if (previousContent !== null && changed) {
1258
- const db = opts.db || getDatabase();
1259
- createSnapshot(config.id, previousContent, config.version, db);
1254
+ const store = opts.store ?? resolveConfigStore();
1255
+ await store.createSnapshot(config.id, previousContent, config.version);
1260
1256
  }
1261
1257
  writeFileSync(path, renderedContent, "utf-8");
1262
1258
  }
@@ -1282,8 +1278,8 @@ async function applyConfig(config, opts = {}) {
1282
1278
  if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
1283
1279
  throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
1284
1280
  }
1285
- const db = opts.db || getDatabase();
1286
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
1281
+ const store = opts.store ?? resolveConfigStore();
1282
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
1287
1283
  if (isGeneratedOutputTarget(config, contextConfigs)) {
1288
1284
  throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
1289
1285
  }
@@ -1304,7 +1300,7 @@ async function applyConfig(config, opts = {}) {
1304
1300
  };
1305
1301
  }
1306
1302
  if (!opts.dryRun) {
1307
- updateConfig(config.id, { synced_at: now() }, db);
1303
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
1308
1304
  }
1309
1305
  return result;
1310
1306
  }
@@ -1420,9 +1416,9 @@ function redactIni(content) {
1420
1416
  for (let i = 0;i < lines.length; i++) {
1421
1417
  const line = lines[i];
1422
1418
  const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
1423
- if (authM && !authM[2].startsWith("{{")) {
1424
- redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
1425
- out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
1419
+ if (authM && !isReferenceValue(authM[2].trim())) {
1420
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
1421
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
1426
1422
  continue;
1427
1423
  }
1428
1424
  const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
@@ -1462,6 +1458,8 @@ function redactGeneric(content) {
1462
1458
  function shouldRedactKeyValue(key, value) {
1463
1459
  if (!value || value.startsWith("{{"))
1464
1460
  return false;
1461
+ if (isReferenceValue(value.trim()))
1462
+ return false;
1465
1463
  if (value.length < MIN_SECRET_VALUE_LEN)
1466
1464
  return false;
1467
1465
  if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
@@ -1483,6 +1481,9 @@ function reasonFor(key, value) {
1483
1481
  }
1484
1482
  return "secret value pattern";
1485
1483
  }
1484
+ function isReferenceValue(value) {
1485
+ return /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(value) || /^\$\{[A-Z][A-Z0-9_]*\}$/.test(value) || /^\$[A-Z][A-Z0-9_]*$/.test(value) || /^%[A-Z][A-Z0-9_]*%$/.test(value);
1486
+ }
1486
1487
  function redactContent(content, format) {
1487
1488
  switch (format) {
1488
1489
  case "shell":
@@ -1529,21 +1530,13 @@ function countBy(items, getValue) {
1529
1530
  }
1530
1531
  return counts;
1531
1532
  }
1532
- function tableCount(db, table) {
1533
- try {
1534
- const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
1535
- return Number(row?.count ?? 0);
1536
- } catch {
1537
- return 0;
1538
- }
1539
- }
1540
- function getConfigsStatus(db = getDatabase()) {
1533
+ async function getConfigsStatus(store = resolveConfigStore()) {
1541
1534
  let databaseReachable = true;
1542
1535
  let configs = [];
1543
1536
  let categoryStats = { total: 0 };
1544
1537
  try {
1545
- configs = listConfigs(undefined, db);
1546
- categoryStats = getConfigStats(db);
1538
+ configs = await store.listConfigs();
1539
+ categoryStats = await store.getConfigStats();
1547
1540
  } catch {
1548
1541
  databaseReachable = false;
1549
1542
  }
@@ -1568,10 +1561,25 @@ function getConfigsStatus(db = getDatabase()) {
1568
1561
  driftedTargets += 1;
1569
1562
  }
1570
1563
  }
1571
- const profiles = databaseReachable ? listProfiles(db).length : 0;
1572
- const machines = databaseReachable ? listMachines(db).length : 0;
1573
- const profileLinks = databaseReachable ? tableCount(db, "profile_configs") : 0;
1574
- const snapshots = databaseReachable ? tableCount(db, "config_snapshots") : 0;
1564
+ let profiles = 0;
1565
+ let machines = 0;
1566
+ let profileLinks = 0;
1567
+ let snapshots = 0;
1568
+ if (databaseReachable) {
1569
+ try {
1570
+ const profileList = await store.listProfiles();
1571
+ profiles = profileList.length;
1572
+ machines = (await store.listMachines()).length;
1573
+ for (const profile of profileList) {
1574
+ profileLinks += (await store.getProfileConfigs(profile.id)).length;
1575
+ }
1576
+ for (const config of configs) {
1577
+ snapshots += (await store.listSnapshots(config.id)).length;
1578
+ }
1579
+ } catch {
1580
+ databaseReachable = false;
1581
+ }
1582
+ }
1575
1583
  const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
1576
1584
  const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
1577
1585
  return {
@@ -1624,6 +1632,69 @@ function getConfigsStatus(db = getDatabase()) {
1624
1632
  }
1625
1633
  };
1626
1634
  }
1635
+ // src/db/pg-migrations.ts
1636
+ var PG_MIGRATIONS = [
1637
+ `CREATE TABLE IF NOT EXISTS configs (
1638
+ id TEXT PRIMARY KEY,
1639
+ name TEXT NOT NULL,
1640
+ slug TEXT NOT NULL UNIQUE,
1641
+ kind TEXT NOT NULL DEFAULT 'file',
1642
+ category TEXT NOT NULL,
1643
+ agent TEXT NOT NULL DEFAULT 'global',
1644
+ target_path TEXT,
1645
+ outputs TEXT NOT NULL DEFAULT '[]',
1646
+ format TEXT NOT NULL DEFAULT 'text',
1647
+ content TEXT NOT NULL DEFAULT '',
1648
+ description TEXT,
1649
+ tags TEXT NOT NULL DEFAULT '[]',
1650
+ is_template BOOLEAN NOT NULL DEFAULT FALSE,
1651
+ version INTEGER NOT NULL DEFAULT 1,
1652
+ created_at TEXT NOT NULL,
1653
+ updated_at TEXT NOT NULL,
1654
+ synced_at TEXT
1655
+ )`,
1656
+ `CREATE TABLE IF NOT EXISTS config_snapshots (
1657
+ id TEXT PRIMARY KEY,
1658
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1659
+ content TEXT NOT NULL,
1660
+ version INTEGER NOT NULL,
1661
+ created_at TEXT NOT NULL
1662
+ )`,
1663
+ `CREATE TABLE IF NOT EXISTS profiles (
1664
+ id TEXT PRIMARY KEY,
1665
+ name TEXT NOT NULL,
1666
+ slug TEXT NOT NULL UNIQUE,
1667
+ description TEXT,
1668
+ selectors TEXT NOT NULL DEFAULT '{}',
1669
+ variables TEXT NOT NULL DEFAULT '{}',
1670
+ created_at TEXT NOT NULL,
1671
+ updated_at TEXT NOT NULL
1672
+ )`,
1673
+ `CREATE TABLE IF NOT EXISTS profile_configs (
1674
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
1675
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1676
+ sort_order INTEGER NOT NULL DEFAULT 0,
1677
+ PRIMARY KEY (profile_id, config_id)
1678
+ )`,
1679
+ `CREATE TABLE IF NOT EXISTS machines (
1680
+ id TEXT PRIMARY KEY,
1681
+ hostname TEXT NOT NULL UNIQUE,
1682
+ os TEXT,
1683
+ arch TEXT,
1684
+ last_applied_at TEXT,
1685
+ created_at TEXT NOT NULL
1686
+ )`,
1687
+ `CREATE TABLE IF NOT EXISTS feedback (
1688
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1689
+ message TEXT NOT NULL,
1690
+ email TEXT,
1691
+ category TEXT DEFAULT 'general',
1692
+ version TEXT,
1693
+ machine_id TEXT,
1694
+ created_at TEXT NOT NULL DEFAULT NOW()::text
1695
+ )`,
1696
+ `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
1697
+ ];
1627
1698
  // src/lib/session-render.ts
1628
1699
  import { createHash } from "crypto";
1629
1700
  import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
@@ -2501,14 +2572,14 @@ function asStringArray(value) {
2501
2572
  return value.filter((item) => typeof item === "string");
2502
2573
  }
2503
2574
  // src/lib/session-apply.ts
2504
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
2575
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2505
2576
  import {
2506
2577
  existsSync as existsSync6,
2507
2578
  lstatSync,
2508
2579
  mkdirSync as mkdirSync3,
2509
2580
  readFileSync as readFileSync4,
2510
2581
  renameSync,
2511
- rmSync,
2582
+ rmSync as rmSync2,
2512
2583
  writeFileSync as writeFileSync2
2513
2584
  } from "fs";
2514
2585
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
@@ -2562,7 +2633,7 @@ function applySessionRender(plan, options = {}) {
2562
2633
  continue;
2563
2634
  assertNoSymlinkSegments(targetHome, result.path);
2564
2635
  if (existsSync6(result.path))
2565
- rmSync(result.path);
2636
+ rmSync2(result.path);
2566
2637
  }
2567
2638
  }
2568
2639
  return {
@@ -2796,7 +2867,7 @@ function writePlannedFile(path, content, targetHome) {
2796
2867
  const dir = dirname3(path);
2797
2868
  mkdirSync3(dir, { recursive: true });
2798
2869
  assertNoSymlinkSegments(targetHome, path);
2799
- const tmp = join4(dir, `.session-${randomUUID2()}.tmp`);
2870
+ const tmp = join4(dir, `.session-${randomUUID3()}.tmp`);
2800
2871
  writeFileSync2(tmp, content, "utf-8");
2801
2872
  renameSync(tmp, path);
2802
2873
  }
@@ -2814,7 +2885,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
2814
2885
  if (!previousManifest && existingFiles.length === 0)
2815
2886
  return null;
2816
2887
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2817
- const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID2()}.json`);
2888
+ const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID3()}.json`);
2818
2889
  const snapshot = {
2819
2890
  schema: "hasna.configs.session-render-snapshot/v1",
2820
2891
  createdAt: new Date().toISOString(),
@@ -2948,7 +3019,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
2948
3019
  ids, passport numbers, credentials, and contract clauses unless an explicit
2949
3020
  approved storage policy exists.
2950
3021
  `;
2951
- function ensureProjectDashboardStandardConfig(db) {
3022
+ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
2952
3023
  const input = {
2953
3024
  name: "Agent Managed Project Dashboard Standard",
2954
3025
  category: "workspace",
@@ -2960,17 +3031,21 @@ function ensureProjectDashboardStandardConfig(db) {
2960
3031
  tags: ["projects-dashboard", "agent-projects", "json-render"]
2961
3032
  };
2962
3033
  try {
2963
- const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG, db);
3034
+ const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
2964
3035
  if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind) {
2965
- return updateConfig(existing.id, input, db);
3036
+ return await store.updateConfig(existing.id, input);
2966
3037
  }
2967
3038
  return existing;
2968
3039
  } catch {
2969
- return createConfig(input, db);
3040
+ return await store.createConfig(input);
2970
3041
  }
2971
3042
  }
2972
3043
 
2973
3044
  // src/lib/platform-profiles.ts
3045
+ function profileHasSelectors2(profile) {
3046
+ const selectors = profile.selectors ?? {};
3047
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
3048
+ }
2974
3049
  var PLATFORM_PROFILE_PRESETS = [
2975
3050
  {
2976
3051
  name: "linux-arm64",
@@ -2997,25 +3072,25 @@ var PLATFORM_PROFILE_PRESETS = [
2997
3072
  }
2998
3073
  }
2999
3074
  ];
3000
- function ensurePlatformProfiles(db) {
3001
- const configs = listConfigs(undefined, db);
3075
+ async function ensurePlatformProfiles(store = resolveConfigStore()) {
3076
+ const configs = await store.listConfigs();
3002
3077
  const ensured = [];
3003
3078
  for (const preset of PLATFORM_PROFILE_PRESETS) {
3004
3079
  let profile;
3005
3080
  try {
3006
- profile = getProfile(preset.name, db);
3007
- if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
3008
- profile = updateProfile(profile.id, {
3081
+ profile = await store.getProfile(preset.name);
3082
+ if (!profileHasSelectors2(profile) || Object.keys(profile.variables).length === 0) {
3083
+ profile = await store.updateProfile(profile.id, {
3009
3084
  description: profile.description ?? preset.description,
3010
- selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
3085
+ selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
3011
3086
  variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
3012
- }, db);
3087
+ });
3013
3088
  }
3014
3089
  } catch {
3015
- profile = createProfile(preset, db);
3090
+ profile = await store.createProfile(preset);
3016
3091
  }
3017
3092
  for (const config of configs) {
3018
- addConfigToProfile(profile.id, config.id, db);
3093
+ await store.addConfigToProfile(profile.id, config.id);
3019
3094
  }
3020
3095
  ensured.push(profile);
3021
3096
  }
@@ -3034,14 +3109,14 @@ function shouldSkip(p) {
3034
3109
  return SKIP.some((s) => p.includes(s));
3035
3110
  }
3036
3111
  async function syncFromDir(dir, opts = {}) {
3037
- const d = opts.db || getDatabase();
3112
+ const store = opts.store ?? resolveConfigStore();
3038
3113
  const absDir = expandPath(dir);
3039
3114
  if (!existsSync7(absDir))
3040
3115
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
3041
3116
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join5(absDir, f)).filter((f) => statSync3(f).isFile());
3042
3117
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3043
3118
  const home = homedir4();
3044
- const allConfigs = listConfigs(undefined, d);
3119
+ const allConfigs = await store.listConfigs();
3045
3120
  for (const file of files) {
3046
3121
  if (shouldSkip(file)) {
3047
3122
  result.skipped.push(file);
@@ -3057,11 +3132,11 @@ async function syncFromDir(dir, opts = {}) {
3057
3132
  const existing = allConfigs.find((c) => c.target_path === targetPath);
3058
3133
  if (!existing) {
3059
3134
  if (!opts.dryRun)
3060
- createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
3135
+ await store.createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
3061
3136
  result.added++;
3062
3137
  } else if (existing.content !== content) {
3063
3138
  if (!opts.dryRun)
3064
- updateConfig(existing.id, { content }, d);
3139
+ await store.updateConfig(existing.id, { content });
3065
3140
  result.updated++;
3066
3141
  } else {
3067
3142
  result.unchanged++;
@@ -3073,17 +3148,17 @@ async function syncFromDir(dir, opts = {}) {
3073
3148
  return result;
3074
3149
  }
3075
3150
  async function syncToDir(dir, opts = {}) {
3076
- const d = opts.db || getDatabase();
3151
+ const store = opts.store ?? resolveConfigStore();
3077
3152
  const home = homedir4();
3078
3153
  const absDir = expandPath(dir);
3079
3154
  const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
3080
- const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
3155
+ const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
3081
3156
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3082
3157
  for (const config of configs) {
3083
3158
  if (config.kind === "reference")
3084
3159
  continue;
3085
3160
  try {
3086
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
3161
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store });
3087
3162
  r.changed ? result.updated++ : result.unchanged++;
3088
3163
  } catch {
3089
3164
  result.skipped.push(config.target_path || config.id);
@@ -3201,11 +3276,11 @@ var PROJECT_CONFIG_FILES = [
3201
3276
  { file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" }
3202
3277
  ];
3203
3278
  async function syncProject(opts) {
3204
- const d = opts.db || getDatabase();
3279
+ const store = opts.store ?? resolveConfigStore();
3205
3280
  const absDir = expandPath(opts.projectDir);
3206
3281
  const projectName = absDir.split("/").pop() || "project";
3207
3282
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3208
- const allConfigs = listConfigs(undefined, d);
3283
+ const allConfigs = await store.listConfigs();
3209
3284
  const machine = detectMachineContext();
3210
3285
  for (const pf of PROJECT_CONFIG_FILES) {
3211
3286
  const abs = join6(absDir, pf.file);
@@ -3227,11 +3302,11 @@ async function syncProject(opts) {
3227
3302
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
3228
3303
  if (!existing) {
3229
3304
  if (!opts.dryRun)
3230
- createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
3305
+ await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
3231
3306
  result.added++;
3232
3307
  } else if (existing.content !== content) {
3233
3308
  if (!opts.dryRun)
3234
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3309
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3235
3310
  result.updated++;
3236
3311
  } else {
3237
3312
  result.unchanged++;
@@ -3256,11 +3331,11 @@ async function syncProject(opts) {
3256
3331
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
3257
3332
  if (!existing) {
3258
3333
  if (!opts.dryRun)
3259
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
3334
+ await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
3260
3335
  result.added++;
3261
3336
  } else if (existing.content !== content) {
3262
3337
  if (!opts.dryRun)
3263
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3338
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3264
3339
  result.updated++;
3265
3340
  } else {
3266
3341
  result.unchanged++;
@@ -3270,7 +3345,7 @@ async function syncProject(opts) {
3270
3345
  return result;
3271
3346
  }
3272
3347
  async function syncKnown(opts = {}) {
3273
- const d = opts.db || getDatabase();
3348
+ const store = opts.store ?? resolveConfigStore();
3274
3349
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3275
3350
  const home = getConfigHome();
3276
3351
  const machine = detectMachineContext();
@@ -3279,7 +3354,7 @@ async function syncKnown(opts = {}) {
3279
3354
  targets = targets.filter((k) => k.agent === opts.agent);
3280
3355
  if (opts.category)
3281
3356
  targets = targets.filter((k) => k.category === opts.category);
3282
- const allConfigs = listConfigs(undefined, d);
3357
+ const allConfigs = await store.listConfigs();
3283
3358
  const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
3284
3359
  for (const known of targets) {
3285
3360
  if (known.rulesDir) {
@@ -3308,15 +3383,15 @@ async function syncKnown(opts = {}) {
3308
3383
  const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
3309
3384
  if (!existing) {
3310
3385
  if (!opts.dryRun)
3311
- createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
3386
+ await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
3312
3387
  result.added++;
3313
3388
  } else if (existing.content !== content) {
3314
3389
  if (!opts.dryRun)
3315
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
3390
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
3316
3391
  result.updated++;
3317
3392
  } else if (!outputsEqual(existing.outputs, outputs)) {
3318
3393
  if (!opts.dryRun)
3319
- updateConfig(existing.id, { outputs }, d);
3394
+ await store.updateConfig(existing.id, { outputs });
3320
3395
  result.updated++;
3321
3396
  } else {
3322
3397
  result.unchanged++;
@@ -3348,7 +3423,7 @@ async function syncKnown(opts = {}) {
3348
3423
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
3349
3424
  if (!existing) {
3350
3425
  if (!opts.dryRun) {
3351
- createConfig({
3426
+ await store.createConfig({
3352
3427
  name: known.name,
3353
3428
  category: known.category,
3354
3429
  agent: known.agent,
@@ -3359,16 +3434,16 @@ async function syncKnown(opts = {}) {
3359
3434
  description: known.description,
3360
3435
  is_template: isTemplate2,
3361
3436
  outputs: known.outputs
3362
- }, d);
3437
+ });
3363
3438
  }
3364
3439
  result.added++;
3365
3440
  } else if (existing.content !== content) {
3366
3441
  if (!opts.dryRun)
3367
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
3442
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
3368
3443
  result.updated++;
3369
3444
  } else if (!outputsEqual(existing.outputs, known.outputs)) {
3370
3445
  if (!opts.dryRun)
3371
- updateConfig(existing.id, { outputs: known.outputs }, d);
3446
+ await store.updateConfig(existing.id, { outputs: known.outputs });
3372
3447
  result.updated++;
3373
3448
  } else {
3374
3449
  result.unchanged++;
@@ -3380,9 +3455,9 @@ async function syncKnown(opts = {}) {
3380
3455
  return result;
3381
3456
  }
3382
3457
  async function syncToDisk(opts = {}) {
3383
- const d = opts.db || getDatabase();
3458
+ const store = opts.store ?? resolveConfigStore();
3384
3459
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3385
- const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
3460
+ const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
3386
3461
  const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
3387
3462
  let configs = allFileConfigs.filter((config) => {
3388
3463
  return !isGeneratedOutputTarget2(config, outputOwners);
@@ -3395,7 +3470,7 @@ async function syncToDisk(opts = {}) {
3395
3470
  if (!config.target_path && config.outputs.length === 0)
3396
3471
  continue;
3397
3472
  try {
3398
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
3473
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
3399
3474
  r.changed ? result.updated++ : result.unchanged++;
3400
3475
  } catch {
3401
3476
  result.skipped.push(config.target_path ?? config.id);
@@ -3432,12 +3507,12 @@ function buildDiff(expectedContent, targetPath) {
3432
3507
  return lines.join(`
3433
3508
  `);
3434
3509
  }
3435
- function diffConfig(config, opts = {}) {
3510
+ async function diffConfig(config, opts = {}) {
3436
3511
  if (!config.target_path && config.outputs.length === 0)
3437
3512
  return "(reference \u2014 no target path)";
3438
3513
  const diffs = [];
3439
- const db = opts.db || getDatabase();
3440
- const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
3514
+ const store = opts.store ?? resolveConfigStore();
3515
+ const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
3441
3516
  if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
3442
3517
  return "(generated output \u2014 managed by fan-out)";
3443
3518
  }
@@ -3515,12 +3590,12 @@ function detectFormat(filePath) {
3515
3590
  return "text";
3516
3591
  }
3517
3592
  // src/lib/export.ts
3518
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
3593
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
3519
3594
  import { join as join7, resolve as resolve4 } from "path";
3520
3595
  import { tmpdir } from "os";
3521
3596
  async function exportConfigs(outputPath, opts = {}) {
3522
- const d = opts.db || getDatabase();
3523
- const configs = listConfigs(opts.filter, d);
3597
+ const store = opts.store ?? resolveConfigStore();
3598
+ const configs = await store.listConfigs(opts.filter);
3524
3599
  const absOutput = resolve4(outputPath);
3525
3600
  const tmpDir = join7(tmpdir(), `configs-export-${Date.now()}`);
3526
3601
  const contentsDir = join7(tmpDir, "contents");
@@ -3528,7 +3603,7 @@ async function exportConfigs(outputPath, opts = {}) {
3528
3603
  mkdirSync4(contentsDir, { recursive: true });
3529
3604
  const manifest = {
3530
3605
  version: "1.0.0",
3531
- exported_at: now(),
3606
+ exported_at: new Date().toISOString(),
3532
3607
  configs: configs.map(({ content: _content, ...meta }) => meta)
3533
3608
  };
3534
3609
  writeFileSync3(join7(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
@@ -3548,16 +3623,16 @@ async function exportConfigs(outputPath, opts = {}) {
3548
3623
  return { path: absOutput, count: configs.length };
3549
3624
  } finally {
3550
3625
  if (existsSync9(tmpDir)) {
3551
- rmSync2(tmpDir, { recursive: true, force: true });
3626
+ rmSync3(tmpDir, { recursive: true, force: true });
3552
3627
  }
3553
3628
  }
3554
3629
  }
3555
3630
  // src/lib/import.ts
3556
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
3631
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
3557
3632
  import { join as join8, resolve as resolve5 } from "path";
3558
3633
  import { tmpdir as tmpdir2 } from "os";
3559
3634
  async function importConfigs(bundlePath, opts = {}) {
3560
- const d = opts.db || getDatabase();
3635
+ const store = opts.store ?? resolveConfigStore();
3561
3636
  const conflict = opts.conflict ?? "skip";
3562
3637
  const absPath = resolve5(bundlePath);
3563
3638
  const tmpDir = join8(tmpdir2(), `configs-import-${Date.now()}`);
@@ -3584,17 +3659,17 @@ async function importConfigs(bundlePath, opts = {}) {
3584
3659
  const content = existsSync10(contentFile) ? readFileSync7(contentFile, "utf-8") : "";
3585
3660
  let existing = null;
3586
3661
  try {
3587
- existing = getConfig(meta.slug, d);
3662
+ existing = await store.getConfig(meta.slug);
3588
3663
  } catch {}
3589
3664
  if (existing) {
3590
3665
  if (conflict === "skip") {
3591
3666
  result.skipped++;
3592
3667
  } else if (conflict === "overwrite" || conflict === "version") {
3593
- updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs }, d);
3668
+ await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
3594
3669
  result.updated++;
3595
3670
  }
3596
3671
  } else {
3597
- createConfig({
3672
+ await store.createConfig({
3598
3673
  name: meta.name,
3599
3674
  kind: meta.kind,
3600
3675
  category: meta.category,
@@ -3606,7 +3681,7 @@ async function importConfigs(bundlePath, opts = {}) {
3606
3681
  description: meta.description ?? undefined,
3607
3682
  tags: meta.tags,
3608
3683
  is_template: meta.is_template
3609
- }, d);
3684
+ });
3610
3685
  result.created++;
3611
3686
  }
3612
3687
  } catch (err) {
@@ -3616,15 +3691,397 @@ async function importConfigs(bundlePath, opts = {}) {
3616
3691
  return result;
3617
3692
  } finally {
3618
3693
  if (existsSync10(tmpDir)) {
3619
- rmSync3(tmpDir, { recursive: true, force: true });
3694
+ rmSync4(tmpDir, { recursive: true, force: true });
3695
+ }
3696
+ }
3697
+ }
3698
+ // src/lib/package-manager-guard.ts
3699
+ import { execFileSync } from "child_process";
3700
+ import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
3701
+ import { homedir as homedir5 } from "os";
3702
+ import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join9, relative as relative4, resolve as resolve6 } from "path";
3703
+ var SKIP_DIRS = new Set([
3704
+ ".git",
3705
+ "node_modules",
3706
+ "dist",
3707
+ "build",
3708
+ "coverage",
3709
+ ".next",
3710
+ ".turbo",
3711
+ ".cache"
3712
+ ]);
3713
+ var LOCKFILE_NAMES = new Set([
3714
+ "bun.lock",
3715
+ "package-lock.json",
3716
+ "npm-shrinkwrap.json",
3717
+ "pnpm-lock.yaml",
3718
+ "yarn.lock"
3719
+ ]);
3720
+ var HOME_FILES = [
3721
+ ".npmrc",
3722
+ ".bunfig.toml",
3723
+ "bunfig.toml",
3724
+ ".bashrc",
3725
+ ".bash_profile",
3726
+ ".zshrc",
3727
+ ".zprofile",
3728
+ ".profile"
3729
+ ];
3730
+ var TOKEN_VALUE_PATTERNS = [
3731
+ { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
3732
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
3733
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
3734
+ { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
3735
+ { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
3736
+ { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
3737
+ ];
3738
+ function scanPackageManagerSecrets(options = {}) {
3739
+ const cwd = options.cwd ? resolve6(options.cwd) : process.cwd();
3740
+ const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve6(cwd, root));
3741
+ const findings = [];
3742
+ let scannedFiles = 0;
3743
+ for (const root of roots) {
3744
+ if (!existsSync11(root))
3745
+ continue;
3746
+ const stat = lstatSync2(root);
3747
+ if (stat.isFile()) {
3748
+ if (!shouldScanRepoFile(root))
3749
+ continue;
3750
+ const text = readTextFile(root);
3751
+ if (text === null)
3752
+ continue;
3753
+ scannedFiles++;
3754
+ findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
3755
+ continue;
3756
+ }
3757
+ if (!stat.isDirectory())
3758
+ continue;
3759
+ const tracked = trackedFiles(root);
3760
+ for (const file of collectRepoFiles(root)) {
3761
+ const rel = toPosix(relative4(root, file));
3762
+ const isTracked = tracked.has(rel);
3763
+ const text = readTextFile(file);
3764
+ if (text === null)
3765
+ continue;
3766
+ scannedFiles++;
3767
+ findings.push(...scanFile(file, text, classifyRepoFile(file), isTracked, root));
3768
+ }
3769
+ }
3770
+ if (options.includeHome) {
3771
+ const home = homedir5();
3772
+ for (const name of HOME_FILES) {
3773
+ const file = join9(home, name);
3774
+ if (!existsSync11(file))
3775
+ continue;
3776
+ const text = readTextFile(file);
3777
+ if (text === null)
3778
+ continue;
3779
+ scannedFiles++;
3780
+ findings.push(...scanFile(file, text, classifyHomeFile(name), false, home));
3781
+ }
3782
+ }
3783
+ findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
3784
+ return {
3785
+ clean: findings.length === 0,
3786
+ scannedFiles,
3787
+ scannedRoots: roots,
3788
+ findings
3789
+ };
3790
+ }
3791
+ function collectRepoFiles(root) {
3792
+ const out = [];
3793
+ const visit = (dir) => {
3794
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
3795
+ if (entry.isDirectory()) {
3796
+ if (SKIP_DIRS.has(entry.name))
3797
+ continue;
3798
+ visit(join9(dir, entry.name));
3799
+ continue;
3800
+ }
3801
+ if (!entry.isFile())
3802
+ continue;
3803
+ const file = join9(dir, entry.name);
3804
+ if (shouldScanRepoFile(file))
3805
+ out.push(file);
3806
+ }
3807
+ };
3808
+ visit(root);
3809
+ return out;
3810
+ }
3811
+ function shouldScanRepoFile(file) {
3812
+ const name = basename5(file);
3813
+ return isNpmrcName(name) || isBunConfigName(name) || LOCKFILE_NAMES.has(name);
3814
+ }
3815
+ function classifyRepoFile(file) {
3816
+ const name = basename5(file);
3817
+ if (isNpmrcName(name))
3818
+ return "repo-npmrc";
3819
+ if (isBunConfigName(name))
3820
+ return "bun-config";
3821
+ return "lockfile";
3822
+ }
3823
+ function classifyHomeFile(name) {
3824
+ if (name === ".npmrc")
3825
+ return "home-npmrc";
3826
+ if (isBunConfigName(name))
3827
+ return "bun-config";
3828
+ return "shell-profile";
3829
+ }
3830
+ function isBunConfigName(name) {
3831
+ return name === "bunfig.toml" || name === ".bunfig.toml";
3832
+ }
3833
+ function isNpmrcName(name) {
3834
+ return name === ".npmrc" || name.startsWith(".npmrc.") || name.endsWith(".npmrc");
3835
+ }
3836
+ function readTextFile(file) {
3837
+ try {
3838
+ const stat = lstatSync2(file);
3839
+ if (!stat.isFile() || stat.size > 5000000)
3840
+ return null;
3841
+ const buf = readFileSync8(file);
3842
+ if (buf.includes(0))
3843
+ return null;
3844
+ return buf.toString("utf-8");
3845
+ } catch {
3846
+ return null;
3847
+ }
3848
+ }
3849
+ function scanFile(file, text, surface, tracked, root) {
3850
+ const findings = [];
3851
+ const path = displayPath(file, root);
3852
+ if (surface === "bun-config")
3853
+ return scanBunConfigFile(text, path, tracked);
3854
+ const lines = text.split(/\r?\n/);
3855
+ for (let i = 0;i < lines.length; i++) {
3856
+ const line = lines[i];
3857
+ const lineNo = i + 1;
3858
+ if (surface === "repo-npmrc" || surface === "home-npmrc") {
3859
+ findings.push(...scanNpmrcLine(line, path, lineNo, surface, tracked));
3860
+ } else if (surface === "shell-profile") {
3861
+ findings.push(...scanShellProfileLine(line, path, lineNo, tracked));
3862
+ } else {
3863
+ findings.push(...scanLockfileLine(line, path, lineNo, tracked));
3864
+ }
3865
+ }
3866
+ return findings;
3867
+ }
3868
+ function scanNpmrcLine(lineText, path, line, surface, tracked) {
3869
+ const findings = [];
3870
+ const stripped = lineText.trim();
3871
+ if (stripped === "" || stripped.startsWith("#") || stripped.startsWith(";"))
3872
+ return findings;
3873
+ const auth = stripped.match(/(?:^|:)(_[A-Za-z]*(?:auth|password)[A-Za-z]*|password)\s*=\s*(.+)$/i);
3874
+ if (auth) {
3875
+ const value = stripQuotes(stripInlineComment(auth[2].trim()));
3876
+ if (value && !isSafeReference(value)) {
3877
+ findings.push({
3878
+ path,
3879
+ line,
3880
+ rule: "npmrc-literal-auth",
3881
+ surface,
3882
+ severity: "error",
3883
+ tracked,
3884
+ detail: tracked ? "tracked npm auth entry uses a literal value" : "npm auth entry uses a literal value"
3885
+ });
3886
+ }
3887
+ }
3888
+ findings.push(...scanCredentialedUrl(stripped, path, line, surface, tracked));
3889
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, surface, tracked));
3890
+ return findings;
3891
+ }
3892
+ function scanBunConfigFile(text, path, tracked) {
3893
+ const findings = [];
3894
+ const lines = text.split(/\r?\n/);
3895
+ let inReleaseAgeExcludes = false;
3896
+ let hasMinimumReleaseAge = false;
3897
+ for (let i = 0;i < lines.length; i++) {
3898
+ const lineText = lines[i];
3899
+ const line = i + 1;
3900
+ const stripped = lineText.trim();
3901
+ if (stripped === "" || stripped.startsWith("#"))
3902
+ continue;
3903
+ const releaseAge = stripped.match(/^minimumReleaseAge\s*=\s*(?:"([^"]+)"|'([^']+)'|([0-9]+))\s*(?:#.*)?$/i);
3904
+ if (releaseAge) {
3905
+ hasMinimumReleaseAge = true;
3906
+ const rawValue = releaseAge[1] ?? releaseAge[2] ?? releaseAge[3] ?? "";
3907
+ const value = Number(rawValue);
3908
+ if (!Number.isFinite(value) || value <= 0) {
3909
+ findings.push({
3910
+ path,
3911
+ line,
3912
+ rule: "bun-release-age-disabled",
3913
+ surface: "bun-config",
3914
+ severity: "error",
3915
+ tracked,
3916
+ detail: "Bun release-age quarantine is disabled"
3917
+ });
3918
+ }
3919
+ }
3920
+ const startsReleaseAgeExcludes = /minimumReleaseAgeExcludes/i.test(stripped);
3921
+ const scanExcludes = startsReleaseAgeExcludes || inReleaseAgeExcludes;
3922
+ if (scanExcludes) {
3923
+ const quoted = [...stripped.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
3924
+ for (const item of quoted) {
3925
+ if (!isExactHasnaPackageName(item)) {
3926
+ findings.push({
3927
+ path,
3928
+ line,
3929
+ rule: "bun-release-age-broad-exclude",
3930
+ surface: "bun-config",
3931
+ severity: "error",
3932
+ tracked,
3933
+ detail: "Bun release-age exclude must be an exact @hasna package name"
3934
+ });
3935
+ }
3936
+ }
3937
+ }
3938
+ inReleaseAgeExcludes = startsReleaseAgeExcludes ? stripped.includes("[") && !stripped.includes("]") : inReleaseAgeExcludes && !stripped.includes("]");
3939
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "bun-config", tracked));
3940
+ }
3941
+ if (!hasMinimumReleaseAge) {
3942
+ findings.push({
3943
+ path,
3944
+ line: 1,
3945
+ rule: "bun-release-age-missing",
3946
+ surface: "bun-config",
3947
+ severity: "error",
3948
+ tracked,
3949
+ detail: "Bun release-age quarantine must be configured with a positive minimumReleaseAge"
3950
+ });
3951
+ }
3952
+ return findings;
3953
+ }
3954
+ function scanShellProfileLine(lineText, path, line, tracked) {
3955
+ const findings = [];
3956
+ const stripped = lineText.trim();
3957
+ if (stripped === "" || stripped.startsWith("#"))
3958
+ return findings;
3959
+ const assignment = stripped.match(/^(?:export\s+)?(NPM(?:_CONFIG)?_[A-Z0-9_]*TOKEN|NODE_AUTH_TOKEN|NPM_TOKEN)\s*=\s*(.+)$/);
3960
+ if (assignment) {
3961
+ const value = stripQuotes(stripInlineComment(assignment[2].trim()));
3962
+ if (value && !isSafeReference(value)) {
3963
+ findings.push({
3964
+ path,
3965
+ line,
3966
+ rule: "shell-literal-package-token",
3967
+ surface: "shell-profile",
3968
+ severity: "error",
3969
+ tracked,
3970
+ detail: "shell profile package-manager token uses a literal value"
3971
+ });
3972
+ }
3973
+ }
3974
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "shell-profile", tracked));
3975
+ return findings;
3976
+ }
3977
+ function scanLockfileLine(lineText, path, line, tracked) {
3978
+ const findings = scanKnownTokenPatterns(lineText, path, line, "lockfile", tracked);
3979
+ if (/(?:^|:)_authToken\s*=\s*/i.test(lineText) && !/\$\{[A-Z0-9_]+\}|\{\{[A-Z0-9_]+\}\}/.test(lineText)) {
3980
+ findings.push({
3981
+ path,
3982
+ line,
3983
+ rule: "lockfile-auth-token",
3984
+ surface: "lockfile",
3985
+ severity: "error",
3986
+ tracked,
3987
+ detail: "lockfile contains package-manager auth token material"
3988
+ });
3989
+ }
3990
+ return findings;
3991
+ }
3992
+ function scanKnownTokenPatterns(lineText, path, line, surface, tracked) {
3993
+ const findings = [];
3994
+ for (const pattern of TOKEN_VALUE_PATTERNS) {
3995
+ if (pattern.re.test(lineText)) {
3996
+ findings.push({
3997
+ path,
3998
+ line,
3999
+ rule: pattern.rule,
4000
+ surface,
4001
+ severity: "error",
4002
+ tracked,
4003
+ detail: pattern.detail
4004
+ });
4005
+ }
4006
+ }
4007
+ return findings;
4008
+ }
4009
+ function scanCredentialedUrl(lineText, path, line, surface, tracked) {
4010
+ const findings = [];
4011
+ for (const match of lineText.matchAll(/\bhttps?:\/\/([^/\s#;]+)@/gi)) {
4012
+ const userInfo = match[1];
4013
+ const credentialPart = userInfo.includes(":") ? userInfo.split(":").slice(1).join(":") : userInfo;
4014
+ if (credentialPart && !isSafeReference(credentialPart)) {
4015
+ findings.push({
4016
+ path,
4017
+ line,
4018
+ rule: "package-manager-url-credentials",
4019
+ surface,
4020
+ severity: "error",
4021
+ tracked,
4022
+ detail: "package-manager URL embeds literal credentials"
4023
+ });
3620
4024
  }
3621
4025
  }
4026
+ return findings;
4027
+ }
4028
+ function trackedFiles(root) {
4029
+ try {
4030
+ const output = execFileSync("git", ["-C", root, "ls-files", "-z"], {
4031
+ encoding: "utf-8",
4032
+ stdio: ["ignore", "pipe", "ignore"]
4033
+ });
4034
+ return new Set(output.split("\x00").filter(Boolean).map(toPosix));
4035
+ } catch {
4036
+ return new Set;
4037
+ }
4038
+ }
4039
+ function isTrackedFile(file) {
4040
+ try {
4041
+ const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
4042
+ encoding: "utf-8",
4043
+ stdio: ["ignore", "pipe", "ignore"]
4044
+ }).trim();
4045
+ const rel = toPosix(relative4(repoRoot, file));
4046
+ execFileSync("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
4047
+ stdio: ["ignore", "ignore", "ignore"]
4048
+ });
4049
+ return true;
4050
+ } catch {
4051
+ return false;
4052
+ }
4053
+ }
4054
+ function isExactHasnaPackageName(item) {
4055
+ return /^@hasna\/[a-z0-9][a-z0-9._-]*$/.test(item);
4056
+ }
4057
+ function isSafeReference(value) {
4058
+ const trimmed = stripQuotes(value.trim());
4059
+ return /^\$\{[A-Z][A-Z0-9_]*\}$/.test(trimmed) || /^\$[A-Z][A-Z0-9_]*$/.test(trimmed) || /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(trimmed) || /^%[A-Z][A-Z0-9_]*%$/.test(trimmed);
4060
+ }
4061
+ function stripQuotes(value) {
4062
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4063
+ return value.slice(1, -1);
4064
+ }
4065
+ return value;
4066
+ }
4067
+ function stripInlineComment(value) {
4068
+ return value.replace(/\s[#;].*$/, "").trim();
4069
+ }
4070
+ function displayPath(file, root) {
4071
+ const home = homedir5();
4072
+ if (root === home && (file === home || file.startsWith(home + "/")))
4073
+ return "~/" + toPosix(relative4(home, file));
4074
+ if (isAbsolute3(root) && file.startsWith(root + "/"))
4075
+ return toPosix(relative4(root, file));
4076
+ if (file === home || file.startsWith(home + "/"))
4077
+ return "~/" + toPosix(relative4(home, file));
4078
+ return file;
4079
+ }
4080
+ function toPosix(path) {
4081
+ return path.split("\\").join("/");
3622
4082
  }
3623
4083
  export {
3624
4084
  uuid,
3625
- updateProfile,
3626
- updateMachineApplied,
3627
- updateConfig,
3628
4085
  transformSkillContent,
3629
4086
  templateizeMachineContent,
3630
4087
  syncToDisk,
@@ -3633,58 +4090,30 @@ export {
3633
4090
  syncKnown,
3634
4091
  syncFromDir,
3635
4092
  stripClaudeOnlySections,
3636
- storageSync,
3637
- storagePush,
3638
- storagePull,
3639
4093
  sourcesFromIdentityExport,
3640
4094
  sourceFromFilePath,
3641
4095
  sourceFromConfig,
3642
4096
  slugify,
3643
4097
  scanSecrets,
3644
- runStorageMigrations,
3645
- resolveTables,
4098
+ scanPackageManagerSecrets,
3646
4099
  resolveSessionTargetOwnership,
3647
4100
  resolveSessionPath,
3648
4101
  resolveProfileVariables,
3649
- resolveProfileForMachine,
3650
- resetDatabase,
4102
+ resolveConfigStore,
4103
+ resolveCloudConfig,
3651
4104
  renderTemplate,
3652
4105
  renderMachineAwareContent,
3653
- removeConfigFromProfile,
3654
- registerMachine,
3655
4106
  redactContent,
3656
- pruneSnapshots,
3657
- profileMatchesMachine,
3658
- profileHasSelectors,
3659
4107
  planSessionRender,
3660
4108
  parseTemplateVars,
3661
4109
  now,
3662
4110
  normalizeOsFamily,
3663
4111
  machineContextToVariables,
3664
- listSnapshots,
3665
- listProfiles,
3666
- listMachines,
3667
- listConfigs,
3668
4112
  isTemplate,
3669
- isLocalPostgresHost,
4113
+ isCloudMode,
3670
4114
  importConfigs,
3671
4115
  hasSecrets,
3672
- getSyncMetaAll,
3673
- getStorageSyncMetaAll,
3674
- getStorageStatus,
3675
- getStoragePg,
3676
- getStorageMode,
3677
- getStorageDatabaseUrl,
3678
- getStorageDatabaseEnvName,
3679
- getSnapshotByVersion,
3680
- getSnapshot,
3681
- getProfileConfigs,
3682
- getProfile,
3683
- getDatabase,
3684
4116
  getConfigsStatus,
3685
- getConfigStats,
3686
- getConfigById,
3687
- getConfig,
3688
4117
  extractTemplateVars,
3689
4118
  exportConfigs,
3690
4119
  expandPath,
@@ -3695,17 +4124,11 @@ export {
3695
4124
  detectFormat,
3696
4125
  detectCategory,
3697
4126
  detectAgent,
3698
- deleteProfile,
3699
- deleteConfig,
3700
4127
  currentOs,
3701
4128
  currentHostname2 as currentHostname,
3702
4129
  currentArch2 as currentArch,
3703
- createSnapshot,
3704
- createProfile,
3705
- createConfig,
3706
4130
  cleanSessionPathInput,
3707
4131
  checkSessionRenderDrift,
3708
- buildPgPoolConfig,
3709
4132
  buildOpenCodeAgentsMd,
3710
4133
  buildCursorMdc,
3711
4134
  buildCodexAgentsMd,
@@ -3713,37 +4136,30 @@ export {
3713
4136
  applySessionRender,
3714
4137
  applyConfigs,
3715
4138
  applyConfig,
3716
- addConfigToProfile,
3717
4139
  TemplateRenderError,
3718
4140
  SessionApplyError,
3719
- STORAGE_TABLES,
3720
- STORAGE_MODE_ENV,
3721
- STORAGE_DATABASE_ENV,
3722
4141
  SESSION_TOOL_ADAPTERS,
3723
4142
  SESSION_RENDER_TOOLS,
3724
4143
  SESSION_RENDER_SCHEMA,
3725
4144
  SESSION_RENDER_MANAGED_MARKER,
3726
4145
  RAW_STORE_ROOT_ENV,
3727
4146
  ProfileNotFoundError,
3728
- PgAdapterAsync,
3729
4147
  PROJECT_DASHBOARD_STANDARD_SLUG,
3730
4148
  PROJECT_DASHBOARD_STANDARD_CONTENT,
3731
4149
  PROJECT_DASHBOARD_PROFILE_VARIABLES,
3732
4150
  PROJECT_CONFIG_FILES,
3733
4151
  PLATFORM_PROFILE_PRESETS,
3734
4152
  PG_MIGRATIONS,
4153
+ LocalConfigStore,
3735
4154
  KNOWN_CONFIGS,
3736
4155
  ConfigNotFoundError,
3737
4156
  ConfigApplyError,
4157
+ CloudHttpError,
4158
+ CloudConfigStore,
3738
4159
  CONFIG_TRANSFORMS,
3739
4160
  CONFIG_KINDS,
3740
4161
  CONFIG_FORMATS,
3741
4162
  CONFIG_CATEGORIES,
3742
4163
  CONFIG_AGENTS,
3743
- CONFIGS_STORAGE_TABLES,
3744
- CONFIGS_STORAGE_MODE_FALLBACK_ENV,
3745
- CONFIGS_STORAGE_MODE_ENV,
3746
- CONFIGS_STORAGE_FALLBACK_ENV,
3747
- CONFIGS_STORAGE_ENV,
3748
4164
  CODEWITH_NATIVE_IMPORTS_ENV
3749
4165
  };