@hasna/instructions 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1271 -841
  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 +896 -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 +649 -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 {
@@ -210,6 +226,10 @@ function ensureFeedbackTable(db) {
210
226
  )
211
227
  `);
212
228
  }
229
+ function insertFeedback(input, db) {
230
+ const d = db || getDatabase();
231
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
232
+ }
213
233
  function migrateDotfile() {
214
234
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
215
235
  const oldDirs = [join(home, ".open-configs"), join(home, ".configs")];
@@ -409,33 +429,7 @@ function getConfigStats(db) {
409
429
  }
410
430
  return stats;
411
431
  }
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
- }
432
+
439
433
  // src/lib/machine.ts
440
434
  import { arch as currentArch, homedir, hostname as currentHostname, type as currentOsType } from "os";
441
435
  import { existsSync as existsSync2 } from "fs";
@@ -701,6 +695,35 @@ function resolveProfileForMachine(machine = detectMachineContext(), db) {
701
695
  }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
702
696
  return matches[0]?.profile ?? null;
703
697
  }
698
+
699
+ // src/db/snapshots.ts
700
+ function createSnapshot(configId, content, version, db) {
701
+ const d = db || getDatabase();
702
+ const id = uuid();
703
+ const ts = now();
704
+ d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
705
+ return { id, config_id: configId, content, version, created_at: ts };
706
+ }
707
+ function listSnapshots(configId, db) {
708
+ const d = db || getDatabase();
709
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
710
+ }
711
+ function getSnapshot(id, db) {
712
+ const d = db || getDatabase();
713
+ return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
714
+ }
715
+ function getSnapshotByVersion(configId, version, db) {
716
+ const d = db || getDatabase();
717
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
718
+ }
719
+ function pruneSnapshots(configId, keep = 10, db) {
720
+ const d = db || getDatabase();
721
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
722
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
723
+ )`, [configId, configId, keep]);
724
+ return result.changes;
725
+ }
726
+
704
727
  // src/db/machines.ts
705
728
  import { arch, hostname, type } from "os";
706
729
  function currentHostname2() {
@@ -739,358 +762,319 @@ function listMachines(db) {
739
762
  const d = db || getDatabase();
740
763
  return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
741
764
  }
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
765
 
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();
766
+ // src/data/config-store.ts
767
+ class CloudHttpError extends Error {
768
+ status;
769
+ body;
770
+ constructor(status, message, body) {
771
+ super(message);
772
+ this.status = status;
773
+ this.body = body;
774
+ this.name = "CloudHttpError";
823
775
  }
824
776
  }
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;
777
+ var API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL";
778
+ var API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
779
+ function resolveCloudConfig(env = process.env) {
780
+ const apiUrl = env[API_URL_ENV]?.trim();
781
+ const apiKey = env[API_KEY_ENV]?.trim();
782
+ if (!apiUrl && !apiKey)
783
+ return null;
784
+ if (!apiUrl || !apiKey) {
785
+ 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.`);
786
+ }
787
+ return { apiUrl, apiKey };
833
788
  }
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
- };
789
+ function isCloudMode(env = process.env) {
790
+ return resolveCloudConfig(env) !== null;
855
791
  }
856
792
 
857
- class PgAdapterAsync {
858
- pool;
859
- constructor(connectionString) {
860
- this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
793
+ class LocalConfigStore {
794
+ db;
795
+ mode = "local";
796
+ constructor(db) {
797
+ this.db = db;
798
+ }
799
+ async listConfigs(filter) {
800
+ return listConfigs(filter, this.db);
801
+ }
802
+ async getConfig(idOrSlug) {
803
+ return getConfig(idOrSlug, this.db);
804
+ }
805
+ async getConfigById(id) {
806
+ return getConfigById(id, this.db);
807
+ }
808
+ async createConfig(input) {
809
+ return createConfig(input, this.db);
810
+ }
811
+ async updateConfig(idOrSlug, input) {
812
+ return updateConfig(idOrSlug, input, this.db);
813
+ }
814
+ async deleteConfig(idOrSlug) {
815
+ deleteConfig(idOrSlug, this.db);
816
+ }
817
+ async getConfigStats() {
818
+ return getConfigStats(this.db);
861
819
  }
862
- async run(sql, ...params) {
863
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
864
- return { changes: result.rowCount ?? 0 };
820
+ async listSnapshots(configId) {
821
+ return listSnapshots(configId, this.db);
865
822
  }
866
- async all(sql, ...params) {
867
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
868
- return result.rows;
823
+ async getSnapshot(id) {
824
+ return getSnapshot(id, this.db);
869
825
  }
870
- async close() {
871
- await this.pool.end();
826
+ async getSnapshotByVersion(configId, version) {
827
+ return getSnapshotByVersion(configId, version, this.db);
828
+ }
829
+ async createSnapshot(configId, content, version) {
830
+ return createSnapshot(configId, content, version, this.db);
831
+ }
832
+ async pruneSnapshots(configId, keep = 10) {
833
+ return pruneSnapshots(configId, keep, this.db);
834
+ }
835
+ async listProfiles() {
836
+ return listProfiles(this.db);
837
+ }
838
+ async getProfile(idOrSlug) {
839
+ return getProfile(idOrSlug, this.db);
840
+ }
841
+ async getProfileConfigs(idOrSlug) {
842
+ return getProfileConfigs(idOrSlug, this.db);
843
+ }
844
+ async createProfile(input) {
845
+ return createProfile(input, this.db);
846
+ }
847
+ async updateProfile(idOrSlug, input) {
848
+ return updateProfile(idOrSlug, input, this.db);
849
+ }
850
+ async deleteProfile(idOrSlug) {
851
+ deleteProfile(idOrSlug, this.db);
852
+ }
853
+ async addConfigToProfile(profileIdOrSlug, configId) {
854
+ addConfigToProfile(profileIdOrSlug, configId, this.db);
855
+ }
856
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
857
+ removeConfigFromProfile(profileIdOrSlug, configId, this.db);
858
+ }
859
+ async resolveProfileForMachine(machine) {
860
+ return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
861
+ }
862
+ async registerMachine(hostname2, os, arch2) {
863
+ return registerMachine(hostname2, os, arch2, this.db);
864
+ }
865
+ async updateMachineApplied(hostname2) {
866
+ updateMachineApplied(hostname2, this.db);
867
+ }
868
+ async listMachines() {
869
+ return listMachines(this.db);
870
+ }
871
+ async sendFeedback(input) {
872
+ insertFeedback(input, this.db);
873
+ }
874
+ async reset() {
875
+ resetLocalDatabase();
872
876
  }
873
877
  }
874
878
 
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;
879
+ class CloudConfigStore {
880
+ mode = "api";
881
+ base;
882
+ apiKey;
883
+ timeoutMs;
884
+ constructor(config) {
885
+ this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
886
+ this.apiKey = config.apiKey;
887
+ this.timeoutMs = config.timeoutMs ?? 30000;
888
+ }
889
+ async request(method, path, body, opts = {}) {
890
+ const controller = new AbortController;
891
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
892
+ const headers = {
893
+ Authorization: `Bearer ${this.apiKey}`,
894
+ Accept: "application/json"
895
+ };
896
+ if (body !== undefined)
897
+ headers["Content-Type"] = "application/json";
898
+ if (opts.idempotent)
899
+ headers["Idempotency-Key"] = randomUUID2();
900
+ try {
901
+ const res = await fetch(`${this.base}${path}`, {
902
+ method,
903
+ headers,
904
+ body: body === undefined ? undefined : JSON.stringify(body),
905
+ signal: controller.signal
906
+ });
907
+ if (res.status === 404 && opts.allow404)
908
+ return { status: 404, data: null };
909
+ const text = await res.text();
910
+ let parsed = null;
911
+ if (text) {
912
+ try {
913
+ parsed = JSON.parse(text);
914
+ } catch {
915
+ parsed = text;
916
+ }
917
+ }
918
+ if (!res.ok) {
919
+ const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
920
+ throw new CloudHttpError(res.status, message, parsed);
921
+ }
922
+ return { status: res.status, data: parsed };
923
+ } finally {
924
+ clearTimeout(timer);
925
+ }
897
926
  }
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;
927
+ async listConfigs(filter = {}) {
928
+ const params = new URLSearchParams;
929
+ if (filter.category)
930
+ params.set("category", filter.category);
931
+ if (filter.agent)
932
+ params.set("agent", filter.agent);
933
+ if (filter.kind)
934
+ params.set("kind", filter.kind);
935
+ if (filter.search)
936
+ params.set("search", filter.search);
937
+ const qs = params.toString();
938
+ const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
939
+ let configs = data?.configs ?? [];
940
+ if (filter.tags && filter.tags.length > 0) {
941
+ configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
942
+ }
943
+ if (filter.is_template !== undefined) {
944
+ configs = configs.filter((c) => c.is_template === filter.is_template);
945
+ }
946
+ return configs;
913
947
  }
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();
948
+ async getConfig(idOrSlug) {
949
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
950
+ if (status === 404 || !data?.config)
951
+ throw new ConfigNotFoundError(idOrSlug);
952
+ return data.config;
945
953
  }
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();
954
+ async getConfigById(id) {
955
+ return this.getConfig(id);
959
956
  }
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));
957
+ async createConfig(input) {
958
+ const { data } = await this.request("POST", "/configs", input, {
959
+ idempotent: true
960
+ });
961
+ return data.config;
1005
962
  }
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));
963
+ async updateConfig(idOrSlug, input) {
964
+ const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
965
+ return data.config;
1019
966
  }
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);
967
+ async deleteConfig(idOrSlug) {
968
+ const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
969
+ if (status === 404)
970
+ throw new ConfigNotFoundError(idOrSlug);
971
+ }
972
+ async getConfigStats() {
973
+ const { data } = await this.request("GET", "/stats");
974
+ return data ?? { total: 0 };
975
+ }
976
+ async listSnapshots(configId) {
977
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
978
+ return data?.snapshots ?? [];
979
+ }
980
+ async getSnapshot(id) {
981
+ const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
982
+ if (status === 404 || !data?.snapshot)
983
+ return null;
984
+ return data.snapshot;
985
+ }
986
+ async getSnapshotByVersion(configId, version) {
987
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
988
+ if (status === 404 || !data?.snapshot)
989
+ return null;
990
+ return data.snapshot;
991
+ }
992
+ async createSnapshot(configId, content, version) {
993
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
994
+ return data.snapshot;
995
+ }
996
+ async pruneSnapshots(configId, keep = 10) {
997
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
998
+ return data?.pruned ?? 0;
999
+ }
1000
+ async listProfiles() {
1001
+ const { data } = await this.request("GET", "/profiles");
1002
+ return data?.profiles ?? [];
1003
+ }
1004
+ async getProfile(idOrSlug) {
1005
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1006
+ if (status === 404 || !data?.profile)
1007
+ throw new ProfileNotFoundError(idOrSlug);
1008
+ const { configs: _configs, ...profile } = data.profile;
1009
+ return profile;
1010
+ }
1011
+ async getProfileConfigs(idOrSlug) {
1012
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1013
+ if (status === 404 || !data?.profile)
1014
+ throw new ProfileNotFoundError(idOrSlug);
1015
+ return data.profile.configs ?? [];
1016
+ }
1017
+ async createProfile(input) {
1018
+ const { data } = await this.request("POST", "/profiles", input, {
1019
+ idempotent: true
1020
+ });
1021
+ return data.profile;
1022
+ }
1023
+ async updateProfile(idOrSlug, input) {
1024
+ const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
1025
+ return data.profile;
1026
+ }
1027
+ async deleteProfile(idOrSlug) {
1028
+ const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1029
+ if (status === 404)
1030
+ throw new ProfileNotFoundError(idOrSlug);
1031
+ }
1032
+ async addConfigToProfile(profileIdOrSlug, configId) {
1033
+ await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
1034
+ }
1035
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
1036
+ await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
1037
+ }
1038
+ async resolveProfileForMachine(machine) {
1039
+ const params = new URLSearchParams;
1040
+ if (machine?.hostname)
1041
+ params.set("hostname", machine.hostname);
1042
+ if (machine?.os)
1043
+ params.set("os", machine.os);
1044
+ if (machine?.arch)
1045
+ params.set("arch", machine.arch);
1046
+ const qs = params.toString();
1047
+ const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
1048
+ if (status === 404 || !data?.profile)
1049
+ return null;
1050
+ return data.profile;
1051
+ }
1052
+ async registerMachine(hostname2, os, arch2) {
1053
+ const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
1054
+ return data.machine;
1055
+ }
1056
+ async updateMachineApplied(hostname2) {
1057
+ await this.request("POST", "/machines/applied", { hostname: hostname2 });
1058
+ }
1059
+ async listMachines() {
1060
+ const { data } = await this.request("GET", "/machines");
1061
+ return data?.machines ?? [];
1062
+ }
1063
+ async sendFeedback(input) {
1064
+ await this.request("POST", "/feedback", {
1065
+ message: input.message,
1066
+ email: input.email ?? undefined,
1067
+ category: input.category ?? undefined,
1068
+ version: input.version ?? undefined
1069
+ });
1070
+ }
1071
+ async reset() {
1072
+ 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
1073
  }
1075
1074
  }
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);
1075
+ function resolveConfigStore(env = process.env) {
1076
+ const cloud = resolveCloudConfig(env);
1077
+ return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
1094
1078
  }
1095
1079
  // src/status.ts
1096
1080
  import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
@@ -1255,8 +1239,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
1255
1239
  mkdirSync2(dir, { recursive: true });
1256
1240
  }
1257
1241
  if (previousContent !== null && changed) {
1258
- const db = opts.db || getDatabase();
1259
- createSnapshot(config.id, previousContent, config.version, db);
1242
+ const store = opts.store ?? resolveConfigStore();
1243
+ await store.createSnapshot(config.id, previousContent, config.version);
1260
1244
  }
1261
1245
  writeFileSync(path, renderedContent, "utf-8");
1262
1246
  }
@@ -1282,8 +1266,8 @@ async function applyConfig(config, opts = {}) {
1282
1266
  if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
1283
1267
  throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
1284
1268
  }
1285
- const db = opts.db || getDatabase();
1286
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
1269
+ const store = opts.store ?? resolveConfigStore();
1270
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
1287
1271
  if (isGeneratedOutputTarget(config, contextConfigs)) {
1288
1272
  throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
1289
1273
  }
@@ -1304,7 +1288,7 @@ async function applyConfig(config, opts = {}) {
1304
1288
  };
1305
1289
  }
1306
1290
  if (!opts.dryRun) {
1307
- updateConfig(config.id, { synced_at: now() }, db);
1291
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
1308
1292
  }
1309
1293
  return result;
1310
1294
  }
@@ -1420,9 +1404,9 @@ function redactIni(content) {
1420
1404
  for (let i = 0;i < lines.length; i++) {
1421
1405
  const line = lines[i];
1422
1406
  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}}`);
1407
+ if (authM && !isReferenceValue(authM[2].trim())) {
1408
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
1409
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
1426
1410
  continue;
1427
1411
  }
1428
1412
  const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
@@ -1462,6 +1446,8 @@ function redactGeneric(content) {
1462
1446
  function shouldRedactKeyValue(key, value) {
1463
1447
  if (!value || value.startsWith("{{"))
1464
1448
  return false;
1449
+ if (isReferenceValue(value.trim()))
1450
+ return false;
1465
1451
  if (value.length < MIN_SECRET_VALUE_LEN)
1466
1452
  return false;
1467
1453
  if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
@@ -1483,6 +1469,9 @@ function reasonFor(key, value) {
1483
1469
  }
1484
1470
  return "secret value pattern";
1485
1471
  }
1472
+ function isReferenceValue(value) {
1473
+ 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);
1474
+ }
1486
1475
  function redactContent(content, format) {
1487
1476
  switch (format) {
1488
1477
  case "shell":
@@ -1529,21 +1518,13 @@ function countBy(items, getValue) {
1529
1518
  }
1530
1519
  return counts;
1531
1520
  }
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()) {
1521
+ async function getConfigsStatus(store = resolveConfigStore()) {
1541
1522
  let databaseReachable = true;
1542
1523
  let configs = [];
1543
1524
  let categoryStats = { total: 0 };
1544
1525
  try {
1545
- configs = listConfigs(undefined, db);
1546
- categoryStats = getConfigStats(db);
1526
+ configs = await store.listConfigs();
1527
+ categoryStats = await store.getConfigStats();
1547
1528
  } catch {
1548
1529
  databaseReachable = false;
1549
1530
  }
@@ -1568,10 +1549,25 @@ function getConfigsStatus(db = getDatabase()) {
1568
1549
  driftedTargets += 1;
1569
1550
  }
1570
1551
  }
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;
1552
+ let profiles = 0;
1553
+ let machines = 0;
1554
+ let profileLinks = 0;
1555
+ let snapshots = 0;
1556
+ if (databaseReachable) {
1557
+ try {
1558
+ const profileList = await store.listProfiles();
1559
+ profiles = profileList.length;
1560
+ machines = (await store.listMachines()).length;
1561
+ for (const profile of profileList) {
1562
+ profileLinks += (await store.getProfileConfigs(profile.id)).length;
1563
+ }
1564
+ for (const config of configs) {
1565
+ snapshots += (await store.listSnapshots(config.id)).length;
1566
+ }
1567
+ } catch {
1568
+ databaseReachable = false;
1569
+ }
1570
+ }
1575
1571
  const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
1576
1572
  const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
1577
1573
  return {
@@ -1624,6 +1620,69 @@ function getConfigsStatus(db = getDatabase()) {
1624
1620
  }
1625
1621
  };
1626
1622
  }
1623
+ // src/db/pg-migrations.ts
1624
+ var PG_MIGRATIONS = [
1625
+ `CREATE TABLE IF NOT EXISTS configs (
1626
+ id TEXT PRIMARY KEY,
1627
+ name TEXT NOT NULL,
1628
+ slug TEXT NOT NULL UNIQUE,
1629
+ kind TEXT NOT NULL DEFAULT 'file',
1630
+ category TEXT NOT NULL,
1631
+ agent TEXT NOT NULL DEFAULT 'global',
1632
+ target_path TEXT,
1633
+ outputs TEXT NOT NULL DEFAULT '[]',
1634
+ format TEXT NOT NULL DEFAULT 'text',
1635
+ content TEXT NOT NULL DEFAULT '',
1636
+ description TEXT,
1637
+ tags TEXT NOT NULL DEFAULT '[]',
1638
+ is_template BOOLEAN NOT NULL DEFAULT FALSE,
1639
+ version INTEGER NOT NULL DEFAULT 1,
1640
+ created_at TEXT NOT NULL,
1641
+ updated_at TEXT NOT NULL,
1642
+ synced_at TEXT
1643
+ )`,
1644
+ `CREATE TABLE IF NOT EXISTS config_snapshots (
1645
+ id TEXT PRIMARY KEY,
1646
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1647
+ content TEXT NOT NULL,
1648
+ version INTEGER NOT NULL,
1649
+ created_at TEXT NOT NULL
1650
+ )`,
1651
+ `CREATE TABLE IF NOT EXISTS profiles (
1652
+ id TEXT PRIMARY KEY,
1653
+ name TEXT NOT NULL,
1654
+ slug TEXT NOT NULL UNIQUE,
1655
+ description TEXT,
1656
+ selectors TEXT NOT NULL DEFAULT '{}',
1657
+ variables TEXT NOT NULL DEFAULT '{}',
1658
+ created_at TEXT NOT NULL,
1659
+ updated_at TEXT NOT NULL
1660
+ )`,
1661
+ `CREATE TABLE IF NOT EXISTS profile_configs (
1662
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
1663
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
1664
+ sort_order INTEGER NOT NULL DEFAULT 0,
1665
+ PRIMARY KEY (profile_id, config_id)
1666
+ )`,
1667
+ `CREATE TABLE IF NOT EXISTS machines (
1668
+ id TEXT PRIMARY KEY,
1669
+ hostname TEXT NOT NULL UNIQUE,
1670
+ os TEXT,
1671
+ arch TEXT,
1672
+ last_applied_at TEXT,
1673
+ created_at TEXT NOT NULL
1674
+ )`,
1675
+ `CREATE TABLE IF NOT EXISTS feedback (
1676
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
1677
+ message TEXT NOT NULL,
1678
+ email TEXT,
1679
+ category TEXT DEFAULT 'general',
1680
+ version TEXT,
1681
+ machine_id TEXT,
1682
+ created_at TEXT NOT NULL DEFAULT NOW()::text
1683
+ )`,
1684
+ `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
1685
+ ];
1627
1686
  // src/lib/session-render.ts
1628
1687
  import { createHash } from "crypto";
1629
1688
  import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
@@ -2501,14 +2560,14 @@ function asStringArray(value) {
2501
2560
  return value.filter((item) => typeof item === "string");
2502
2561
  }
2503
2562
  // src/lib/session-apply.ts
2504
- import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
2563
+ import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
2505
2564
  import {
2506
2565
  existsSync as existsSync6,
2507
2566
  lstatSync,
2508
2567
  mkdirSync as mkdirSync3,
2509
2568
  readFileSync as readFileSync4,
2510
2569
  renameSync,
2511
- rmSync,
2570
+ rmSync as rmSync2,
2512
2571
  writeFileSync as writeFileSync2
2513
2572
  } from "fs";
2514
2573
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
@@ -2562,7 +2621,7 @@ function applySessionRender(plan, options = {}) {
2562
2621
  continue;
2563
2622
  assertNoSymlinkSegments(targetHome, result.path);
2564
2623
  if (existsSync6(result.path))
2565
- rmSync(result.path);
2624
+ rmSync2(result.path);
2566
2625
  }
2567
2626
  }
2568
2627
  return {
@@ -2796,7 +2855,7 @@ function writePlannedFile(path, content, targetHome) {
2796
2855
  const dir = dirname3(path);
2797
2856
  mkdirSync3(dir, { recursive: true });
2798
2857
  assertNoSymlinkSegments(targetHome, path);
2799
- const tmp = join4(dir, `.session-${randomUUID2()}.tmp`);
2858
+ const tmp = join4(dir, `.session-${randomUUID3()}.tmp`);
2800
2859
  writeFileSync2(tmp, content, "utf-8");
2801
2860
  renameSync(tmp, path);
2802
2861
  }
@@ -2814,7 +2873,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
2814
2873
  if (!previousManifest && existingFiles.length === 0)
2815
2874
  return null;
2816
2875
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
2817
- const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID2()}.json`);
2876
+ const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID3()}.json`);
2818
2877
  const snapshot = {
2819
2878
  schema: "hasna.configs.session-render-snapshot/v1",
2820
2879
  createdAt: new Date().toISOString(),
@@ -2948,7 +3007,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
2948
3007
  ids, passport numbers, credentials, and contract clauses unless an explicit
2949
3008
  approved storage policy exists.
2950
3009
  `;
2951
- function ensureProjectDashboardStandardConfig(db) {
3010
+ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
2952
3011
  const input = {
2953
3012
  name: "Agent Managed Project Dashboard Standard",
2954
3013
  category: "workspace",
@@ -2960,17 +3019,21 @@ function ensureProjectDashboardStandardConfig(db) {
2960
3019
  tags: ["projects-dashboard", "agent-projects", "json-render"]
2961
3020
  };
2962
3021
  try {
2963
- const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG, db);
3022
+ const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
2964
3023
  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);
3024
+ return await store.updateConfig(existing.id, input);
2966
3025
  }
2967
3026
  return existing;
2968
3027
  } catch {
2969
- return createConfig(input, db);
3028
+ return await store.createConfig(input);
2970
3029
  }
2971
3030
  }
2972
3031
 
2973
3032
  // src/lib/platform-profiles.ts
3033
+ function profileHasSelectors2(profile) {
3034
+ const selectors = profile.selectors ?? {};
3035
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
3036
+ }
2974
3037
  var PLATFORM_PROFILE_PRESETS = [
2975
3038
  {
2976
3039
  name: "linux-arm64",
@@ -2997,25 +3060,25 @@ var PLATFORM_PROFILE_PRESETS = [
2997
3060
  }
2998
3061
  }
2999
3062
  ];
3000
- function ensurePlatformProfiles(db) {
3001
- const configs = listConfigs(undefined, db);
3063
+ async function ensurePlatformProfiles(store = resolveConfigStore()) {
3064
+ const configs = await store.listConfigs();
3002
3065
  const ensured = [];
3003
3066
  for (const preset of PLATFORM_PROFILE_PRESETS) {
3004
3067
  let profile;
3005
3068
  try {
3006
- profile = getProfile(preset.name, db);
3007
- if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
3008
- profile = updateProfile(profile.id, {
3069
+ profile = await store.getProfile(preset.name);
3070
+ if (!profileHasSelectors2(profile) || Object.keys(profile.variables).length === 0) {
3071
+ profile = await store.updateProfile(profile.id, {
3009
3072
  description: profile.description ?? preset.description,
3010
- selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
3073
+ selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
3011
3074
  variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
3012
- }, db);
3075
+ });
3013
3076
  }
3014
3077
  } catch {
3015
- profile = createProfile(preset, db);
3078
+ profile = await store.createProfile(preset);
3016
3079
  }
3017
3080
  for (const config of configs) {
3018
- addConfigToProfile(profile.id, config.id, db);
3081
+ await store.addConfigToProfile(profile.id, config.id);
3019
3082
  }
3020
3083
  ensured.push(profile);
3021
3084
  }
@@ -3034,14 +3097,14 @@ function shouldSkip(p) {
3034
3097
  return SKIP.some((s) => p.includes(s));
3035
3098
  }
3036
3099
  async function syncFromDir(dir, opts = {}) {
3037
- const d = opts.db || getDatabase();
3100
+ const store = opts.store ?? resolveConfigStore();
3038
3101
  const absDir = expandPath(dir);
3039
3102
  if (!existsSync7(absDir))
3040
3103
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
3041
3104
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join5(absDir, f)).filter((f) => statSync3(f).isFile());
3042
3105
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3043
3106
  const home = homedir4();
3044
- const allConfigs = listConfigs(undefined, d);
3107
+ const allConfigs = await store.listConfigs();
3045
3108
  for (const file of files) {
3046
3109
  if (shouldSkip(file)) {
3047
3110
  result.skipped.push(file);
@@ -3057,11 +3120,11 @@ async function syncFromDir(dir, opts = {}) {
3057
3120
  const existing = allConfigs.find((c) => c.target_path === targetPath);
3058
3121
  if (!existing) {
3059
3122
  if (!opts.dryRun)
3060
- createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
3123
+ await store.createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
3061
3124
  result.added++;
3062
3125
  } else if (existing.content !== content) {
3063
3126
  if (!opts.dryRun)
3064
- updateConfig(existing.id, { content }, d);
3127
+ await store.updateConfig(existing.id, { content });
3065
3128
  result.updated++;
3066
3129
  } else {
3067
3130
  result.unchanged++;
@@ -3073,17 +3136,17 @@ async function syncFromDir(dir, opts = {}) {
3073
3136
  return result;
3074
3137
  }
3075
3138
  async function syncToDir(dir, opts = {}) {
3076
- const d = opts.db || getDatabase();
3139
+ const store = opts.store ?? resolveConfigStore();
3077
3140
  const home = homedir4();
3078
3141
  const absDir = expandPath(dir);
3079
3142
  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)));
3143
+ const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
3081
3144
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3082
3145
  for (const config of configs) {
3083
3146
  if (config.kind === "reference")
3084
3147
  continue;
3085
3148
  try {
3086
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
3149
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store });
3087
3150
  r.changed ? result.updated++ : result.unchanged++;
3088
3151
  } catch {
3089
3152
  result.skipped.push(config.target_path || config.id);
@@ -3201,11 +3264,11 @@ var PROJECT_CONFIG_FILES = [
3201
3264
  { file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" }
3202
3265
  ];
3203
3266
  async function syncProject(opts) {
3204
- const d = opts.db || getDatabase();
3267
+ const store = opts.store ?? resolveConfigStore();
3205
3268
  const absDir = expandPath(opts.projectDir);
3206
3269
  const projectName = absDir.split("/").pop() || "project";
3207
3270
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3208
- const allConfigs = listConfigs(undefined, d);
3271
+ const allConfigs = await store.listConfigs();
3209
3272
  const machine = detectMachineContext();
3210
3273
  for (const pf of PROJECT_CONFIG_FILES) {
3211
3274
  const abs = join6(absDir, pf.file);
@@ -3227,11 +3290,11 @@ async function syncProject(opts) {
3227
3290
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
3228
3291
  if (!existing) {
3229
3292
  if (!opts.dryRun)
3230
- createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
3293
+ await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
3231
3294
  result.added++;
3232
3295
  } else if (existing.content !== content) {
3233
3296
  if (!opts.dryRun)
3234
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3297
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3235
3298
  result.updated++;
3236
3299
  } else {
3237
3300
  result.unchanged++;
@@ -3256,11 +3319,11 @@ async function syncProject(opts) {
3256
3319
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
3257
3320
  if (!existing) {
3258
3321
  if (!opts.dryRun)
3259
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
3322
+ await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
3260
3323
  result.added++;
3261
3324
  } else if (existing.content !== content) {
3262
3325
  if (!opts.dryRun)
3263
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3326
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3264
3327
  result.updated++;
3265
3328
  } else {
3266
3329
  result.unchanged++;
@@ -3270,7 +3333,7 @@ async function syncProject(opts) {
3270
3333
  return result;
3271
3334
  }
3272
3335
  async function syncKnown(opts = {}) {
3273
- const d = opts.db || getDatabase();
3336
+ const store = opts.store ?? resolveConfigStore();
3274
3337
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3275
3338
  const home = getConfigHome();
3276
3339
  const machine = detectMachineContext();
@@ -3279,7 +3342,7 @@ async function syncKnown(opts = {}) {
3279
3342
  targets = targets.filter((k) => k.agent === opts.agent);
3280
3343
  if (opts.category)
3281
3344
  targets = targets.filter((k) => k.category === opts.category);
3282
- const allConfigs = listConfigs(undefined, d);
3345
+ const allConfigs = await store.listConfigs();
3283
3346
  const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
3284
3347
  for (const known of targets) {
3285
3348
  if (known.rulesDir) {
@@ -3308,15 +3371,15 @@ async function syncKnown(opts = {}) {
3308
3371
  const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
3309
3372
  if (!existing) {
3310
3373
  if (!opts.dryRun)
3311
- createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
3374
+ await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
3312
3375
  result.added++;
3313
3376
  } else if (existing.content !== content) {
3314
3377
  if (!opts.dryRun)
3315
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
3378
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
3316
3379
  result.updated++;
3317
3380
  } else if (!outputsEqual(existing.outputs, outputs)) {
3318
3381
  if (!opts.dryRun)
3319
- updateConfig(existing.id, { outputs }, d);
3382
+ await store.updateConfig(existing.id, { outputs });
3320
3383
  result.updated++;
3321
3384
  } else {
3322
3385
  result.unchanged++;
@@ -3348,7 +3411,7 @@ async function syncKnown(opts = {}) {
3348
3411
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
3349
3412
  if (!existing) {
3350
3413
  if (!opts.dryRun) {
3351
- createConfig({
3414
+ await store.createConfig({
3352
3415
  name: known.name,
3353
3416
  category: known.category,
3354
3417
  agent: known.agent,
@@ -3359,16 +3422,16 @@ async function syncKnown(opts = {}) {
3359
3422
  description: known.description,
3360
3423
  is_template: isTemplate2,
3361
3424
  outputs: known.outputs
3362
- }, d);
3425
+ });
3363
3426
  }
3364
3427
  result.added++;
3365
3428
  } else if (existing.content !== content) {
3366
3429
  if (!opts.dryRun)
3367
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
3430
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
3368
3431
  result.updated++;
3369
3432
  } else if (!outputsEqual(existing.outputs, known.outputs)) {
3370
3433
  if (!opts.dryRun)
3371
- updateConfig(existing.id, { outputs: known.outputs }, d);
3434
+ await store.updateConfig(existing.id, { outputs: known.outputs });
3372
3435
  result.updated++;
3373
3436
  } else {
3374
3437
  result.unchanged++;
@@ -3380,9 +3443,9 @@ async function syncKnown(opts = {}) {
3380
3443
  return result;
3381
3444
  }
3382
3445
  async function syncToDisk(opts = {}) {
3383
- const d = opts.db || getDatabase();
3446
+ const store = opts.store ?? resolveConfigStore();
3384
3447
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3385
- const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
3448
+ const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
3386
3449
  const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
3387
3450
  let configs = allFileConfigs.filter((config) => {
3388
3451
  return !isGeneratedOutputTarget2(config, outputOwners);
@@ -3395,7 +3458,7 @@ async function syncToDisk(opts = {}) {
3395
3458
  if (!config.target_path && config.outputs.length === 0)
3396
3459
  continue;
3397
3460
  try {
3398
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
3461
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
3399
3462
  r.changed ? result.updated++ : result.unchanged++;
3400
3463
  } catch {
3401
3464
  result.skipped.push(config.target_path ?? config.id);
@@ -3432,12 +3495,12 @@ function buildDiff(expectedContent, targetPath) {
3432
3495
  return lines.join(`
3433
3496
  `);
3434
3497
  }
3435
- function diffConfig(config, opts = {}) {
3498
+ async function diffConfig(config, opts = {}) {
3436
3499
  if (!config.target_path && config.outputs.length === 0)
3437
3500
  return "(reference \u2014 no target path)";
3438
3501
  const diffs = [];
3439
- const db = opts.db || getDatabase();
3440
- const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
3502
+ const store = opts.store ?? resolveConfigStore();
3503
+ const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
3441
3504
  if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
3442
3505
  return "(generated output \u2014 managed by fan-out)";
3443
3506
  }
@@ -3515,12 +3578,12 @@ function detectFormat(filePath) {
3515
3578
  return "text";
3516
3579
  }
3517
3580
  // src/lib/export.ts
3518
- import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
3581
+ import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
3519
3582
  import { join as join7, resolve as resolve4 } from "path";
3520
3583
  import { tmpdir } from "os";
3521
3584
  async function exportConfigs(outputPath, opts = {}) {
3522
- const d = opts.db || getDatabase();
3523
- const configs = listConfigs(opts.filter, d);
3585
+ const store = opts.store ?? resolveConfigStore();
3586
+ const configs = await store.listConfigs(opts.filter);
3524
3587
  const absOutput = resolve4(outputPath);
3525
3588
  const tmpDir = join7(tmpdir(), `configs-export-${Date.now()}`);
3526
3589
  const contentsDir = join7(tmpDir, "contents");
@@ -3528,7 +3591,7 @@ async function exportConfigs(outputPath, opts = {}) {
3528
3591
  mkdirSync4(contentsDir, { recursive: true });
3529
3592
  const manifest = {
3530
3593
  version: "1.0.0",
3531
- exported_at: now(),
3594
+ exported_at: new Date().toISOString(),
3532
3595
  configs: configs.map(({ content: _content, ...meta }) => meta)
3533
3596
  };
3534
3597
  writeFileSync3(join7(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
@@ -3548,16 +3611,16 @@ async function exportConfigs(outputPath, opts = {}) {
3548
3611
  return { path: absOutput, count: configs.length };
3549
3612
  } finally {
3550
3613
  if (existsSync9(tmpDir)) {
3551
- rmSync2(tmpDir, { recursive: true, force: true });
3614
+ rmSync3(tmpDir, { recursive: true, force: true });
3552
3615
  }
3553
3616
  }
3554
3617
  }
3555
3618
  // src/lib/import.ts
3556
- import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
3619
+ import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
3557
3620
  import { join as join8, resolve as resolve5 } from "path";
3558
3621
  import { tmpdir as tmpdir2 } from "os";
3559
3622
  async function importConfigs(bundlePath, opts = {}) {
3560
- const d = opts.db || getDatabase();
3623
+ const store = opts.store ?? resolveConfigStore();
3561
3624
  const conflict = opts.conflict ?? "skip";
3562
3625
  const absPath = resolve5(bundlePath);
3563
3626
  const tmpDir = join8(tmpdir2(), `configs-import-${Date.now()}`);
@@ -3584,17 +3647,17 @@ async function importConfigs(bundlePath, opts = {}) {
3584
3647
  const content = existsSync10(contentFile) ? readFileSync7(contentFile, "utf-8") : "";
3585
3648
  let existing = null;
3586
3649
  try {
3587
- existing = getConfig(meta.slug, d);
3650
+ existing = await store.getConfig(meta.slug);
3588
3651
  } catch {}
3589
3652
  if (existing) {
3590
3653
  if (conflict === "skip") {
3591
3654
  result.skipped++;
3592
3655
  } else if (conflict === "overwrite" || conflict === "version") {
3593
- updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs }, d);
3656
+ await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
3594
3657
  result.updated++;
3595
3658
  }
3596
3659
  } else {
3597
- createConfig({
3660
+ await store.createConfig({
3598
3661
  name: meta.name,
3599
3662
  kind: meta.kind,
3600
3663
  category: meta.category,
@@ -3606,7 +3669,7 @@ async function importConfigs(bundlePath, opts = {}) {
3606
3669
  description: meta.description ?? undefined,
3607
3670
  tags: meta.tags,
3608
3671
  is_template: meta.is_template
3609
- }, d);
3672
+ });
3610
3673
  result.created++;
3611
3674
  }
3612
3675
  } catch (err) {
@@ -3616,15 +3679,397 @@ async function importConfigs(bundlePath, opts = {}) {
3616
3679
  return result;
3617
3680
  } finally {
3618
3681
  if (existsSync10(tmpDir)) {
3619
- rmSync3(tmpDir, { recursive: true, force: true });
3682
+ rmSync4(tmpDir, { recursive: true, force: true });
3683
+ }
3684
+ }
3685
+ }
3686
+ // src/lib/package-manager-guard.ts
3687
+ import { execFileSync } from "child_process";
3688
+ import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
3689
+ import { homedir as homedir5 } from "os";
3690
+ import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join9, relative as relative4, resolve as resolve6 } from "path";
3691
+ var SKIP_DIRS = new Set([
3692
+ ".git",
3693
+ "node_modules",
3694
+ "dist",
3695
+ "build",
3696
+ "coverage",
3697
+ ".next",
3698
+ ".turbo",
3699
+ ".cache"
3700
+ ]);
3701
+ var LOCKFILE_NAMES = new Set([
3702
+ "bun.lock",
3703
+ "package-lock.json",
3704
+ "npm-shrinkwrap.json",
3705
+ "pnpm-lock.yaml",
3706
+ "yarn.lock"
3707
+ ]);
3708
+ var HOME_FILES = [
3709
+ ".npmrc",
3710
+ ".bunfig.toml",
3711
+ "bunfig.toml",
3712
+ ".bashrc",
3713
+ ".bash_profile",
3714
+ ".zshrc",
3715
+ ".zprofile",
3716
+ ".profile"
3717
+ ];
3718
+ var TOKEN_VALUE_PATTERNS = [
3719
+ { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
3720
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
3721
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
3722
+ { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
3723
+ { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
3724
+ { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
3725
+ ];
3726
+ function scanPackageManagerSecrets(options = {}) {
3727
+ const cwd = options.cwd ? resolve6(options.cwd) : process.cwd();
3728
+ const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve6(cwd, root));
3729
+ const findings = [];
3730
+ let scannedFiles = 0;
3731
+ for (const root of roots) {
3732
+ if (!existsSync11(root))
3733
+ continue;
3734
+ const stat = lstatSync2(root);
3735
+ if (stat.isFile()) {
3736
+ if (!shouldScanRepoFile(root))
3737
+ continue;
3738
+ const text = readTextFile(root);
3739
+ if (text === null)
3740
+ continue;
3741
+ scannedFiles++;
3742
+ findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
3743
+ continue;
3744
+ }
3745
+ if (!stat.isDirectory())
3746
+ continue;
3747
+ const tracked = trackedFiles(root);
3748
+ for (const file of collectRepoFiles(root)) {
3749
+ const rel = toPosix(relative4(root, file));
3750
+ const isTracked = tracked.has(rel);
3751
+ const text = readTextFile(file);
3752
+ if (text === null)
3753
+ continue;
3754
+ scannedFiles++;
3755
+ findings.push(...scanFile(file, text, classifyRepoFile(file), isTracked, root));
3756
+ }
3757
+ }
3758
+ if (options.includeHome) {
3759
+ const home = homedir5();
3760
+ for (const name of HOME_FILES) {
3761
+ const file = join9(home, name);
3762
+ if (!existsSync11(file))
3763
+ continue;
3764
+ const text = readTextFile(file);
3765
+ if (text === null)
3766
+ continue;
3767
+ scannedFiles++;
3768
+ findings.push(...scanFile(file, text, classifyHomeFile(name), false, home));
3769
+ }
3770
+ }
3771
+ findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
3772
+ return {
3773
+ clean: findings.length === 0,
3774
+ scannedFiles,
3775
+ scannedRoots: roots,
3776
+ findings
3777
+ };
3778
+ }
3779
+ function collectRepoFiles(root) {
3780
+ const out = [];
3781
+ const visit = (dir) => {
3782
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
3783
+ if (entry.isDirectory()) {
3784
+ if (SKIP_DIRS.has(entry.name))
3785
+ continue;
3786
+ visit(join9(dir, entry.name));
3787
+ continue;
3788
+ }
3789
+ if (!entry.isFile())
3790
+ continue;
3791
+ const file = join9(dir, entry.name);
3792
+ if (shouldScanRepoFile(file))
3793
+ out.push(file);
3794
+ }
3795
+ };
3796
+ visit(root);
3797
+ return out;
3798
+ }
3799
+ function shouldScanRepoFile(file) {
3800
+ const name = basename5(file);
3801
+ return isNpmrcName(name) || isBunConfigName(name) || LOCKFILE_NAMES.has(name);
3802
+ }
3803
+ function classifyRepoFile(file) {
3804
+ const name = basename5(file);
3805
+ if (isNpmrcName(name))
3806
+ return "repo-npmrc";
3807
+ if (isBunConfigName(name))
3808
+ return "bun-config";
3809
+ return "lockfile";
3810
+ }
3811
+ function classifyHomeFile(name) {
3812
+ if (name === ".npmrc")
3813
+ return "home-npmrc";
3814
+ if (isBunConfigName(name))
3815
+ return "bun-config";
3816
+ return "shell-profile";
3817
+ }
3818
+ function isBunConfigName(name) {
3819
+ return name === "bunfig.toml" || name === ".bunfig.toml";
3820
+ }
3821
+ function isNpmrcName(name) {
3822
+ return name === ".npmrc" || name.startsWith(".npmrc.") || name.endsWith(".npmrc");
3823
+ }
3824
+ function readTextFile(file) {
3825
+ try {
3826
+ const stat = lstatSync2(file);
3827
+ if (!stat.isFile() || stat.size > 5000000)
3828
+ return null;
3829
+ const buf = readFileSync8(file);
3830
+ if (buf.includes(0))
3831
+ return null;
3832
+ return buf.toString("utf-8");
3833
+ } catch {
3834
+ return null;
3835
+ }
3836
+ }
3837
+ function scanFile(file, text, surface, tracked, root) {
3838
+ const findings = [];
3839
+ const path = displayPath(file, root);
3840
+ if (surface === "bun-config")
3841
+ return scanBunConfigFile(text, path, tracked);
3842
+ const lines = text.split(/\r?\n/);
3843
+ for (let i = 0;i < lines.length; i++) {
3844
+ const line = lines[i];
3845
+ const lineNo = i + 1;
3846
+ if (surface === "repo-npmrc" || surface === "home-npmrc") {
3847
+ findings.push(...scanNpmrcLine(line, path, lineNo, surface, tracked));
3848
+ } else if (surface === "shell-profile") {
3849
+ findings.push(...scanShellProfileLine(line, path, lineNo, tracked));
3850
+ } else {
3851
+ findings.push(...scanLockfileLine(line, path, lineNo, tracked));
3852
+ }
3853
+ }
3854
+ return findings;
3855
+ }
3856
+ function scanNpmrcLine(lineText, path, line, surface, tracked) {
3857
+ const findings = [];
3858
+ const stripped = lineText.trim();
3859
+ if (stripped === "" || stripped.startsWith("#") || stripped.startsWith(";"))
3860
+ return findings;
3861
+ const auth = stripped.match(/(?:^|:)(_[A-Za-z]*(?:auth|password)[A-Za-z]*|password)\s*=\s*(.+)$/i);
3862
+ if (auth) {
3863
+ const value = stripQuotes(stripInlineComment(auth[2].trim()));
3864
+ if (value && !isSafeReference(value)) {
3865
+ findings.push({
3866
+ path,
3867
+ line,
3868
+ rule: "npmrc-literal-auth",
3869
+ surface,
3870
+ severity: "error",
3871
+ tracked,
3872
+ detail: tracked ? "tracked npm auth entry uses a literal value" : "npm auth entry uses a literal value"
3873
+ });
3874
+ }
3875
+ }
3876
+ findings.push(...scanCredentialedUrl(stripped, path, line, surface, tracked));
3877
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, surface, tracked));
3878
+ return findings;
3879
+ }
3880
+ function scanBunConfigFile(text, path, tracked) {
3881
+ const findings = [];
3882
+ const lines = text.split(/\r?\n/);
3883
+ let inReleaseAgeExcludes = false;
3884
+ let hasMinimumReleaseAge = false;
3885
+ for (let i = 0;i < lines.length; i++) {
3886
+ const lineText = lines[i];
3887
+ const line = i + 1;
3888
+ const stripped = lineText.trim();
3889
+ if (stripped === "" || stripped.startsWith("#"))
3890
+ continue;
3891
+ const releaseAge = stripped.match(/^minimumReleaseAge\s*=\s*(?:"([^"]+)"|'([^']+)'|([0-9]+))\s*(?:#.*)?$/i);
3892
+ if (releaseAge) {
3893
+ hasMinimumReleaseAge = true;
3894
+ const rawValue = releaseAge[1] ?? releaseAge[2] ?? releaseAge[3] ?? "";
3895
+ const value = Number(rawValue);
3896
+ if (!Number.isFinite(value) || value <= 0) {
3897
+ findings.push({
3898
+ path,
3899
+ line,
3900
+ rule: "bun-release-age-disabled",
3901
+ surface: "bun-config",
3902
+ severity: "error",
3903
+ tracked,
3904
+ detail: "Bun release-age quarantine is disabled"
3905
+ });
3906
+ }
3907
+ }
3908
+ const startsReleaseAgeExcludes = /minimumReleaseAgeExcludes/i.test(stripped);
3909
+ const scanExcludes = startsReleaseAgeExcludes || inReleaseAgeExcludes;
3910
+ if (scanExcludes) {
3911
+ const quoted = [...stripped.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
3912
+ for (const item of quoted) {
3913
+ if (!isExactHasnaPackageName(item)) {
3914
+ findings.push({
3915
+ path,
3916
+ line,
3917
+ rule: "bun-release-age-broad-exclude",
3918
+ surface: "bun-config",
3919
+ severity: "error",
3920
+ tracked,
3921
+ detail: "Bun release-age exclude must be an exact @hasna package name"
3922
+ });
3923
+ }
3924
+ }
3925
+ }
3926
+ inReleaseAgeExcludes = startsReleaseAgeExcludes ? stripped.includes("[") && !stripped.includes("]") : inReleaseAgeExcludes && !stripped.includes("]");
3927
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "bun-config", tracked));
3928
+ }
3929
+ if (!hasMinimumReleaseAge) {
3930
+ findings.push({
3931
+ path,
3932
+ line: 1,
3933
+ rule: "bun-release-age-missing",
3934
+ surface: "bun-config",
3935
+ severity: "error",
3936
+ tracked,
3937
+ detail: "Bun release-age quarantine must be configured with a positive minimumReleaseAge"
3938
+ });
3939
+ }
3940
+ return findings;
3941
+ }
3942
+ function scanShellProfileLine(lineText, path, line, tracked) {
3943
+ const findings = [];
3944
+ const stripped = lineText.trim();
3945
+ if (stripped === "" || stripped.startsWith("#"))
3946
+ return findings;
3947
+ const assignment = stripped.match(/^(?:export\s+)?(NPM(?:_CONFIG)?_[A-Z0-9_]*TOKEN|NODE_AUTH_TOKEN|NPM_TOKEN)\s*=\s*(.+)$/);
3948
+ if (assignment) {
3949
+ const value = stripQuotes(stripInlineComment(assignment[2].trim()));
3950
+ if (value && !isSafeReference(value)) {
3951
+ findings.push({
3952
+ path,
3953
+ line,
3954
+ rule: "shell-literal-package-token",
3955
+ surface: "shell-profile",
3956
+ severity: "error",
3957
+ tracked,
3958
+ detail: "shell profile package-manager token uses a literal value"
3959
+ });
3960
+ }
3961
+ }
3962
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "shell-profile", tracked));
3963
+ return findings;
3964
+ }
3965
+ function scanLockfileLine(lineText, path, line, tracked) {
3966
+ const findings = scanKnownTokenPatterns(lineText, path, line, "lockfile", tracked);
3967
+ if (/(?:^|:)_authToken\s*=\s*/i.test(lineText) && !/\$\{[A-Z0-9_]+\}|\{\{[A-Z0-9_]+\}\}/.test(lineText)) {
3968
+ findings.push({
3969
+ path,
3970
+ line,
3971
+ rule: "lockfile-auth-token",
3972
+ surface: "lockfile",
3973
+ severity: "error",
3974
+ tracked,
3975
+ detail: "lockfile contains package-manager auth token material"
3976
+ });
3977
+ }
3978
+ return findings;
3979
+ }
3980
+ function scanKnownTokenPatterns(lineText, path, line, surface, tracked) {
3981
+ const findings = [];
3982
+ for (const pattern of TOKEN_VALUE_PATTERNS) {
3983
+ if (pattern.re.test(lineText)) {
3984
+ findings.push({
3985
+ path,
3986
+ line,
3987
+ rule: pattern.rule,
3988
+ surface,
3989
+ severity: "error",
3990
+ tracked,
3991
+ detail: pattern.detail
3992
+ });
3993
+ }
3994
+ }
3995
+ return findings;
3996
+ }
3997
+ function scanCredentialedUrl(lineText, path, line, surface, tracked) {
3998
+ const findings = [];
3999
+ for (const match of lineText.matchAll(/\bhttps?:\/\/([^/\s#;]+)@/gi)) {
4000
+ const userInfo = match[1];
4001
+ const credentialPart = userInfo.includes(":") ? userInfo.split(":").slice(1).join(":") : userInfo;
4002
+ if (credentialPart && !isSafeReference(credentialPart)) {
4003
+ findings.push({
4004
+ path,
4005
+ line,
4006
+ rule: "package-manager-url-credentials",
4007
+ surface,
4008
+ severity: "error",
4009
+ tracked,
4010
+ detail: "package-manager URL embeds literal credentials"
4011
+ });
3620
4012
  }
3621
4013
  }
4014
+ return findings;
4015
+ }
4016
+ function trackedFiles(root) {
4017
+ try {
4018
+ const output = execFileSync("git", ["-C", root, "ls-files", "-z"], {
4019
+ encoding: "utf-8",
4020
+ stdio: ["ignore", "pipe", "ignore"]
4021
+ });
4022
+ return new Set(output.split("\x00").filter(Boolean).map(toPosix));
4023
+ } catch {
4024
+ return new Set;
4025
+ }
4026
+ }
4027
+ function isTrackedFile(file) {
4028
+ try {
4029
+ const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
4030
+ encoding: "utf-8",
4031
+ stdio: ["ignore", "pipe", "ignore"]
4032
+ }).trim();
4033
+ const rel = toPosix(relative4(repoRoot, file));
4034
+ execFileSync("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
4035
+ stdio: ["ignore", "ignore", "ignore"]
4036
+ });
4037
+ return true;
4038
+ } catch {
4039
+ return false;
4040
+ }
4041
+ }
4042
+ function isExactHasnaPackageName(item) {
4043
+ return /^@hasna\/[a-z0-9][a-z0-9._-]*$/.test(item);
4044
+ }
4045
+ function isSafeReference(value) {
4046
+ const trimmed = stripQuotes(value.trim());
4047
+ 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);
4048
+ }
4049
+ function stripQuotes(value) {
4050
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4051
+ return value.slice(1, -1);
4052
+ }
4053
+ return value;
4054
+ }
4055
+ function stripInlineComment(value) {
4056
+ return value.replace(/\s[#;].*$/, "").trim();
4057
+ }
4058
+ function displayPath(file, root) {
4059
+ const home = homedir5();
4060
+ if (root === home && (file === home || file.startsWith(home + "/")))
4061
+ return "~/" + toPosix(relative4(home, file));
4062
+ if (isAbsolute3(root) && file.startsWith(root + "/"))
4063
+ return toPosix(relative4(root, file));
4064
+ if (file === home || file.startsWith(home + "/"))
4065
+ return "~/" + toPosix(relative4(home, file));
4066
+ return file;
4067
+ }
4068
+ function toPosix(path) {
4069
+ return path.split("\\").join("/");
3622
4070
  }
3623
4071
  export {
3624
4072
  uuid,
3625
- updateProfile,
3626
- updateMachineApplied,
3627
- updateConfig,
3628
4073
  transformSkillContent,
3629
4074
  templateizeMachineContent,
3630
4075
  syncToDisk,
@@ -3633,58 +4078,30 @@ export {
3633
4078
  syncKnown,
3634
4079
  syncFromDir,
3635
4080
  stripClaudeOnlySections,
3636
- storageSync,
3637
- storagePush,
3638
- storagePull,
3639
4081
  sourcesFromIdentityExport,
3640
4082
  sourceFromFilePath,
3641
4083
  sourceFromConfig,
3642
4084
  slugify,
3643
4085
  scanSecrets,
3644
- runStorageMigrations,
3645
- resolveTables,
4086
+ scanPackageManagerSecrets,
3646
4087
  resolveSessionTargetOwnership,
3647
4088
  resolveSessionPath,
3648
4089
  resolveProfileVariables,
3649
- resolveProfileForMachine,
3650
- resetDatabase,
4090
+ resolveConfigStore,
4091
+ resolveCloudConfig,
3651
4092
  renderTemplate,
3652
4093
  renderMachineAwareContent,
3653
- removeConfigFromProfile,
3654
- registerMachine,
3655
4094
  redactContent,
3656
- pruneSnapshots,
3657
- profileMatchesMachine,
3658
- profileHasSelectors,
3659
4095
  planSessionRender,
3660
4096
  parseTemplateVars,
3661
4097
  now,
3662
4098
  normalizeOsFamily,
3663
4099
  machineContextToVariables,
3664
- listSnapshots,
3665
- listProfiles,
3666
- listMachines,
3667
- listConfigs,
3668
4100
  isTemplate,
3669
- isLocalPostgresHost,
4101
+ isCloudMode,
3670
4102
  importConfigs,
3671
4103
  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
4104
  getConfigsStatus,
3685
- getConfigStats,
3686
- getConfigById,
3687
- getConfig,
3688
4105
  extractTemplateVars,
3689
4106
  exportConfigs,
3690
4107
  expandPath,
@@ -3695,17 +4112,11 @@ export {
3695
4112
  detectFormat,
3696
4113
  detectCategory,
3697
4114
  detectAgent,
3698
- deleteProfile,
3699
- deleteConfig,
3700
4115
  currentOs,
3701
4116
  currentHostname2 as currentHostname,
3702
4117
  currentArch2 as currentArch,
3703
- createSnapshot,
3704
- createProfile,
3705
- createConfig,
3706
4118
  cleanSessionPathInput,
3707
4119
  checkSessionRenderDrift,
3708
- buildPgPoolConfig,
3709
4120
  buildOpenCodeAgentsMd,
3710
4121
  buildCursorMdc,
3711
4122
  buildCodexAgentsMd,
@@ -3713,37 +4124,30 @@ export {
3713
4124
  applySessionRender,
3714
4125
  applyConfigs,
3715
4126
  applyConfig,
3716
- addConfigToProfile,
3717
4127
  TemplateRenderError,
3718
4128
  SessionApplyError,
3719
- STORAGE_TABLES,
3720
- STORAGE_MODE_ENV,
3721
- STORAGE_DATABASE_ENV,
3722
4129
  SESSION_TOOL_ADAPTERS,
3723
4130
  SESSION_RENDER_TOOLS,
3724
4131
  SESSION_RENDER_SCHEMA,
3725
4132
  SESSION_RENDER_MANAGED_MARKER,
3726
4133
  RAW_STORE_ROOT_ENV,
3727
4134
  ProfileNotFoundError,
3728
- PgAdapterAsync,
3729
4135
  PROJECT_DASHBOARD_STANDARD_SLUG,
3730
4136
  PROJECT_DASHBOARD_STANDARD_CONTENT,
3731
4137
  PROJECT_DASHBOARD_PROFILE_VARIABLES,
3732
4138
  PROJECT_CONFIG_FILES,
3733
4139
  PLATFORM_PROFILE_PRESETS,
3734
4140
  PG_MIGRATIONS,
4141
+ LocalConfigStore,
3735
4142
  KNOWN_CONFIGS,
3736
4143
  ConfigNotFoundError,
3737
4144
  ConfigApplyError,
4145
+ CloudHttpError,
4146
+ CloudConfigStore,
3738
4147
  CONFIG_TRANSFORMS,
3739
4148
  CONFIG_KINDS,
3740
4149
  CONFIG_FORMATS,
3741
4150
  CONFIG_CATEGORIES,
3742
4151
  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
4152
  CODEWITH_NATIVE_IMPORTS_ENV
3749
4153
  };