@hasna/instructions 0.3.0 → 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 (60) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1321 -1101
  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/generated/storage-kit/index.d.ts +1 -1
  11. package/dist/index.d.ts +6 -8
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +896 -492
  14. package/dist/lib/apply.d.ts +2 -2
  15. package/dist/lib/apply.d.ts.map +1 -1
  16. package/dist/lib/export.d.ts +2 -2
  17. package/dist/lib/export.d.ts.map +1 -1
  18. package/dist/lib/import.d.ts +2 -2
  19. package/dist/lib/import.d.ts.map +1 -1
  20. package/dist/lib/package-manager-guard.d.ts +24 -0
  21. package/dist/lib/package-manager-guard.d.ts.map +1 -0
  22. package/dist/lib/package-manager-guard.test.d.ts +2 -0
  23. package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
  24. package/dist/lib/platform-profiles.d.ts +2 -2
  25. package/dist/lib/platform-profiles.d.ts.map +1 -1
  26. package/dist/lib/project-dashboard-standard.d.ts +2 -2
  27. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  28. package/dist/lib/redact.d.ts.map +1 -1
  29. package/dist/lib/sync-dir.d.ts +3 -3
  30. package/dist/lib/sync-dir.d.ts.map +1 -1
  31. package/dist/lib/sync.d.ts +6 -6
  32. package/dist/lib/sync.d.ts.map +1 -1
  33. package/dist/mcp/http.d.ts +0 -13
  34. package/dist/mcp/http.d.ts.map +1 -1
  35. package/dist/mcp/index.js +650 -575
  36. package/dist/mcp/server.d.ts.map +1 -1
  37. package/dist/server/index.d.ts.map +1 -1
  38. package/dist/server/index.js +1756 -17542
  39. package/dist/server/v1.d.ts.map +1 -1
  40. package/dist/status.d.ts +2 -2
  41. package/dist/status.d.ts.map +1 -1
  42. package/dist/storage/cloud-store.d.ts +21 -1
  43. package/dist/storage/cloud-store.d.ts.map +1 -1
  44. package/dist/storage/schema.d.ts.map +1 -1
  45. package/package.json +5 -8
  46. package/dist/cli/storage.d.ts +0 -3
  47. package/dist/cli/storage.d.ts.map +0 -1
  48. package/dist/cli/storage.test.d.ts +0 -2
  49. package/dist/cli/storage.test.d.ts.map +0 -1
  50. package/dist/db/remote-storage.d.ts +0 -13
  51. package/dist/db/remote-storage.d.ts.map +0 -1
  52. package/dist/db/storage-sync.d.ts +0 -53
  53. package/dist/db/storage-sync.d.ts.map +0 -1
  54. package/dist/db/storage-sync.test.d.ts +0 -2
  55. package/dist/db/storage-sync.test.d.ts.map +0 -1
  56. package/dist/server/server.test.d.ts +0 -2
  57. package/dist/server/server.test.d.ts.map +0 -1
  58. package/dist/storage.d.ts +0 -5
  59. package/dist/storage.d.ts.map +0 -1
  60. package/dist/storage.js +0 -537
package/dist/cli/index.js CHANGED
@@ -2112,7 +2112,7 @@ var init_types = __esm(() => {
2112
2112
 
2113
2113
  // src/db/database.ts
2114
2114
  import { Database } from "bun:sqlite";
2115
- import { cpSync, existsSync as existsSync2, mkdirSync, statSync } from "fs";
2115
+ import { cpSync, existsSync as existsSync2, mkdirSync, rmSync, statSync } from "fs";
2116
2116
  import { join as join2 } from "path";
2117
2117
  import { randomUUID as randomUUID3 } from "crypto";
2118
2118
  function getDbPath() {
@@ -2140,6 +2140,9 @@ function slugify(name) {
2140
2140
  function getDatabase(path) {
2141
2141
  if (_db)
2142
2142
  return _db;
2143
+ if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
2144
+ 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.");
2145
+ }
2143
2146
  const dbPath = path || getDbPath();
2144
2147
  const db = new Database(dbPath);
2145
2148
  db.run("PRAGMA journal_mode = WAL");
@@ -2157,6 +2160,16 @@ function resetDatabase() {
2157
2160
  }
2158
2161
  _db = null;
2159
2162
  }
2163
+ function resetLocalDatabase() {
2164
+ resetDatabase();
2165
+ const dbPath = getDbPath();
2166
+ if (dbPath === ":memory:")
2167
+ return;
2168
+ for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
2169
+ if (existsSync2(p))
2170
+ rmSync(p);
2171
+ }
2172
+ }
2160
2173
  function applyMigrations(db) {
2161
2174
  let currentVersion = 0;
2162
2175
  try {
@@ -2183,6 +2196,10 @@ function ensureFeedbackTable(db) {
2183
2196
  )
2184
2197
  `);
2185
2198
  }
2199
+ function insertFeedback(input, db) {
2200
+ const d = db || getDatabase();
2201
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
2202
+ }
2186
2203
  function migrateDotfile() {
2187
2204
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2188
2205
  const oldDirs = [join2(home, ".open-configs"), join2(home, ".configs")];
@@ -2613,6 +2630,135 @@ var init_machine = __esm(() => {
2613
2630
  init_template();
2614
2631
  });
2615
2632
 
2633
+ // src/db/profiles.ts
2634
+ function rowToProfile(row) {
2635
+ return {
2636
+ ...row,
2637
+ selectors: JSON.parse(row.selectors || "{}"),
2638
+ variables: JSON.parse(row.variables || "{}")
2639
+ };
2640
+ }
2641
+ function uniqueProfileSlug(name, db, excludeId) {
2642
+ const base = slugify(name);
2643
+ let slug = base;
2644
+ let i = 1;
2645
+ while (true) {
2646
+ const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
2647
+ if (!existing || existing.id === excludeId)
2648
+ return slug;
2649
+ slug = `${base}-${i++}`;
2650
+ }
2651
+ }
2652
+ function createProfile(input, db) {
2653
+ const d = db || getDatabase();
2654
+ const id = uuid();
2655
+ const ts = now2();
2656
+ const slug = uniqueProfileSlug(input.name, d);
2657
+ d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
2658
+ id,
2659
+ input.name,
2660
+ slug,
2661
+ input.description ?? null,
2662
+ JSON.stringify(input.selectors ?? {}),
2663
+ JSON.stringify(input.variables ?? {}),
2664
+ ts,
2665
+ ts
2666
+ ]);
2667
+ return getProfile(id, d);
2668
+ }
2669
+ function getProfile(idOrSlug, db) {
2670
+ const d = db || getDatabase();
2671
+ const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
2672
+ if (!row)
2673
+ throw new ProfileNotFoundError(idOrSlug);
2674
+ return rowToProfile(row);
2675
+ }
2676
+ function listProfiles(db) {
2677
+ const d = db || getDatabase();
2678
+ return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
2679
+ }
2680
+ function updateProfile(idOrSlug, input, db) {
2681
+ const d = db || getDatabase();
2682
+ const existing = getProfile(idOrSlug, d);
2683
+ const ts = now2();
2684
+ const updates = ["updated_at = ?"];
2685
+ const params = [ts];
2686
+ if (input.name !== undefined) {
2687
+ updates.push("name = ?", "slug = ?");
2688
+ params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
2689
+ }
2690
+ if (input.description !== undefined) {
2691
+ updates.push("description = ?");
2692
+ params.push(input.description);
2693
+ }
2694
+ if (input.selectors !== undefined) {
2695
+ updates.push("selectors = ?");
2696
+ params.push(JSON.stringify(input.selectors));
2697
+ }
2698
+ if (input.variables !== undefined) {
2699
+ updates.push("variables = ?");
2700
+ params.push(JSON.stringify(input.variables));
2701
+ }
2702
+ params.push(existing.id);
2703
+ d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
2704
+ return getProfile(existing.id, d);
2705
+ }
2706
+ function deleteProfile(idOrSlug, db) {
2707
+ const d = db || getDatabase();
2708
+ const existing = getProfile(idOrSlug, d);
2709
+ d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
2710
+ }
2711
+ function addConfigToProfile(profileIdOrSlug, configId, db) {
2712
+ const d = db || getDatabase();
2713
+ const profile = getProfile(profileIdOrSlug, d);
2714
+ const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
2715
+ const order = (maxRow?.max_order ?? -1) + 1;
2716
+ d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
2717
+ }
2718
+ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
2719
+ const d = db || getDatabase();
2720
+ const profile = getProfile(profileIdOrSlug, d);
2721
+ d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
2722
+ }
2723
+ function getProfileConfigs(profileIdOrSlug, db) {
2724
+ const d = db || getDatabase();
2725
+ const profile = getProfile(profileIdOrSlug, d);
2726
+ const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
2727
+ if (rows.length === 0)
2728
+ return [];
2729
+ const ids = rows.map((r) => r.config_id);
2730
+ return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
2731
+ }
2732
+ function profileHasSelectors(profile) {
2733
+ const selectors = profile.selectors ?? {};
2734
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
2735
+ }
2736
+ function profileMatchesMachine(profile, machine) {
2737
+ const selectors = profile.selectors ?? {};
2738
+ const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
2739
+ const value = candidate.trim().toLowerCase();
2740
+ return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
2741
+ });
2742
+ const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
2743
+ const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
2744
+ return osMatches && archMatches && hostnameMatches;
2745
+ }
2746
+ function resolveProfileForMachine(machine = detectMachineContext(), db) {
2747
+ const profiles = listProfiles(db).filter(profileHasSelectors);
2748
+ const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
2749
+ const selectors = profile.selectors;
2750
+ const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
2751
+ return { profile, score };
2752
+ }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
2753
+ return matches[0]?.profile ?? null;
2754
+ }
2755
+ var init_profiles = __esm(() => {
2756
+ init_types();
2757
+ init_database();
2758
+ init_configs();
2759
+ init_machine();
2760
+ });
2761
+
2616
2762
  // src/db/snapshots.ts
2617
2763
  function createSnapshot(configId, content, version, db) {
2618
2764
  const d = db || getDatabase();
@@ -2629,10 +2775,385 @@ function getSnapshot(id, db) {
2629
2775
  const d = db || getDatabase();
2630
2776
  return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
2631
2777
  }
2778
+ function getSnapshotByVersion(configId, version, db) {
2779
+ const d = db || getDatabase();
2780
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
2781
+ }
2782
+ function pruneSnapshots(configId, keep = 10, db) {
2783
+ const d = db || getDatabase();
2784
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
2785
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
2786
+ )`, [configId, configId, keep]);
2787
+ return result.changes;
2788
+ }
2632
2789
  var init_snapshots = __esm(() => {
2633
2790
  init_database();
2634
2791
  });
2635
2792
 
2793
+ // src/db/machines.ts
2794
+ import { arch, hostname, type } from "os";
2795
+ function currentHostname2() {
2796
+ return hostname();
2797
+ }
2798
+ function currentOs() {
2799
+ return type();
2800
+ }
2801
+ function currentArch2() {
2802
+ return arch();
2803
+ }
2804
+ function registerMachine(hostnameStr, os, archStr, db) {
2805
+ const d = db || getDatabase();
2806
+ const h = hostnameStr ?? currentHostname2();
2807
+ const o = os ?? currentOs();
2808
+ const a = archStr ?? currentArch2();
2809
+ const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
2810
+ if (existing) {
2811
+ if (existing.os !== o || existing.arch !== a) {
2812
+ d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
2813
+ return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
2814
+ }
2815
+ return existing;
2816
+ }
2817
+ const id = uuid();
2818
+ const ts = now2();
2819
+ d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
2820
+ return d.query("SELECT * FROM machines WHERE id = ?").get(id);
2821
+ }
2822
+ function updateMachineApplied(hostnameStr, db) {
2823
+ const d = db || getDatabase();
2824
+ const h = hostnameStr ?? currentHostname2();
2825
+ d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now2(), h]);
2826
+ }
2827
+ function listMachines(db) {
2828
+ const d = db || getDatabase();
2829
+ return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
2830
+ }
2831
+ var init_machines = __esm(() => {
2832
+ init_database();
2833
+ });
2834
+
2835
+ // src/data/config-store.ts
2836
+ import { randomUUID as randomUUID5 } from "crypto";
2837
+ function resolveCloudConfig(env = process.env) {
2838
+ const apiUrl = env[API_URL_ENV]?.trim();
2839
+ const apiKey = env[API_KEY_ENV]?.trim();
2840
+ if (!apiUrl && !apiKey)
2841
+ return null;
2842
+ if (!apiUrl || !apiKey) {
2843
+ 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.`);
2844
+ }
2845
+ return { apiUrl, apiKey };
2846
+ }
2847
+ function isCloudMode(env = process.env) {
2848
+ return resolveCloudConfig(env) !== null;
2849
+ }
2850
+
2851
+ class LocalConfigStore {
2852
+ db;
2853
+ mode = "local";
2854
+ constructor(db) {
2855
+ this.db = db;
2856
+ }
2857
+ async listConfigs(filter) {
2858
+ return listConfigs(filter, this.db);
2859
+ }
2860
+ async getConfig(idOrSlug) {
2861
+ return getConfig(idOrSlug, this.db);
2862
+ }
2863
+ async getConfigById(id) {
2864
+ return getConfigById(id, this.db);
2865
+ }
2866
+ async createConfig(input) {
2867
+ return createConfig(input, this.db);
2868
+ }
2869
+ async updateConfig(idOrSlug, input) {
2870
+ return updateConfig(idOrSlug, input, this.db);
2871
+ }
2872
+ async deleteConfig(idOrSlug) {
2873
+ deleteConfig(idOrSlug, this.db);
2874
+ }
2875
+ async getConfigStats() {
2876
+ return getConfigStats(this.db);
2877
+ }
2878
+ async listSnapshots(configId) {
2879
+ return listSnapshots(configId, this.db);
2880
+ }
2881
+ async getSnapshot(id) {
2882
+ return getSnapshot(id, this.db);
2883
+ }
2884
+ async getSnapshotByVersion(configId, version) {
2885
+ return getSnapshotByVersion(configId, version, this.db);
2886
+ }
2887
+ async createSnapshot(configId, content, version) {
2888
+ return createSnapshot(configId, content, version, this.db);
2889
+ }
2890
+ async pruneSnapshots(configId, keep = 10) {
2891
+ return pruneSnapshots(configId, keep, this.db);
2892
+ }
2893
+ async listProfiles() {
2894
+ return listProfiles(this.db);
2895
+ }
2896
+ async getProfile(idOrSlug) {
2897
+ return getProfile(idOrSlug, this.db);
2898
+ }
2899
+ async getProfileConfigs(idOrSlug) {
2900
+ return getProfileConfigs(idOrSlug, this.db);
2901
+ }
2902
+ async createProfile(input) {
2903
+ return createProfile(input, this.db);
2904
+ }
2905
+ async updateProfile(idOrSlug, input) {
2906
+ return updateProfile(idOrSlug, input, this.db);
2907
+ }
2908
+ async deleteProfile(idOrSlug) {
2909
+ deleteProfile(idOrSlug, this.db);
2910
+ }
2911
+ async addConfigToProfile(profileIdOrSlug, configId) {
2912
+ addConfigToProfile(profileIdOrSlug, configId, this.db);
2913
+ }
2914
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
2915
+ removeConfigFromProfile(profileIdOrSlug, configId, this.db);
2916
+ }
2917
+ async resolveProfileForMachine(machine) {
2918
+ return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
2919
+ }
2920
+ async registerMachine(hostname2, os, arch2) {
2921
+ return registerMachine(hostname2, os, arch2, this.db);
2922
+ }
2923
+ async updateMachineApplied(hostname2) {
2924
+ updateMachineApplied(hostname2, this.db);
2925
+ }
2926
+ async listMachines() {
2927
+ return listMachines(this.db);
2928
+ }
2929
+ async sendFeedback(input) {
2930
+ insertFeedback(input, this.db);
2931
+ }
2932
+ async reset() {
2933
+ resetLocalDatabase();
2934
+ }
2935
+ }
2936
+
2937
+ class CloudConfigStore {
2938
+ mode = "api";
2939
+ base;
2940
+ apiKey;
2941
+ timeoutMs;
2942
+ constructor(config) {
2943
+ this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
2944
+ this.apiKey = config.apiKey;
2945
+ this.timeoutMs = config.timeoutMs ?? 30000;
2946
+ }
2947
+ async request(method, path, body, opts = {}) {
2948
+ const controller = new AbortController;
2949
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
2950
+ const headers = {
2951
+ Authorization: `Bearer ${this.apiKey}`,
2952
+ Accept: "application/json"
2953
+ };
2954
+ if (body !== undefined)
2955
+ headers["Content-Type"] = "application/json";
2956
+ if (opts.idempotent)
2957
+ headers["Idempotency-Key"] = randomUUID5();
2958
+ try {
2959
+ const res = await fetch(`${this.base}${path}`, {
2960
+ method,
2961
+ headers,
2962
+ body: body === undefined ? undefined : JSON.stringify(body),
2963
+ signal: controller.signal
2964
+ });
2965
+ if (res.status === 404 && opts.allow404)
2966
+ return { status: 404, data: null };
2967
+ const text = await res.text();
2968
+ let parsed = null;
2969
+ if (text) {
2970
+ try {
2971
+ parsed = JSON.parse(text);
2972
+ } catch {
2973
+ parsed = text;
2974
+ }
2975
+ }
2976
+ if (!res.ok) {
2977
+ const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
2978
+ throw new CloudHttpError(res.status, message, parsed);
2979
+ }
2980
+ return { status: res.status, data: parsed };
2981
+ } finally {
2982
+ clearTimeout(timer);
2983
+ }
2984
+ }
2985
+ async listConfigs(filter = {}) {
2986
+ const params = new URLSearchParams;
2987
+ if (filter.category)
2988
+ params.set("category", filter.category);
2989
+ if (filter.agent)
2990
+ params.set("agent", filter.agent);
2991
+ if (filter.kind)
2992
+ params.set("kind", filter.kind);
2993
+ if (filter.search)
2994
+ params.set("search", filter.search);
2995
+ const qs = params.toString();
2996
+ const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
2997
+ let configs = data?.configs ?? [];
2998
+ if (filter.tags && filter.tags.length > 0) {
2999
+ configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
3000
+ }
3001
+ if (filter.is_template !== undefined) {
3002
+ configs = configs.filter((c) => c.is_template === filter.is_template);
3003
+ }
3004
+ return configs;
3005
+ }
3006
+ async getConfig(idOrSlug) {
3007
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3008
+ if (status === 404 || !data?.config)
3009
+ throw new ConfigNotFoundError(idOrSlug);
3010
+ return data.config;
3011
+ }
3012
+ async getConfigById(id) {
3013
+ return this.getConfig(id);
3014
+ }
3015
+ async createConfig(input) {
3016
+ const { data } = await this.request("POST", "/configs", input, {
3017
+ idempotent: true
3018
+ });
3019
+ return data.config;
3020
+ }
3021
+ async updateConfig(idOrSlug, input) {
3022
+ const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
3023
+ return data.config;
3024
+ }
3025
+ async deleteConfig(idOrSlug) {
3026
+ const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3027
+ if (status === 404)
3028
+ throw new ConfigNotFoundError(idOrSlug);
3029
+ }
3030
+ async getConfigStats() {
3031
+ const { data } = await this.request("GET", "/stats");
3032
+ return data ?? { total: 0 };
3033
+ }
3034
+ async listSnapshots(configId) {
3035
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
3036
+ return data?.snapshots ?? [];
3037
+ }
3038
+ async getSnapshot(id) {
3039
+ const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
3040
+ if (status === 404 || !data?.snapshot)
3041
+ return null;
3042
+ return data.snapshot;
3043
+ }
3044
+ async getSnapshotByVersion(configId, version) {
3045
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
3046
+ if (status === 404 || !data?.snapshot)
3047
+ return null;
3048
+ return data.snapshot;
3049
+ }
3050
+ async createSnapshot(configId, content, version) {
3051
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
3052
+ return data.snapshot;
3053
+ }
3054
+ async pruneSnapshots(configId, keep = 10) {
3055
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
3056
+ return data?.pruned ?? 0;
3057
+ }
3058
+ async listProfiles() {
3059
+ const { data } = await this.request("GET", "/profiles");
3060
+ return data?.profiles ?? [];
3061
+ }
3062
+ async getProfile(idOrSlug) {
3063
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3064
+ if (status === 404 || !data?.profile)
3065
+ throw new ProfileNotFoundError(idOrSlug);
3066
+ const { configs: _configs, ...profile } = data.profile;
3067
+ return profile;
3068
+ }
3069
+ async getProfileConfigs(idOrSlug) {
3070
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3071
+ if (status === 404 || !data?.profile)
3072
+ throw new ProfileNotFoundError(idOrSlug);
3073
+ return data.profile.configs ?? [];
3074
+ }
3075
+ async createProfile(input) {
3076
+ const { data } = await this.request("POST", "/profiles", input, {
3077
+ idempotent: true
3078
+ });
3079
+ return data.profile;
3080
+ }
3081
+ async updateProfile(idOrSlug, input) {
3082
+ const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
3083
+ return data.profile;
3084
+ }
3085
+ async deleteProfile(idOrSlug) {
3086
+ const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3087
+ if (status === 404)
3088
+ throw new ProfileNotFoundError(idOrSlug);
3089
+ }
3090
+ async addConfigToProfile(profileIdOrSlug, configId) {
3091
+ await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
3092
+ }
3093
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
3094
+ await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
3095
+ }
3096
+ async resolveProfileForMachine(machine) {
3097
+ const params = new URLSearchParams;
3098
+ if (machine?.hostname)
3099
+ params.set("hostname", machine.hostname);
3100
+ if (machine?.os)
3101
+ params.set("os", machine.os);
3102
+ if (machine?.arch)
3103
+ params.set("arch", machine.arch);
3104
+ const qs = params.toString();
3105
+ const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
3106
+ if (status === 404 || !data?.profile)
3107
+ return null;
3108
+ return data.profile;
3109
+ }
3110
+ async registerMachine(hostname2, os, arch2) {
3111
+ const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
3112
+ return data.machine;
3113
+ }
3114
+ async updateMachineApplied(hostname2) {
3115
+ await this.request("POST", "/machines/applied", { hostname: hostname2 });
3116
+ }
3117
+ async listMachines() {
3118
+ const { data } = await this.request("GET", "/machines");
3119
+ return data?.machines ?? [];
3120
+ }
3121
+ async sendFeedback(input) {
3122
+ await this.request("POST", "/feedback", {
3123
+ message: input.message,
3124
+ email: input.email ?? undefined,
3125
+ category: input.category ?? undefined,
3126
+ version: input.version ?? undefined
3127
+ });
3128
+ }
3129
+ async reset() {
3130
+ 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.");
3131
+ }
3132
+ }
3133
+ function resolveConfigStore(env = process.env) {
3134
+ const cloud = resolveCloudConfig(env);
3135
+ return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
3136
+ }
3137
+ var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
3138
+ var init_config_store = __esm(() => {
3139
+ init_configs();
3140
+ init_profiles();
3141
+ init_snapshots();
3142
+ init_machines();
3143
+ init_database();
3144
+ init_types();
3145
+ CloudHttpError = class CloudHttpError extends Error {
3146
+ status;
3147
+ body;
3148
+ constructor(status, message, body) {
3149
+ super(message);
3150
+ this.status = status;
3151
+ this.body = body;
3152
+ this.name = "CloudHttpError";
3153
+ }
3154
+ };
3155
+ });
3156
+
2636
3157
  // src/lib/transforms.ts
2637
3158
  import { basename, extname } from "path";
2638
3159
  function ensureTrailingNewline(content) {
@@ -2800,8 +3321,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
2800
3321
  mkdirSync2(dir, { recursive: true });
2801
3322
  }
2802
3323
  if (previousContent !== null && changed) {
2803
- const db = opts.db || getDatabase();
2804
- createSnapshot(config.id, previousContent, config.version, db);
3324
+ const store = opts.store ?? resolveConfigStore();
3325
+ await store.createSnapshot(config.id, previousContent, config.version);
2805
3326
  }
2806
3327
  writeFileSync(path, renderedContent, "utf-8");
2807
3328
  }
@@ -2827,8 +3348,8 @@ async function applyConfig(config, opts = {}) {
2827
3348
  if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
2828
3349
  throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
2829
3350
  }
2830
- const db = opts.db || getDatabase();
2831
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
3351
+ const store = opts.store ?? resolveConfigStore();
3352
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
2832
3353
  if (isGeneratedOutputTarget(config, contextConfigs)) {
2833
3354
  throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
2834
3355
  }
@@ -2849,7 +3370,7 @@ async function applyConfig(config, opts = {}) {
2849
3370
  };
2850
3371
  }
2851
3372
  if (!opts.dryRun) {
2852
- updateConfig(config.id, { synced_at: now2() }, db);
3373
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
2853
3374
  }
2854
3375
  return result;
2855
3376
  }
@@ -2871,9 +3392,7 @@ async function applyConfigs(configs, opts = {}) {
2871
3392
  }
2872
3393
  var init_apply = __esm(() => {
2873
3394
  init_types();
2874
- init_database();
2875
- init_configs();
2876
- init_snapshots();
3395
+ init_config_store();
2877
3396
  init_machine();
2878
3397
  init_transforms();
2879
3398
  });
@@ -2961,9 +3480,9 @@ function redactIni(content) {
2961
3480
  for (let i = 0;i < lines.length; i++) {
2962
3481
  const line = lines[i];
2963
3482
  const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
2964
- if (authM && !authM[2].startsWith("{{")) {
2965
- redacted.push({ varName: "NPM_AUTH_TOKEN", line: i + 1, reason: "npm auth token" });
2966
- out.push(`${authM[1]}{{NPM_AUTH_TOKEN}}`);
3483
+ if (authM && !isReferenceValue(authM[2].trim())) {
3484
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
3485
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
2967
3486
  continue;
2968
3487
  }
2969
3488
  const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
@@ -3003,6 +3522,8 @@ function redactGeneric(content) {
3003
3522
  function shouldRedactKeyValue(key, value) {
3004
3523
  if (!value || value.startsWith("{{"))
3005
3524
  return false;
3525
+ if (isReferenceValue(value.trim()))
3526
+ return false;
3006
3527
  if (value.length < MIN_SECRET_VALUE_LEN)
3007
3528
  return false;
3008
3529
  if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
@@ -3024,6 +3545,9 @@ function reasonFor(key, value) {
3024
3545
  }
3025
3546
  return "secret value pattern";
3026
3547
  }
3548
+ function isReferenceValue(value) {
3549
+ 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);
3550
+ }
3027
3551
  function redactContent(content, format) {
3028
3552
  switch (format) {
3029
3553
  case "shell":
@@ -3065,14 +3589,14 @@ function shouldSkip(p) {
3065
3589
  return SKIP.some((s) => p.includes(s));
3066
3590
  }
3067
3591
  async function syncFromDir(dir, opts = {}) {
3068
- const d = opts.db || getDatabase();
3592
+ const store = opts.store ?? resolveConfigStore();
3069
3593
  const absDir = expandPath(dir);
3070
3594
  if (!existsSync5(absDir))
3071
3595
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
3072
3596
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
3073
3597
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3074
3598
  const home = homedir4();
3075
- const allConfigs = listConfigs(undefined, d);
3599
+ const allConfigs = await store.listConfigs();
3076
3600
  for (const file of files) {
3077
3601
  if (shouldSkip(file)) {
3078
3602
  result.skipped.push(file);
@@ -3088,11 +3612,11 @@ async function syncFromDir(dir, opts = {}) {
3088
3612
  const existing = allConfigs.find((c) => c.target_path === targetPath);
3089
3613
  if (!existing) {
3090
3614
  if (!opts.dryRun)
3091
- createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
3615
+ await store.createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
3092
3616
  result.added++;
3093
3617
  } else if (existing.content !== content) {
3094
3618
  if (!opts.dryRun)
3095
- updateConfig(existing.id, { content }, d);
3619
+ await store.updateConfig(existing.id, { content });
3096
3620
  result.updated++;
3097
3621
  } else {
3098
3622
  result.unchanged++;
@@ -3104,17 +3628,17 @@ async function syncFromDir(dir, opts = {}) {
3104
3628
  return result;
3105
3629
  }
3106
3630
  async function syncToDir(dir, opts = {}) {
3107
- const d = opts.db || getDatabase();
3631
+ const store = opts.store ?? resolveConfigStore();
3108
3632
  const home = homedir4();
3109
3633
  const absDir = expandPath(dir);
3110
3634
  const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
3111
- const configs = listConfigs(undefined, d).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
3635
+ const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
3112
3636
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3113
3637
  for (const config of configs) {
3114
3638
  if (config.kind === "reference")
3115
3639
  continue;
3116
3640
  try {
3117
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
3641
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store });
3118
3642
  r.changed ? result.updated++ : result.unchanged++;
3119
3643
  } catch {
3120
3644
  result.skipped.push(config.target_path || config.id);
@@ -3136,8 +3660,7 @@ function walkDir(dir, files = []) {
3136
3660
  }
3137
3661
  var SKIP;
3138
3662
  var init_sync_dir = __esm(() => {
3139
- init_database();
3140
- init_configs();
3663
+ init_config_store();
3141
3664
  init_apply();
3142
3665
  init_sync();
3143
3666
  SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
@@ -3210,11 +3733,11 @@ function isKnownGeneratedTargetPath(targetPath) {
3210
3733
  return hasClaudeRuleSourceForCursorTarget(targetPath);
3211
3734
  }
3212
3735
  async function syncProject(opts) {
3213
- const d = opts.db || getDatabase();
3736
+ const store = opts.store ?? resolveConfigStore();
3214
3737
  const absDir = expandPath(opts.projectDir);
3215
3738
  const projectName = absDir.split("/").pop() || "project";
3216
3739
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3217
- const allConfigs = listConfigs(undefined, d);
3740
+ const allConfigs = await store.listConfigs();
3218
3741
  const machine = detectMachineContext();
3219
3742
  for (const pf of PROJECT_CONFIG_FILES) {
3220
3743
  const abs = join5(absDir, pf.file);
@@ -3236,11 +3759,11 @@ async function syncProject(opts) {
3236
3759
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
3237
3760
  if (!existing) {
3238
3761
  if (!opts.dryRun)
3239
- createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
3762
+ await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
3240
3763
  result.added++;
3241
3764
  } else if (existing.content !== content) {
3242
3765
  if (!opts.dryRun)
3243
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3766
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3244
3767
  result.updated++;
3245
3768
  } else {
3246
3769
  result.unchanged++;
@@ -3265,11 +3788,11 @@ async function syncProject(opts) {
3265
3788
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
3266
3789
  if (!existing) {
3267
3790
  if (!opts.dryRun)
3268
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
3791
+ await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
3269
3792
  result.added++;
3270
3793
  } else if (existing.content !== content) {
3271
3794
  if (!opts.dryRun)
3272
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3795
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3273
3796
  result.updated++;
3274
3797
  } else {
3275
3798
  result.unchanged++;
@@ -3279,7 +3802,7 @@ async function syncProject(opts) {
3279
3802
  return result;
3280
3803
  }
3281
3804
  async function syncKnown(opts = {}) {
3282
- const d = opts.db || getDatabase();
3805
+ const store = opts.store ?? resolveConfigStore();
3283
3806
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3284
3807
  const home = getConfigHome();
3285
3808
  const machine = detectMachineContext();
@@ -3288,7 +3811,7 @@ async function syncKnown(opts = {}) {
3288
3811
  targets = targets.filter((k) => k.agent === opts.agent);
3289
3812
  if (opts.category)
3290
3813
  targets = targets.filter((k) => k.category === opts.category);
3291
- const allConfigs = listConfigs(undefined, d);
3814
+ const allConfigs = await store.listConfigs();
3292
3815
  const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
3293
3816
  for (const known of targets) {
3294
3817
  if (known.rulesDir) {
@@ -3317,15 +3840,15 @@ async function syncKnown(opts = {}) {
3317
3840
  const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
3318
3841
  if (!existing) {
3319
3842
  if (!opts.dryRun)
3320
- createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
3843
+ await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
3321
3844
  result.added++;
3322
3845
  } else if (existing.content !== content) {
3323
3846
  if (!opts.dryRun)
3324
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
3847
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
3325
3848
  result.updated++;
3326
3849
  } else if (!outputsEqual(existing.outputs, outputs)) {
3327
3850
  if (!opts.dryRun)
3328
- updateConfig(existing.id, { outputs }, d);
3851
+ await store.updateConfig(existing.id, { outputs });
3329
3852
  result.updated++;
3330
3853
  } else {
3331
3854
  result.unchanged++;
@@ -3357,7 +3880,7 @@ async function syncKnown(opts = {}) {
3357
3880
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
3358
3881
  if (!existing) {
3359
3882
  if (!opts.dryRun) {
3360
- createConfig({
3883
+ await store.createConfig({
3361
3884
  name: known.name,
3362
3885
  category: known.category,
3363
3886
  agent: known.agent,
@@ -3368,16 +3891,16 @@ async function syncKnown(opts = {}) {
3368
3891
  description: known.description,
3369
3892
  is_template: isTemplate2,
3370
3893
  outputs: known.outputs
3371
- }, d);
3894
+ });
3372
3895
  }
3373
3896
  result.added++;
3374
3897
  } else if (existing.content !== content) {
3375
3898
  if (!opts.dryRun)
3376
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
3899
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
3377
3900
  result.updated++;
3378
3901
  } else if (!outputsEqual(existing.outputs, known.outputs)) {
3379
3902
  if (!opts.dryRun)
3380
- updateConfig(existing.id, { outputs: known.outputs }, d);
3903
+ await store.updateConfig(existing.id, { outputs: known.outputs });
3381
3904
  result.updated++;
3382
3905
  } else {
3383
3906
  result.unchanged++;
@@ -3389,9 +3912,9 @@ async function syncKnown(opts = {}) {
3389
3912
  return result;
3390
3913
  }
3391
3914
  async function syncToDisk(opts = {}) {
3392
- const d = opts.db || getDatabase();
3915
+ const store = opts.store ?? resolveConfigStore();
3393
3916
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3394
- const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
3917
+ const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
3395
3918
  const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
3396
3919
  let configs = allFileConfigs.filter((config) => {
3397
3920
  return !isGeneratedOutputTarget2(config, outputOwners);
@@ -3404,7 +3927,7 @@ async function syncToDisk(opts = {}) {
3404
3927
  if (!config.target_path && config.outputs.length === 0)
3405
3928
  continue;
3406
3929
  try {
3407
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
3930
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
3408
3931
  r.changed ? result.updated++ : result.unchanged++;
3409
3932
  } catch {
3410
3933
  result.skipped.push(config.target_path ?? config.id);
@@ -3441,12 +3964,12 @@ function buildDiff(expectedContent, targetPath) {
3441
3964
  return lines.join(`
3442
3965
  `);
3443
3966
  }
3444
- function diffConfig(config, opts = {}) {
3967
+ async function diffConfig(config, opts = {}) {
3445
3968
  if (!config.target_path && config.outputs.length === 0)
3446
3969
  return "(reference \u2014 no target path)";
3447
3970
  const diffs = [];
3448
- const db = opts.db || getDatabase();
3449
- const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
3971
+ const store = opts.store ?? resolveConfigStore();
3972
+ const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
3450
3973
  if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
3451
3974
  return "(generated output \u2014 managed by fan-out)";
3452
3975
  }
@@ -3525,8 +4048,7 @@ function detectFormat(filePath) {
3525
4048
  }
3526
4049
  var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
3527
4050
  var init_sync = __esm(() => {
3528
- init_database();
3529
- init_configs();
4051
+ init_config_store();
3530
4052
  init_apply();
3531
4053
  init_redact();
3532
4054
  init_machine();
@@ -3581,100 +4103,439 @@ var init_sync = __esm(() => {
3581
4103
  ];
3582
4104
  });
3583
4105
 
3584
- // node_modules/.pnpm/@hasna+events@0.1.13/node_modules/@hasna/events/dist/commander.js
3585
- import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
3586
- import { existsSync } from "fs";
3587
- import { homedir } from "os";
3588
- import { join } from "path";
3589
- import { createHmac, timingSafeEqual } from "crypto";
3590
- import { randomUUID } from "crypto";
3591
- import { spawn } from "child_process";
3592
- import { randomUUID as randomUUID2 } from "crypto";
3593
- function getPathValue(input, path) {
3594
- return path.split(".").reduce((value, part) => {
3595
- if (value && typeof value === "object" && part in value) {
3596
- return value[part];
4106
+ // src/lib/package-manager-guard.ts
4107
+ var exports_package_manager_guard = {};
4108
+ __export(exports_package_manager_guard, {
4109
+ scanPackageManagerSecrets: () => scanPackageManagerSecrets
4110
+ });
4111
+ import { execFileSync } from "child_process";
4112
+ import { existsSync as existsSync12, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
4113
+ import { homedir as homedir6 } from "os";
4114
+ import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative as relative4, resolve as resolve6 } from "path";
4115
+ function scanPackageManagerSecrets(options = {}) {
4116
+ const cwd = options.cwd ? resolve6(options.cwd) : process.cwd();
4117
+ const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve6(cwd, root));
4118
+ const findings = [];
4119
+ let scannedFiles = 0;
4120
+ for (const root of roots) {
4121
+ if (!existsSync12(root))
4122
+ continue;
4123
+ const stat = lstatSync2(root);
4124
+ if (stat.isFile()) {
4125
+ if (!shouldScanRepoFile(root))
4126
+ continue;
4127
+ const text = readTextFile(root);
4128
+ if (text === null)
4129
+ continue;
4130
+ scannedFiles++;
4131
+ findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
4132
+ continue;
3597
4133
  }
3598
- return;
3599
- }, input);
3600
- }
3601
- function getFieldValues(input, path) {
3602
- const values = [];
3603
- const push = (value) => {
3604
- if (!values.some((item) => Object.is(item, value)))
3605
- values.push(value);
4134
+ if (!stat.isDirectory())
4135
+ continue;
4136
+ const tracked = trackedFiles(root);
4137
+ for (const file of collectRepoFiles(root)) {
4138
+ const rel = toPosix(relative4(root, file));
4139
+ const isTracked = tracked.has(rel);
4140
+ const text = readTextFile(file);
4141
+ if (text === null)
4142
+ continue;
4143
+ scannedFiles++;
4144
+ findings.push(...scanFile(file, text, classifyRepoFile(file), isTracked, root));
4145
+ }
4146
+ }
4147
+ if (options.includeHome) {
4148
+ const home = homedir6();
4149
+ for (const name of HOME_FILES) {
4150
+ const file = join10(home, name);
4151
+ if (!existsSync12(file))
4152
+ continue;
4153
+ const text = readTextFile(file);
4154
+ if (text === null)
4155
+ continue;
4156
+ scannedFiles++;
4157
+ findings.push(...scanFile(file, text, classifyHomeFile(name), false, home));
4158
+ }
4159
+ }
4160
+ findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
4161
+ return {
4162
+ clean: findings.length === 0,
4163
+ scannedFiles,
4164
+ scannedRoots: roots,
4165
+ findings
3606
4166
  };
3607
- if (path.includes(".") && path in input)
3608
- push(input[path]);
3609
- const nestedValue = getPathValue(input, path);
3610
- if (nestedValue !== undefined || !path.includes("."))
3611
- push(nestedValue);
3612
- return values;
3613
- }
3614
- function wildcardToRegExp(pattern, options = {}) {
3615
- let body = "";
3616
- for (let index = 0;index < pattern.length; index += 1) {
3617
- const char = pattern[index];
3618
- if (char === "*") {
3619
- if (pattern[index + 1] === "*") {
3620
- body += ".*";
3621
- index += 1;
3622
- } else {
3623
- body += options.segmentSafe ? "[^/]*" : ".*";
4167
+ }
4168
+ function collectRepoFiles(root) {
4169
+ const out = [];
4170
+ const visit = (dir) => {
4171
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
4172
+ if (entry.isDirectory()) {
4173
+ if (SKIP_DIRS.has(entry.name))
4174
+ continue;
4175
+ visit(join10(dir, entry.name));
4176
+ continue;
3624
4177
  }
4178
+ if (!entry.isFile())
4179
+ continue;
4180
+ const file = join10(dir, entry.name);
4181
+ if (shouldScanRepoFile(file))
4182
+ out.push(file);
4183
+ }
4184
+ };
4185
+ visit(root);
4186
+ return out;
4187
+ }
4188
+ function shouldScanRepoFile(file) {
4189
+ const name = basename5(file);
4190
+ return isNpmrcName(name) || isBunConfigName(name) || LOCKFILE_NAMES.has(name);
4191
+ }
4192
+ function classifyRepoFile(file) {
4193
+ const name = basename5(file);
4194
+ if (isNpmrcName(name))
4195
+ return "repo-npmrc";
4196
+ if (isBunConfigName(name))
4197
+ return "bun-config";
4198
+ return "lockfile";
4199
+ }
4200
+ function classifyHomeFile(name) {
4201
+ if (name === ".npmrc")
4202
+ return "home-npmrc";
4203
+ if (isBunConfigName(name))
4204
+ return "bun-config";
4205
+ return "shell-profile";
4206
+ }
4207
+ function isBunConfigName(name) {
4208
+ return name === "bunfig.toml" || name === ".bunfig.toml";
4209
+ }
4210
+ function isNpmrcName(name) {
4211
+ return name === ".npmrc" || name.startsWith(".npmrc.") || name.endsWith(".npmrc");
4212
+ }
4213
+ function readTextFile(file) {
4214
+ try {
4215
+ const stat = lstatSync2(file);
4216
+ if (!stat.isFile() || stat.size > 5000000)
4217
+ return null;
4218
+ const buf = readFileSync8(file);
4219
+ if (buf.includes(0))
4220
+ return null;
4221
+ return buf.toString("utf-8");
4222
+ } catch {
4223
+ return null;
4224
+ }
4225
+ }
4226
+ function scanFile(file, text, surface, tracked, root) {
4227
+ const findings = [];
4228
+ const path = displayPath(file, root);
4229
+ if (surface === "bun-config")
4230
+ return scanBunConfigFile(text, path, tracked);
4231
+ const lines = text.split(/\r?\n/);
4232
+ for (let i = 0;i < lines.length; i++) {
4233
+ const line = lines[i];
4234
+ const lineNo = i + 1;
4235
+ if (surface === "repo-npmrc" || surface === "home-npmrc") {
4236
+ findings.push(...scanNpmrcLine(line, path, lineNo, surface, tracked));
4237
+ } else if (surface === "shell-profile") {
4238
+ findings.push(...scanShellProfileLine(line, path, lineNo, tracked));
3625
4239
  } else {
3626
- body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
4240
+ findings.push(...scanLockfileLine(line, path, lineNo, tracked));
4241
+ }
4242
+ }
4243
+ return findings;
4244
+ }
4245
+ function scanNpmrcLine(lineText, path, line, surface, tracked) {
4246
+ const findings = [];
4247
+ const stripped = lineText.trim();
4248
+ if (stripped === "" || stripped.startsWith("#") || stripped.startsWith(";"))
4249
+ return findings;
4250
+ const auth = stripped.match(/(?:^|:)(_[A-Za-z]*(?:auth|password)[A-Za-z]*|password)\s*=\s*(.+)$/i);
4251
+ if (auth) {
4252
+ const value = stripQuotes(stripInlineComment(auth[2].trim()));
4253
+ if (value && !isSafeReference(value)) {
4254
+ findings.push({
4255
+ path,
4256
+ line,
4257
+ rule: "npmrc-literal-auth",
4258
+ surface,
4259
+ severity: "error",
4260
+ tracked,
4261
+ detail: tracked ? "tracked npm auth entry uses a literal value" : "npm auth entry uses a literal value"
4262
+ });
4263
+ }
4264
+ }
4265
+ findings.push(...scanCredentialedUrl(stripped, path, line, surface, tracked));
4266
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, surface, tracked));
4267
+ return findings;
4268
+ }
4269
+ function scanBunConfigFile(text, path, tracked) {
4270
+ const findings = [];
4271
+ const lines = text.split(/\r?\n/);
4272
+ let inReleaseAgeExcludes = false;
4273
+ let hasMinimumReleaseAge = false;
4274
+ for (let i = 0;i < lines.length; i++) {
4275
+ const lineText = lines[i];
4276
+ const line = i + 1;
4277
+ const stripped = lineText.trim();
4278
+ if (stripped === "" || stripped.startsWith("#"))
4279
+ continue;
4280
+ const releaseAge = stripped.match(/^minimumReleaseAge\s*=\s*(?:"([^"]+)"|'([^']+)'|([0-9]+))\s*(?:#.*)?$/i);
4281
+ if (releaseAge) {
4282
+ hasMinimumReleaseAge = true;
4283
+ const rawValue = releaseAge[1] ?? releaseAge[2] ?? releaseAge[3] ?? "";
4284
+ const value = Number(rawValue);
4285
+ if (!Number.isFinite(value) || value <= 0) {
4286
+ findings.push({
4287
+ path,
4288
+ line,
4289
+ rule: "bun-release-age-disabled",
4290
+ surface: "bun-config",
4291
+ severity: "error",
4292
+ tracked,
4293
+ detail: "Bun release-age quarantine is disabled"
4294
+ });
4295
+ }
4296
+ }
4297
+ const startsReleaseAgeExcludes = /minimumReleaseAgeExcludes/i.test(stripped);
4298
+ const scanExcludes = startsReleaseAgeExcludes || inReleaseAgeExcludes;
4299
+ if (scanExcludes) {
4300
+ const quoted = [...stripped.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
4301
+ for (const item of quoted) {
4302
+ if (!isExactHasnaPackageName(item)) {
4303
+ findings.push({
4304
+ path,
4305
+ line,
4306
+ rule: "bun-release-age-broad-exclude",
4307
+ surface: "bun-config",
4308
+ severity: "error",
4309
+ tracked,
4310
+ detail: "Bun release-age exclude must be an exact @hasna package name"
4311
+ });
4312
+ }
4313
+ }
4314
+ }
4315
+ inReleaseAgeExcludes = startsReleaseAgeExcludes ? stripped.includes("[") && !stripped.includes("]") : inReleaseAgeExcludes && !stripped.includes("]");
4316
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "bun-config", tracked));
4317
+ }
4318
+ if (!hasMinimumReleaseAge) {
4319
+ findings.push({
4320
+ path,
4321
+ line: 1,
4322
+ rule: "bun-release-age-missing",
4323
+ surface: "bun-config",
4324
+ severity: "error",
4325
+ tracked,
4326
+ detail: "Bun release-age quarantine must be configured with a positive minimumReleaseAge"
4327
+ });
4328
+ }
4329
+ return findings;
4330
+ }
4331
+ function scanShellProfileLine(lineText, path, line, tracked) {
4332
+ const findings = [];
4333
+ const stripped = lineText.trim();
4334
+ if (stripped === "" || stripped.startsWith("#"))
4335
+ return findings;
4336
+ const assignment = stripped.match(/^(?:export\s+)?(NPM(?:_CONFIG)?_[A-Z0-9_]*TOKEN|NODE_AUTH_TOKEN|NPM_TOKEN)\s*=\s*(.+)$/);
4337
+ if (assignment) {
4338
+ const value = stripQuotes(stripInlineComment(assignment[2].trim()));
4339
+ if (value && !isSafeReference(value)) {
4340
+ findings.push({
4341
+ path,
4342
+ line,
4343
+ rule: "shell-literal-package-token",
4344
+ surface: "shell-profile",
4345
+ severity: "error",
4346
+ tracked,
4347
+ detail: "shell profile package-manager token uses a literal value"
4348
+ });
4349
+ }
4350
+ }
4351
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "shell-profile", tracked));
4352
+ return findings;
4353
+ }
4354
+ function scanLockfileLine(lineText, path, line, tracked) {
4355
+ const findings = scanKnownTokenPatterns(lineText, path, line, "lockfile", tracked);
4356
+ if (/(?:^|:)_authToken\s*=\s*/i.test(lineText) && !/\$\{[A-Z0-9_]+\}|\{\{[A-Z0-9_]+\}\}/.test(lineText)) {
4357
+ findings.push({
4358
+ path,
4359
+ line,
4360
+ rule: "lockfile-auth-token",
4361
+ surface: "lockfile",
4362
+ severity: "error",
4363
+ tracked,
4364
+ detail: "lockfile contains package-manager auth token material"
4365
+ });
4366
+ }
4367
+ return findings;
4368
+ }
4369
+ function scanKnownTokenPatterns(lineText, path, line, surface, tracked) {
4370
+ const findings = [];
4371
+ for (const pattern of TOKEN_VALUE_PATTERNS) {
4372
+ if (pattern.re.test(lineText)) {
4373
+ findings.push({
4374
+ path,
4375
+ line,
4376
+ rule: pattern.rule,
4377
+ surface,
4378
+ severity: "error",
4379
+ tracked,
4380
+ detail: pattern.detail
4381
+ });
4382
+ }
4383
+ }
4384
+ return findings;
4385
+ }
4386
+ function scanCredentialedUrl(lineText, path, line, surface, tracked) {
4387
+ const findings = [];
4388
+ for (const match of lineText.matchAll(/\bhttps?:\/\/([^/\s#;]+)@/gi)) {
4389
+ const userInfo = match[1];
4390
+ const credentialPart = userInfo.includes(":") ? userInfo.split(":").slice(1).join(":") : userInfo;
4391
+ if (credentialPart && !isSafeReference(credentialPart)) {
4392
+ findings.push({
4393
+ path,
4394
+ line,
4395
+ rule: "package-manager-url-credentials",
4396
+ surface,
4397
+ severity: "error",
4398
+ tracked,
4399
+ detail: "package-manager URL embeds literal credentials"
4400
+ });
3627
4401
  }
3628
4402
  }
3629
- return new RegExp(`^${body}$`);
4403
+ return findings;
3630
4404
  }
3631
- function matchString(value, matcher, options = {}) {
4405
+ function trackedFiles(root) {
4406
+ try {
4407
+ const output = execFileSync("git", ["-C", root, "ls-files", "-z"], {
4408
+ encoding: "utf-8",
4409
+ stdio: ["ignore", "pipe", "ignore"]
4410
+ });
4411
+ return new Set(output.split("\x00").filter(Boolean).map(toPosix));
4412
+ } catch {
4413
+ return new Set;
4414
+ }
4415
+ }
4416
+ function isTrackedFile(file) {
4417
+ try {
4418
+ const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
4419
+ encoding: "utf-8",
4420
+ stdio: ["ignore", "pipe", "ignore"]
4421
+ }).trim();
4422
+ const rel = toPosix(relative4(repoRoot, file));
4423
+ execFileSync("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
4424
+ stdio: ["ignore", "ignore", "ignore"]
4425
+ });
4426
+ return true;
4427
+ } catch {
4428
+ return false;
4429
+ }
4430
+ }
4431
+ function isExactHasnaPackageName(item) {
4432
+ return /^@hasna\/[a-z0-9][a-z0-9._-]*$/.test(item);
4433
+ }
4434
+ function isSafeReference(value) {
4435
+ const trimmed = stripQuotes(value.trim());
4436
+ 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);
4437
+ }
4438
+ function stripQuotes(value) {
4439
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4440
+ return value.slice(1, -1);
4441
+ }
4442
+ return value;
4443
+ }
4444
+ function stripInlineComment(value) {
4445
+ return value.replace(/\s[#;].*$/, "").trim();
4446
+ }
4447
+ function displayPath(file, root) {
4448
+ const home = homedir6();
4449
+ if (root === home && (file === home || file.startsWith(home + "/")))
4450
+ return "~/" + toPosix(relative4(home, file));
4451
+ if (isAbsolute3(root) && file.startsWith(root + "/"))
4452
+ return toPosix(relative4(root, file));
4453
+ if (file === home || file.startsWith(home + "/"))
4454
+ return "~/" + toPosix(relative4(home, file));
4455
+ return file;
4456
+ }
4457
+ function toPosix(path) {
4458
+ return path.split("\\").join("/");
4459
+ }
4460
+ var SKIP_DIRS, LOCKFILE_NAMES, HOME_FILES, TOKEN_VALUE_PATTERNS;
4461
+ var init_package_manager_guard = __esm(() => {
4462
+ SKIP_DIRS = new Set([
4463
+ ".git",
4464
+ "node_modules",
4465
+ "dist",
4466
+ "build",
4467
+ "coverage",
4468
+ ".next",
4469
+ ".turbo",
4470
+ ".cache"
4471
+ ]);
4472
+ LOCKFILE_NAMES = new Set([
4473
+ "bun.lock",
4474
+ "package-lock.json",
4475
+ "npm-shrinkwrap.json",
4476
+ "pnpm-lock.yaml",
4477
+ "yarn.lock"
4478
+ ]);
4479
+ HOME_FILES = [
4480
+ ".npmrc",
4481
+ ".bunfig.toml",
4482
+ "bunfig.toml",
4483
+ ".bashrc",
4484
+ ".bash_profile",
4485
+ ".zshrc",
4486
+ ".zprofile",
4487
+ ".profile"
4488
+ ];
4489
+ TOKEN_VALUE_PATTERNS = [
4490
+ { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
4491
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
4492
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
4493
+ { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
4494
+ { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
4495
+ { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
4496
+ ];
4497
+ });
4498
+
4499
+ // node_modules/@hasna/events/dist/commander.js
4500
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
4501
+ import { existsSync } from "fs";
4502
+ import { homedir } from "os";
4503
+ import { join } from "path";
4504
+ import { createHmac, timingSafeEqual } from "crypto";
4505
+ import { randomUUID } from "crypto";
4506
+ import { spawn } from "child_process";
4507
+ import { randomUUID as randomUUID2 } from "crypto";
4508
+ function getPathValue(input, path) {
4509
+ return path.split(".").reduce((value, part) => {
4510
+ if (value && typeof value === "object" && part in value) {
4511
+ return value[part];
4512
+ }
4513
+ return;
4514
+ }, input);
4515
+ }
4516
+ function wildcardToRegExp(pattern) {
4517
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, "\\$&").replace(/\*/g, ".*");
4518
+ return new RegExp(`^${escaped}$`);
4519
+ }
4520
+ function matchString(value, matcher) {
3632
4521
  if (matcher === undefined)
3633
4522
  return true;
3634
4523
  if (value === undefined)
3635
4524
  return false;
3636
4525
  const matchers = Array.isArray(matcher) ? matcher : [matcher];
3637
- return matchers.some((item) => wildcardToRegExp(item, options).test(value));
4526
+ return matchers.some((item) => wildcardToRegExp(item).test(value));
3638
4527
  }
3639
4528
  function matchRecord(input, matcher) {
3640
4529
  if (!matcher)
3641
4530
  return true;
3642
4531
  return Object.entries(matcher).every(([path, expected]) => {
3643
- const actualValues = getFieldValues(input, path);
3644
- return matchField(actualValues, expected, path);
4532
+ const actual = getPathValue(input, path);
4533
+ if (typeof expected === "string" || Array.isArray(expected)) {
4534
+ return matchString(actual === undefined ? undefined : String(actual), expected);
4535
+ }
4536
+ return actual === expected;
3645
4537
  });
3646
4538
  }
3647
- function matchField(actualValues, expected, path) {
3648
- if (isNegativeMatcher(expected)) {
3649
- return !actualValues.some((actual) => matchPositiveField(actual, expected.not, path));
3650
- }
3651
- return actualValues.some((actual) => matchPositiveField(actual, expected, path));
3652
- }
3653
- function matchPositiveField(actual, expected, path) {
3654
- if (typeof expected === "string" || Array.isArray(expected)) {
3655
- return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
3656
- segmentSafe: path.endsWith("_path") || path.endsWith(".path")
3657
- }));
3658
- }
3659
- if (Array.isArray(actual)) {
3660
- return actual.some((item) => item === expected);
3661
- }
3662
- return actual === expected;
3663
- }
3664
- function stringCandidates(actual) {
3665
- if (actual === undefined)
3666
- return [];
3667
- if (Array.isArray(actual)) {
3668
- return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
3669
- }
3670
- return [String(actual)];
3671
- }
3672
- function isPrimitiveFieldValue(value) {
3673
- return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
3674
- }
3675
- function isNegativeMatcher(value) {
3676
- return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
3677
- }
3678
4539
  function eventMatchesFilter(event, filter) {
3679
4540
  return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
3680
4541
  }
@@ -3690,14 +4551,6 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
3690
4551
  function getEventsDataDir(override) {
3691
4552
  return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join(homedir(), ".hasna", "events");
3692
4553
  }
3693
- function getActiveEventsDirEnv() {
3694
- if (process.env[HASNA_EVENTS_DIR_ENV])
3695
- return HASNA_EVENTS_DIR_ENV;
3696
- if (process.env[HASNA_EVENTS_HOME_ENV])
3697
- return HASNA_EVENTS_HOME_ENV;
3698
- return null;
3699
- }
3700
-
3701
4554
  class JsonEventsStore {
3702
4555
  dataDir;
3703
4556
  channelsPath;
@@ -3809,52 +4662,6 @@ class JsonEventsStore {
3809
4662
  });
3810
4663
  }
3811
4664
  }
3812
- async function getEventsStatus(dataDir) {
3813
- const store = new JsonEventsStore(dataDir);
3814
- await store.init();
3815
- const [channels, events, deliveries] = await Promise.all([
3816
- store.listChannels(),
3817
- store.listEvents(),
3818
- store.listDeliveries()
3819
- ]);
3820
- const transports = channels.reduce((counts, channel) => {
3821
- counts[channel.transport] = (counts[channel.transport] ?? 0) + 1;
3822
- return counts;
3823
- }, {});
3824
- return {
3825
- service: "events",
3826
- schemaVersion: "1.0",
3827
- dataDir: store.dataDir,
3828
- env: {
3829
- primary: HASNA_EVENTS_DIR_ENV,
3830
- fallback: HASNA_EVENTS_HOME_ENV,
3831
- active: getActiveEventsDirEnv()
3832
- },
3833
- files: {
3834
- channels: statusFile(store.dataDir, "channels.json", channels.length),
3835
- events: statusFile(store.dataDir, "events.json", events.length),
3836
- deliveries: statusFile(store.dataDir, "deliveries.json", deliveries.length)
3837
- },
3838
- counts: {
3839
- channels: channels.length,
3840
- enabledChannels: channels.filter((channel) => channel.enabled).length,
3841
- disabledChannels: channels.filter((channel) => !channel.enabled).length,
3842
- events: events.length,
3843
- deliveries: deliveries.length
3844
- },
3845
- transports,
3846
- safety: {
3847
- includesEventPayloads: false,
3848
- includesWebhookSecrets: false,
3849
- listOutputsRedactSecrets: true,
3850
- statusOutputIsMetadataOnly: true
3851
- }
3852
- };
3853
- }
3854
- function statusFile(dataDir, fileName, records) {
3855
- const path = join(dataDir, fileName);
3856
- return { path, exists: existsSync(path), records };
3857
- }
3858
4665
  var DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
3859
4666
  function buildSignatureBase(timestamp, body) {
3860
4667
  return `${timestamp}.${body}`;
@@ -4080,7 +4887,7 @@ class EventsClient {
4080
4887
  }
4081
4888
  return deliveries;
4082
4889
  }
4083
- async matchChannel(id, input = {}) {
4890
+ async testChannel(id, input = {}) {
4084
4891
  const channel = await this.store.getChannel(id);
4085
4892
  if (!channel)
4086
4893
  throw new Error(`Channel not found: ${id}`);
@@ -4097,34 +4904,6 @@ class EventsClient {
4097
4904
  time: input.time,
4098
4905
  id: input.id
4099
4906
  });
4100
- const matched = channelMatchesEvent(channel, event);
4101
- return {
4102
- channelId: channel.id,
4103
- matched,
4104
- event,
4105
- filters: channel.filters,
4106
- reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
4107
- };
4108
- }
4109
- async testChannel(id, input = {}, options = {}) {
4110
- const channel = await this.store.getChannel(id);
4111
- if (!channel)
4112
- throw new Error(`Channel not found: ${id}`);
4113
- const match = await this.matchChannel(id, input);
4114
- const event = match.event;
4115
- if (options.honorFilters && !match.matched) {
4116
- const timestamp = new Date().toISOString();
4117
- const result2 = createDeliveryResult(event, channel, [{
4118
- attempt: 1,
4119
- status: "skipped",
4120
- startedAt: timestamp,
4121
- completedAt: timestamp,
4122
- error: match.reason
4123
- }]);
4124
- result2.metadata = { reason: "filter_mismatch" };
4125
- await this.store.appendDelivery(result2);
4126
- return result2;
4127
- }
4128
4907
  const eventForChannel = await this.applyRedaction(event, channel);
4129
4908
  const result = await this.deliverWithRetry(eventForChannel, channel);
4130
4909
  await this.store.appendDelivery(result);
@@ -4235,76 +5014,6 @@ function normalizeRetryPolicy(policy) {
4235
5014
  multiplier: Math.max(1, policy?.multiplier ?? 2)
4236
5015
  };
4237
5016
  }
4238
- function parseFieldMatchers(values, label, typed = false) {
4239
- if (!values?.length)
4240
- return;
4241
- const result = {};
4242
- for (const value of values) {
4243
- const parsed = parseMatcherExpression(value, label);
4244
- const path = parsed.path;
4245
- if (path in result)
4246
- throw new Error(`Duplicate ${label} filter path: ${path}`);
4247
- const matcherValue = typed ? parseTypedMatcherValue(parsed.rawValue, label) : parsed.rawValue;
4248
- result[path] = parsed.negated ? { not: matcherValue } : matcherValue;
4249
- }
4250
- return result;
4251
- }
4252
- function parseFilterOptions(options) {
4253
- const filter2 = {};
4254
- if (options.source)
4255
- filter2.source = options.source;
4256
- if (options.type)
4257
- filter2.type = options.type;
4258
- if (options.subject)
4259
- filter2.subject = options.subject;
4260
- if (options.severity)
4261
- filter2.severity = options.severity;
4262
- const data = mergeMatchers(parseFieldMatchers(options.data, "data"), parseFieldMatchers(options.dataJson, "data-json", true));
4263
- const metadata = mergeMatchers(parseFieldMatchers(options.metadata, "metadata"), parseFieldMatchers(options.metadataJson, "metadata-json", true));
4264
- if (Object.keys(data).length > 0)
4265
- filter2.data = data;
4266
- if (Object.keys(metadata).length > 0)
4267
- filter2.metadata = metadata;
4268
- return Object.keys(filter2).length > 0 ? [filter2] : undefined;
4269
- }
4270
- function mergeMatchers(...records) {
4271
- const result = {};
4272
- for (const record of records) {
4273
- if (!record)
4274
- continue;
4275
- for (const [path, value] of Object.entries(record)) {
4276
- if (path in result)
4277
- throw new Error(`Duplicate filter path: ${path}`);
4278
- result[path] = value;
4279
- }
4280
- }
4281
- return result;
4282
- }
4283
- function parseTypedMatcherValue(value, label) {
4284
- const parsed = JSON.parse(value);
4285
- if (parsed === null || typeof parsed === "string" || typeof parsed === "number" || typeof parsed === "boolean" || Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
4286
- return parsed;
4287
- }
4288
- throw new Error(`${label} filter JSON values must be string, string[], number, boolean, or null`);
4289
- }
4290
- function parseMatcherExpression(value, label) {
4291
- const negativeSeparator = value.indexOf("!=");
4292
- if (negativeSeparator > 0) {
4293
- return {
4294
- path: value.slice(0, negativeSeparator),
4295
- rawValue: value.slice(negativeSeparator + 2),
4296
- negated: true
4297
- };
4298
- }
4299
- const separator = value.indexOf("=");
4300
- if (separator <= 0)
4301
- throw new Error(`Invalid ${label} filter, expected path=value or path!=value: ${value}`);
4302
- return {
4303
- path: value.slice(0, separator),
4304
- rawValue: value.slice(separator + 1),
4305
- negated: false
4306
- };
4307
- }
4308
5017
  function parseJsonObject(value, fallback) {
4309
5018
  if (!value)
4310
5019
  return fallback;
@@ -4326,6 +5035,18 @@ function parseHeaders(values) {
4326
5035
  }
4327
5036
  return headers;
4328
5037
  }
5038
+ function parseFilter(options) {
5039
+ const filter2 = {};
5040
+ if (options.source)
5041
+ filter2.source = options.source;
5042
+ if (options.type)
5043
+ filter2.type = options.type;
5044
+ if (options.subject)
5045
+ filter2.subject = options.subject;
5046
+ if (options.severity)
5047
+ filter2.severity = options.severity;
5048
+ return Object.keys(filter2).length > 0 ? [filter2] : undefined;
5049
+ }
4329
5050
  function createClient(options) {
4330
5051
  if (options.createClient)
4331
5052
  return options.createClient();
@@ -4343,16 +5064,16 @@ function hasJsonOption(options) {
4343
5064
  function wantsJson(actionOptions, command) {
4344
5065
  return hasJsonOption(actionOptions) || hasJsonOption(command);
4345
5066
  }
4346
- function registerChannelCommands(program, options) {
4347
- const channels = program.command(options.channelsCommandName ?? "channels").description("Manage Hasna event channels");
4348
- channels.command("add").description("Add or replace a channel").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--data <path=value...>", "Event data field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--metadata <path=value...>", "Event metadata field filter; string values, path!=value negatives, array-member matching, dot paths, * segment wildcard, ** recursive wildcard", collectValues, []).option("--data-json <path=json...>", "Event data field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--metadata-json <path=json...>", "Event metadata field filter with typed JSON value; path!=json negatives supported", collectValues, []).option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
5067
+ function registerWebhookCommands(program, options) {
5068
+ const webhooks = program.command(options.webhooksCommandName ?? "webhooks").description("Manage Hasna event webhook subscriptions");
5069
+ webhooks.command("add").description("Add or replace a webhook or command subscription").argument("<target>", "Webhook URL or command binary").requiredOption("--id <id>", "Subscription/channel identifier").option("--transport <kind>", "Transport kind: webhook or command", "webhook").option("--name <name>", "Display name").option("--type <pattern>", "Event type filter, e.g. todos.task.*").option("--source <pattern>", "Event source filter").option("--subject <pattern>", "Event subject filter").option("--severity <pattern>", "Event severity filter").option("--secret <secret>", "Webhook HMAC secret").option("--header <name=value...>", "Webhook header", collectValues, []).option("--arg <arg...>", "Command argument", collectValues, []).option("--timeout-ms <ms>", "Transport timeout in milliseconds", parseNumber).option("--retry-attempts <n>", "Maximum delivery attempts", parseNumber).option("--retry-backoff-ms <ms>", "Initial retry backoff in milliseconds", parseNumber).option("--redact <path...>", "Event field path to redact before delivery", collectValues, []).option("--disabled", "Create channel disabled", false).option("-j, --json", "Print JSON output", false).action(async (target, actionOptions, command) => {
4349
5070
  const timestamp = new Date().toISOString();
4350
5071
  const channel = {
4351
5072
  id: actionOptions.id,
4352
5073
  name: actionOptions.name,
4353
5074
  enabled: !actionOptions.disabled,
4354
5075
  transport: actionOptions.transport,
4355
- filters: parseFilterOptions(actionOptions),
5076
+ filters: parseFilter(actionOptions),
4356
5077
  retry: actionOptions.retryAttempts || actionOptions.retryBackoffMs ? { maxAttempts: actionOptions.retryAttempts, backoffMs: actionOptions.retryBackoffMs } : undefined,
4357
5078
  redact: actionOptions.redact?.length ? { paths: actionOptions.redact } : undefined,
4358
5079
  createdAt: timestamp,
@@ -4368,51 +5089,35 @@ function registerChannelCommands(program, options) {
4368
5089
  const saved = await createClient(options).addChannel(channel);
4369
5090
  print(sanitizeChannelForOutput(saved), wantsJson(actionOptions, command), `Added ${saved.transport} channel ${saved.id}`);
4370
5091
  });
4371
- channels.command("list").description("List configured channels").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
4372
- const channels2 = await createClient(options).listChannels();
5092
+ webhooks.command("list").description("List configured subscriptions").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
5093
+ const channels = await createClient(options).listChannels();
4373
5094
  if (wantsJson(actionOptions, command)) {
4374
- console.log(JSON.stringify(sanitizeChannelsForOutput(channels2), null, 2));
5095
+ console.log(JSON.stringify(sanitizeChannelsForOutput(channels), null, 2));
4375
5096
  return;
4376
5097
  }
4377
- if (!channels2.length) {
5098
+ if (!channels.length) {
4378
5099
  console.log("No channels configured.");
4379
5100
  return;
4380
5101
  }
4381
- for (const channel of channels2) {
5102
+ for (const channel of channels) {
4382
5103
  console.log(`${channel.id} ${channel.enabled ? "enabled" : "disabled"} ${channel.transport} ${channel.webhook?.url ?? channel.command?.command ?? channel.transport}`);
4383
5104
  }
4384
5105
  });
4385
- channels.command("status").description("Show events channel storage status").option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
4386
- const status = await getEventsStatus(options.dataDir);
4387
- print(status, wantsJson(actionOptions, command), `events dataDir: ${status.dataDir}`);
4388
- });
4389
- channels.command("remove").description("Remove a channel").argument("<id>", "Channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
5106
+ webhooks.command("remove").description("Remove a subscription").argument("<id>", "Subscription/channel identifier").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
4390
5107
  const removed = await createClient(options).removeChannel(id);
4391
5108
  print({ removed }, wantsJson(actionOptions, command), removed ? `Removed ${id}` : `Channel not found: ${id}`);
4392
5109
  });
4393
- channels.command("test").description("Send a test event to one channel").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("--honor-filters", "Skip delivery when the sample event does not match channel filters", false).option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
5110
+ webhooks.command("test").description("Send a test event to one subscription").argument("<id>", "Subscription/channel identifier").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events test delivery").option("--data <json>", "Event data JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
4394
5111
  const result = await createClient(options).testChannel(id, {
4395
- source: actionOptions.source ?? options.source,
4396
- type: actionOptions.type,
4397
- subject: actionOptions.subject ?? id,
4398
- message: actionOptions.message,
4399
- data: parseJsonObject(actionOptions.data, { test: true }),
4400
- metadata: parseJsonObject(actionOptions.metadata, {})
4401
- }, { honorFilters: actionOptions.honorFilters });
4402
- print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
4403
- });
4404
- channels.command("match").description("Check whether a sample event matches one channel without delivering").argument("<id>", "Channel identifier").option("--source <source>", "Event source override").option("--type <type>", "Event type", "events.test").option("--subject <subject>", "Event subject").option("--message <message>", "Event message", "Hasna events match preview").option("--data <json>", "Event data JSON object").option("--metadata <json>", "Event metadata JSON object").option("-j, --json", "Print JSON output", false).action(async (id, actionOptions, command) => {
4405
- const result = await createClient(options).matchChannel(id, {
4406
- source: actionOptions.source ?? options.source,
5112
+ source: options.source,
4407
5113
  type: actionOptions.type,
4408
5114
  subject: actionOptions.subject ?? id,
4409
5115
  message: actionOptions.message,
4410
- data: parseJsonObject(actionOptions.data, { test: true }),
4411
- metadata: parseJsonObject(actionOptions.metadata, {})
5116
+ data: parseJsonObject(actionOptions.data, { test: true })
4412
5117
  });
4413
- print(result, wantsJson(actionOptions, command), `${result.matched ? "matched" : "skipped"}: ${result.channelId}`);
5118
+ print(result, wantsJson(actionOptions, command), `${result.status}: ${result.channelId}`);
4414
5119
  });
4415
- return channels;
5120
+ return webhooks;
4416
5121
  }
4417
5122
  function registerEventCommands(program, options) {
4418
5123
  const events = program.command(options.eventsCommandName ?? "events").description("Emit, list, and replay Hasna events");
@@ -4434,212 +5139,79 @@ function registerEventCommands(program, options) {
4434
5139
  if (actionOptions.source)
4435
5140
  rows = rows.filter((event) => event.source === actionOptions.source);
4436
5141
  if (actionOptions.type)
4437
- rows = rows.filter((event) => event.type === actionOptions.type);
4438
- if (actionOptions.limit)
4439
- rows = rows.slice(-actionOptions.limit);
4440
- if (wantsJson(actionOptions, command)) {
4441
- console.log(JSON.stringify(rows, null, 2));
4442
- return;
4443
- }
4444
- if (!rows.length) {
4445
- console.log("No events recorded.");
4446
- return;
4447
- }
4448
- for (const event of rows)
4449
- console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
4450
- });
4451
- events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
4452
- const result = await createClient(options).replay({
4453
- eventId: actionOptions.id,
4454
- source: actionOptions.source,
4455
- type: actionOptions.type,
4456
- dryRun: actionOptions.dryRun
4457
- });
4458
- print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
4459
- });
4460
- return events;
4461
- }
4462
- function registerEventsCommands(program, options) {
4463
- registerChannelCommands(program, options);
4464
- registerEventCommands(program, options);
4465
- }
4466
- function parseNumber(value) {
4467
- const parsed = Number(value);
4468
- if (!Number.isFinite(parsed))
4469
- throw new Error(`Expected a number, got ${value}`);
4470
- return parsed;
4471
- }
4472
- function collectValues(value, previous) {
4473
- previous.push(value);
4474
- return previous;
4475
- }
4476
-
4477
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
4478
- var import__ = __toESM(require_commander(), 1);
4479
- var {
4480
- program,
4481
- createCommand,
4482
- createArgument,
4483
- createOption,
4484
- CommanderError,
4485
- InvalidArgumentError,
4486
- InvalidOptionArgumentError,
4487
- Command,
4488
- Argument,
4489
- Option,
4490
- Help
4491
- } = import__.default;
4492
-
4493
- // src/cli/index.tsx
4494
- init_configs();
4495
- import chalk from "chalk";
4496
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
4497
- import { homedir as homedir6 } from "os";
4498
- import { basename as basename5, join as join10, resolve as resolve6 } from "path";
4499
-
4500
- // src/db/profiles.ts
4501
- init_types();
4502
- init_database();
4503
- init_configs();
4504
- init_machine();
4505
- function rowToProfile(row) {
4506
- return {
4507
- ...row,
4508
- selectors: JSON.parse(row.selectors || "{}"),
4509
- variables: JSON.parse(row.variables || "{}")
4510
- };
4511
- }
4512
- function uniqueProfileSlug(name, db, excludeId) {
4513
- const base = slugify(name);
4514
- let slug = base;
4515
- let i = 1;
4516
- while (true) {
4517
- const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
4518
- if (!existing || existing.id === excludeId)
4519
- return slug;
4520
- slug = `${base}-${i++}`;
4521
- }
4522
- }
4523
- function createProfile(input, db) {
4524
- const d = db || getDatabase();
4525
- const id = uuid();
4526
- const ts = now2();
4527
- const slug = uniqueProfileSlug(input.name, d);
4528
- d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
4529
- id,
4530
- input.name,
4531
- slug,
4532
- input.description ?? null,
4533
- JSON.stringify(input.selectors ?? {}),
4534
- JSON.stringify(input.variables ?? {}),
4535
- ts,
4536
- ts
4537
- ]);
4538
- return getProfile(id, d);
4539
- }
4540
- function getProfile(idOrSlug, db) {
4541
- const d = db || getDatabase();
4542
- const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
4543
- if (!row)
4544
- throw new ProfileNotFoundError(idOrSlug);
4545
- return rowToProfile(row);
4546
- }
4547
- function listProfiles(db) {
4548
- const d = db || getDatabase();
4549
- return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
4550
- }
4551
- function updateProfile(idOrSlug, input, db) {
4552
- const d = db || getDatabase();
4553
- const existing = getProfile(idOrSlug, d);
4554
- const ts = now2();
4555
- const updates = ["updated_at = ?"];
4556
- const params = [ts];
4557
- if (input.name !== undefined) {
4558
- updates.push("name = ?", "slug = ?");
4559
- params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
4560
- }
4561
- if (input.description !== undefined) {
4562
- updates.push("description = ?");
4563
- params.push(input.description);
4564
- }
4565
- if (input.selectors !== undefined) {
4566
- updates.push("selectors = ?");
4567
- params.push(JSON.stringify(input.selectors));
4568
- }
4569
- if (input.variables !== undefined) {
4570
- updates.push("variables = ?");
4571
- params.push(JSON.stringify(input.variables));
4572
- }
4573
- params.push(existing.id);
4574
- d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
4575
- return getProfile(existing.id, d);
4576
- }
4577
- function deleteProfile(idOrSlug, db) {
4578
- const d = db || getDatabase();
4579
- const existing = getProfile(idOrSlug, d);
4580
- d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
4581
- }
4582
- function addConfigToProfile(profileIdOrSlug, configId, db) {
4583
- const d = db || getDatabase();
4584
- const profile = getProfile(profileIdOrSlug, d);
4585
- const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
4586
- const order = (maxRow?.max_order ?? -1) + 1;
4587
- d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
4588
- }
4589
- function removeConfigFromProfile(profileIdOrSlug, configId, db) {
4590
- const d = db || getDatabase();
4591
- const profile = getProfile(profileIdOrSlug, d);
4592
- d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
4593
- }
4594
- function getProfileConfigs(profileIdOrSlug, db) {
4595
- const d = db || getDatabase();
4596
- const profile = getProfile(profileIdOrSlug, d);
4597
- const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
4598
- if (rows.length === 0)
4599
- return [];
4600
- const ids = rows.map((r) => r.config_id);
4601
- return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
5142
+ rows = rows.filter((event) => event.type === actionOptions.type);
5143
+ if (actionOptions.limit)
5144
+ rows = rows.slice(-actionOptions.limit);
5145
+ if (wantsJson(actionOptions, command)) {
5146
+ console.log(JSON.stringify(rows, null, 2));
5147
+ return;
5148
+ }
5149
+ if (!rows.length) {
5150
+ console.log("No events recorded.");
5151
+ return;
5152
+ }
5153
+ for (const event of rows)
5154
+ console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
5155
+ });
5156
+ events.command("replay").description("Replay recorded events").option("--id <id>", "Replay one event id").option("--source <source>", "Filter by source").option("--type <type>", "Filter by type").option("--dry-run", "Preview without delivery", false).option("-j, --json", "Print JSON output", false).action(async (actionOptions, command) => {
5157
+ const result = await createClient(options).replay({
5158
+ eventId: actionOptions.id,
5159
+ source: actionOptions.source,
5160
+ type: actionOptions.type,
5161
+ dryRun: actionOptions.dryRun
5162
+ });
5163
+ print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
5164
+ });
5165
+ return events;
4602
5166
  }
4603
- function profileHasSelectors(profile) {
4604
- const selectors = profile.selectors ?? {};
4605
- return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
5167
+ function registerEventsCommands(program, options) {
5168
+ registerWebhookCommands(program, options);
5169
+ registerEventCommands(program, options);
4606
5170
  }
4607
- function profileMatchesMachine(profile, machine) {
4608
- const selectors = profile.selectors ?? {};
4609
- const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
4610
- const value = candidate.trim().toLowerCase();
4611
- return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
4612
- });
4613
- const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
4614
- const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
4615
- return osMatches && archMatches && hostnameMatches;
5171
+ function parseNumber(value) {
5172
+ const parsed = Number(value);
5173
+ if (!Number.isFinite(parsed))
5174
+ throw new Error(`Expected a number, got ${value}`);
5175
+ return parsed;
4616
5176
  }
4617
- function resolveProfileForMachine(machine = detectMachineContext(), db) {
4618
- const profiles = listProfiles(db).filter(profileHasSelectors);
4619
- const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
4620
- const selectors = profile.selectors;
4621
- const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
4622
- return { profile, score };
4623
- }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
4624
- return matches[0]?.profile ?? null;
5177
+ function collectValues(value, previous) {
5178
+ previous.push(value);
5179
+ return previous;
4625
5180
  }
4626
5181
 
5182
+ // node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
5183
+ var import__ = __toESM(require_commander(), 1);
5184
+ var {
5185
+ program,
5186
+ createCommand,
5187
+ createArgument,
5188
+ createOption,
5189
+ CommanderError,
5190
+ InvalidArgumentError,
5191
+ InvalidOptionArgumentError,
5192
+ Command,
5193
+ Argument,
5194
+ Option,
5195
+ Help
5196
+ } = import__.default;
5197
+
4627
5198
  // src/cli/index.tsx
4628
- init_snapshots();
4629
- init_database();
4630
5199
  init_apply();
4631
5200
  init_sync();
4632
5201
  init_redact();
5202
+ import chalk from "chalk";
5203
+ import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
5204
+ import { homedir as homedir7 } from "os";
5205
+ import { basename as basename6, join as join11, resolve as resolve7 } from "path";
4633
5206
 
4634
5207
  // src/lib/export.ts
4635
- init_database();
4636
- init_configs();
4637
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
5208
+ init_config_store();
5209
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4638
5210
  import { join as join6, resolve as resolve2 } from "path";
4639
5211
  import { tmpdir } from "os";
4640
5212
  async function exportConfigs(outputPath, opts = {}) {
4641
- const d = opts.db || getDatabase();
4642
- const configs = listConfigs(opts.filter, d);
5213
+ const store = opts.store ?? resolveConfigStore();
5214
+ const configs = await store.listConfigs(opts.filter);
4643
5215
  const absOutput = resolve2(outputPath);
4644
5216
  const tmpDir = join6(tmpdir(), `configs-export-${Date.now()}`);
4645
5217
  const contentsDir = join6(tmpDir, "contents");
@@ -4647,7 +5219,7 @@ async function exportConfigs(outputPath, opts = {}) {
4647
5219
  mkdirSync3(contentsDir, { recursive: true });
4648
5220
  const manifest = {
4649
5221
  version: "1.0.0",
4650
- exported_at: now2(),
5222
+ exported_at: new Date().toISOString(),
4651
5223
  configs: configs.map(({ content: _content, ...meta }) => meta)
4652
5224
  };
4653
5225
  writeFileSync2(join6(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
@@ -4667,19 +5239,18 @@ async function exportConfigs(outputPath, opts = {}) {
4667
5239
  return { path: absOutput, count: configs.length };
4668
5240
  } finally {
4669
5241
  if (existsSync7(tmpDir)) {
4670
- rmSync(tmpDir, { recursive: true, force: true });
5242
+ rmSync2(tmpDir, { recursive: true, force: true });
4671
5243
  }
4672
5244
  }
4673
5245
  }
4674
5246
 
4675
5247
  // src/lib/import.ts
4676
- init_database();
4677
- init_configs();
4678
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2 } from "fs";
5248
+ init_config_store();
5249
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync3 } from "fs";
4679
5250
  import { join as join7, resolve as resolve3 } from "path";
4680
5251
  import { tmpdir as tmpdir2 } from "os";
4681
5252
  async function importConfigs(bundlePath, opts = {}) {
4682
- const d = opts.db || getDatabase();
5253
+ const store = opts.store ?? resolveConfigStore();
4683
5254
  const conflict = opts.conflict ?? "skip";
4684
5255
  const absPath = resolve3(bundlePath);
4685
5256
  const tmpDir = join7(tmpdir2(), `configs-import-${Date.now()}`);
@@ -4706,17 +5277,17 @@ async function importConfigs(bundlePath, opts = {}) {
4706
5277
  const content = existsSync8(contentFile) ? readFileSync4(contentFile, "utf-8") : "";
4707
5278
  let existing = null;
4708
5279
  try {
4709
- existing = getConfig(meta.slug, d);
5280
+ existing = await store.getConfig(meta.slug);
4710
5281
  } catch {}
4711
5282
  if (existing) {
4712
5283
  if (conflict === "skip") {
4713
5284
  result.skipped++;
4714
5285
  } else if (conflict === "overwrite" || conflict === "version") {
4715
- updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs }, d);
5286
+ await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
4716
5287
  result.updated++;
4717
5288
  }
4718
5289
  } else {
4719
- createConfig({
5290
+ await store.createConfig({
4720
5291
  name: meta.name,
4721
5292
  kind: meta.kind,
4722
5293
  category: meta.category,
@@ -4728,7 +5299,7 @@ async function importConfigs(bundlePath, opts = {}) {
4728
5299
  description: meta.description ?? undefined,
4729
5300
  tags: meta.tags,
4730
5301
  is_template: meta.is_template
4731
- }, d);
5302
+ });
4732
5303
  result.created++;
4733
5304
  }
4734
5305
  } catch (err) {
@@ -4738,7 +5309,7 @@ async function importConfigs(bundlePath, opts = {}) {
4738
5309
  return result;
4739
5310
  } finally {
4740
5311
  if (existsSync8(tmpDir)) {
4741
- rmSync2(tmpDir, { recursive: true, force: true });
5312
+ rmSync3(tmpDir, { recursive: true, force: true });
4742
5313
  }
4743
5314
  }
4744
5315
  }
@@ -4748,14 +5319,14 @@ init_template();
4748
5319
  init_machine();
4749
5320
 
4750
5321
  // src/lib/session-apply.ts
4751
- import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
5322
+ import { createHash as createHash2, randomUUID as randomUUID6 } from "crypto";
4752
5323
  import {
4753
5324
  existsSync as existsSync10,
4754
5325
  lstatSync,
4755
5326
  mkdirSync as mkdirSync5,
4756
5327
  readFileSync as readFileSync6,
4757
5328
  renameSync,
4758
- rmSync as rmSync3,
5329
+ rmSync as rmSync4,
4759
5330
  writeFileSync as writeFileSync3
4760
5331
  } from "fs";
4761
5332
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join9, parse as parse2, relative as relative3, resolve as resolve5 } from "path";
@@ -5688,7 +6259,7 @@ function applySessionRender(plan, options = {}) {
5688
6259
  continue;
5689
6260
  assertNoSymlinkSegments(targetHome, result.path);
5690
6261
  if (existsSync10(result.path))
5691
- rmSync3(result.path);
6262
+ rmSync4(result.path);
5692
6263
  }
5693
6264
  }
5694
6265
  return {
@@ -5922,7 +6493,7 @@ function writePlannedFile(path, content, targetHome) {
5922
6493
  const dir = dirname3(path);
5923
6494
  mkdirSync5(dir, { recursive: true });
5924
6495
  assertNoSymlinkSegments(targetHome, path);
5925
- const tmp = join9(dir, `.session-${randomUUID5()}.tmp`);
6496
+ const tmp = join9(dir, `.session-${randomUUID6()}.tmp`);
5926
6497
  writeFileSync3(tmp, content, "utf-8");
5927
6498
  renameSync(tmp, path);
5928
6499
  }
@@ -5940,7 +6511,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
5940
6511
  if (!previousManifest && existingFiles.length === 0)
5941
6512
  return null;
5942
6513
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
5943
- const snapshotPath = resolve5(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID5()}.json`);
6514
+ const snapshotPath = resolve5(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID6()}.json`);
5944
6515
  const snapshot = {
5945
6516
  schema: "hasna.configs.session-render-snapshot/v1",
5946
6517
  createdAt: new Date().toISOString(),
@@ -5998,10 +6569,10 @@ function sha2562(content) {
5998
6569
  }
5999
6570
 
6000
6571
  // src/lib/platform-profiles.ts
6001
- init_configs();
6572
+ init_config_store();
6002
6573
 
6003
6574
  // src/lib/project-dashboard-standard.ts
6004
- init_configs();
6575
+ init_config_store();
6005
6576
  var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
6006
6577
  var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
6007
6578
  PROJECT_DASHBOARD_DIR: ".hasna/project",
@@ -6079,7 +6650,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
6079
6650
  ids, passport numbers, credentials, and contract clauses unless an explicit
6080
6651
  approved storage policy exists.
6081
6652
  `;
6082
- function ensureProjectDashboardStandardConfig(db) {
6653
+ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
6083
6654
  const input = {
6084
6655
  name: "Agent Managed Project Dashboard Standard",
6085
6656
  category: "workspace",
@@ -6091,17 +6662,21 @@ function ensureProjectDashboardStandardConfig(db) {
6091
6662
  tags: ["projects-dashboard", "agent-projects", "json-render"]
6092
6663
  };
6093
6664
  try {
6094
- const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG, db);
6665
+ const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
6095
6666
  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) {
6096
- return updateConfig(existing.id, input, db);
6667
+ return await store.updateConfig(existing.id, input);
6097
6668
  }
6098
6669
  return existing;
6099
6670
  } catch {
6100
- return createConfig(input, db);
6671
+ return await store.createConfig(input);
6101
6672
  }
6102
6673
  }
6103
6674
 
6104
6675
  // src/lib/platform-profiles.ts
6676
+ function profileHasSelectors2(profile) {
6677
+ const selectors = profile.selectors ?? {};
6678
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
6679
+ }
6105
6680
  var PLATFORM_PROFILE_PRESETS = [
6106
6681
  {
6107
6682
  name: "linux-arm64",
@@ -6128,25 +6703,25 @@ var PLATFORM_PROFILE_PRESETS = [
6128
6703
  }
6129
6704
  }
6130
6705
  ];
6131
- function ensurePlatformProfiles(db) {
6132
- const configs = listConfigs(undefined, db);
6706
+ async function ensurePlatformProfiles(store = resolveConfigStore()) {
6707
+ const configs = await store.listConfigs();
6133
6708
  const ensured = [];
6134
6709
  for (const preset of PLATFORM_PROFILE_PRESETS) {
6135
6710
  let profile;
6136
6711
  try {
6137
- profile = getProfile(preset.name, db);
6138
- if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
6139
- profile = updateProfile(profile.id, {
6712
+ profile = await store.getProfile(preset.name);
6713
+ if (!profileHasSelectors2(profile) || Object.keys(profile.variables).length === 0) {
6714
+ profile = await store.updateProfile(profile.id, {
6140
6715
  description: profile.description ?? preset.description,
6141
- selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
6716
+ selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
6142
6717
  variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
6143
- }, db);
6718
+ });
6144
6719
  }
6145
6720
  } catch {
6146
- profile = createProfile(preset, db);
6721
+ profile = await store.createProfile(preset);
6147
6722
  }
6148
6723
  for (const config of configs) {
6149
- addConfigToProfile(profile.id, config.id, db);
6724
+ await store.addConfigToProfile(profile.id, config.id);
6150
6725
  }
6151
6726
  ensured.push(profile);
6152
6727
  }
@@ -6154,20 +6729,10 @@ function ensurePlatformProfiles(db) {
6154
6729
  }
6155
6730
 
6156
6731
  // src/status.ts
6157
- init_database();
6158
- init_configs();
6159
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
6160
-
6161
- // src/db/machines.ts
6162
- init_database();
6163
- function listMachines(db) {
6164
- const d = db || getDatabase();
6165
- return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
6166
- }
6167
-
6168
- // src/status.ts
6732
+ init_config_store();
6169
6733
  init_apply();
6170
6734
  init_redact();
6735
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
6171
6736
  var PACKAGE_NAME = "@hasna/instructions";
6172
6737
  var PACKAGE_VERSION = "0.3.0";
6173
6738
  function activeDatabaseEnv() {
@@ -6191,21 +6756,13 @@ function countBy(items, getValue) {
6191
6756
  }
6192
6757
  return counts;
6193
6758
  }
6194
- function tableCount(db, table) {
6195
- try {
6196
- const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
6197
- return Number(row?.count ?? 0);
6198
- } catch {
6199
- return 0;
6200
- }
6201
- }
6202
- function getConfigsStatus(db = getDatabase()) {
6759
+ async function getConfigsStatus(store = resolveConfigStore()) {
6203
6760
  let databaseReachable = true;
6204
6761
  let configs = [];
6205
6762
  let categoryStats = { total: 0 };
6206
6763
  try {
6207
- configs = listConfigs(undefined, db);
6208
- categoryStats = getConfigStats(db);
6764
+ configs = await store.listConfigs();
6765
+ categoryStats = await store.getConfigStats();
6209
6766
  } catch {
6210
6767
  databaseReachable = false;
6211
6768
  }
@@ -6230,10 +6787,25 @@ function getConfigsStatus(db = getDatabase()) {
6230
6787
  driftedTargets += 1;
6231
6788
  }
6232
6789
  }
6233
- const profiles = databaseReachable ? listProfiles(db).length : 0;
6234
- const machines = databaseReachable ? listMachines(db).length : 0;
6235
- const profileLinks = databaseReachable ? tableCount(db, "profile_configs") : 0;
6236
- const snapshots = databaseReachable ? tableCount(db, "config_snapshots") : 0;
6790
+ let profiles = 0;
6791
+ let machines = 0;
6792
+ let profileLinks = 0;
6793
+ let snapshots = 0;
6794
+ if (databaseReachable) {
6795
+ try {
6796
+ const profileList = await store.listProfiles();
6797
+ profiles = profileList.length;
6798
+ machines = (await store.listMachines()).length;
6799
+ for (const profile of profileList) {
6800
+ profileLinks += (await store.getProfileConfigs(profile.id)).length;
6801
+ }
6802
+ for (const config of configs) {
6803
+ snapshots += (await store.listSnapshots(config.id)).length;
6804
+ }
6805
+ } catch {
6806
+ databaseReachable = false;
6807
+ }
6808
+ }
6237
6809
  const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
6238
6810
  const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
6239
6811
  return {
@@ -6287,425 +6859,8 @@ function getConfigsStatus(db = getDatabase()) {
6287
6859
  };
6288
6860
  }
6289
6861
 
6290
- // src/db/storage-sync.ts
6291
- init_database();
6292
-
6293
- // src/db/pg-migrations.ts
6294
- var PG_MIGRATIONS = [
6295
- `CREATE TABLE IF NOT EXISTS configs (
6296
- id TEXT PRIMARY KEY,
6297
- name TEXT NOT NULL,
6298
- slug TEXT NOT NULL UNIQUE,
6299
- kind TEXT NOT NULL DEFAULT 'file',
6300
- category TEXT NOT NULL,
6301
- agent TEXT NOT NULL DEFAULT 'global',
6302
- target_path TEXT,
6303
- outputs TEXT NOT NULL DEFAULT '[]',
6304
- format TEXT NOT NULL DEFAULT 'text',
6305
- content TEXT NOT NULL DEFAULT '',
6306
- description TEXT,
6307
- tags TEXT NOT NULL DEFAULT '[]',
6308
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
6309
- version INTEGER NOT NULL DEFAULT 1,
6310
- created_at TEXT NOT NULL,
6311
- updated_at TEXT NOT NULL,
6312
- synced_at TEXT
6313
- )`,
6314
- `CREATE TABLE IF NOT EXISTS config_snapshots (
6315
- id TEXT PRIMARY KEY,
6316
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
6317
- content TEXT NOT NULL,
6318
- version INTEGER NOT NULL,
6319
- created_at TEXT NOT NULL
6320
- )`,
6321
- `CREATE TABLE IF NOT EXISTS profiles (
6322
- id TEXT PRIMARY KEY,
6323
- name TEXT NOT NULL,
6324
- slug TEXT NOT NULL UNIQUE,
6325
- description TEXT,
6326
- selectors TEXT NOT NULL DEFAULT '{}',
6327
- variables TEXT NOT NULL DEFAULT '{}',
6328
- created_at TEXT NOT NULL,
6329
- updated_at TEXT NOT NULL
6330
- )`,
6331
- `CREATE TABLE IF NOT EXISTS profile_configs (
6332
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
6333
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
6334
- sort_order INTEGER NOT NULL DEFAULT 0,
6335
- PRIMARY KEY (profile_id, config_id)
6336
- )`,
6337
- `CREATE TABLE IF NOT EXISTS machines (
6338
- id TEXT PRIMARY KEY,
6339
- hostname TEXT NOT NULL UNIQUE,
6340
- os TEXT,
6341
- arch TEXT,
6342
- last_applied_at TEXT,
6343
- created_at TEXT NOT NULL
6344
- )`,
6345
- `CREATE TABLE IF NOT EXISTS feedback (
6346
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
6347
- message TEXT NOT NULL,
6348
- email TEXT,
6349
- category TEXT DEFAULT 'general',
6350
- version TEXT,
6351
- machine_id TEXT,
6352
- created_at TEXT NOT NULL DEFAULT NOW()::text
6353
- )`,
6354
- `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
6355
- ];
6356
-
6357
- // src/db/remote-storage.ts
6358
- import pg from "pg";
6359
- var DISABLED_SSL_MODE = "disable";
6360
- function translatePlaceholders(sql) {
6361
- let index = 0;
6362
- return sql.replace(/\?/g, () => `$${++index}`);
6363
- }
6364
- function normalizeParams(params) {
6365
- const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
6366
- return flat.map((value) => value === undefined ? null : value);
6367
- }
6368
- function normalizeHost(hostname) {
6369
- const stripped = hostname.replace(/^\[/, "").replace(/\]$/, "");
6370
- try {
6371
- return decodeURIComponent(stripped).toLowerCase();
6372
- } catch {
6373
- return stripped.toLowerCase();
6374
- }
6375
- }
6376
- function isLocalPostgresHost(hostname) {
6377
- const host = normalizeHost(hostname);
6378
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
6379
- }
6380
- function effectivePgHost(url) {
6381
- const hosts = url.searchParams.getAll("host");
6382
- const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
6383
- return finalHost?.trim() ? finalHost : url.hostname;
6384
- }
6385
- function buildPgPoolConfig(connectionString) {
6386
- let url;
6387
- try {
6388
- url = new URL(connectionString);
6389
- } catch {
6390
- throw new Error("Invalid PostgreSQL connection string");
6391
- }
6392
- const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
6393
- const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
6394
- const isLocal = isLocalPostgresHost(effectivePgHost(url));
6395
- const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
6396
- if (!isLocal && hasDisabledSsl) {
6397
- throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
6398
- }
6399
- const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
6400
- url.searchParams.delete("sslmode");
6401
- url.searchParams.delete("ssl");
6402
- return {
6403
- connectionString: url.toString(),
6404
- ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
6405
- };
6406
- }
6407
-
6408
- class PgAdapterAsync {
6409
- pool;
6410
- constructor(connectionString) {
6411
- this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
6412
- }
6413
- async run(sql, ...params) {
6414
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6415
- return { changes: result.rowCount ?? 0 };
6416
- }
6417
- async all(sql, ...params) {
6418
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6419
- return result.rows;
6420
- }
6421
- async close() {
6422
- await this.pool.end();
6423
- }
6424
- }
6425
-
6426
- // src/db/storage-sync.ts
6427
- var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
6428
- var PRIMARY_KEYS = {
6429
- configs: ["id"],
6430
- config_snapshots: ["id"],
6431
- profiles: ["id"],
6432
- profile_configs: ["profile_id", "config_id"],
6433
- machines: ["id"],
6434
- feedback: ["id"]
6435
- };
6436
- var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
6437
- var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
6438
- var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
6439
- var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
6440
- var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
6441
- var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
6442
- function firstEnv(names) {
6443
- for (const name of names) {
6444
- const value = process.env[name];
6445
- if (value)
6446
- return value;
6447
- }
6448
- return null;
6449
- }
6450
- function normalizeStorageMode(value) {
6451
- const normalized = value?.trim().toLowerCase();
6452
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
6453
- return normalized;
6454
- return;
6455
- }
6456
- function getStorageDatabaseUrl() {
6457
- return firstEnv(STORAGE_DATABASE_ENV);
6458
- }
6459
- function getStorageMode() {
6460
- const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
6461
- if (mode)
6462
- return mode;
6463
- return getStorageDatabaseUrl() ? "hybrid" : "local";
6464
- }
6465
- async function getStoragePg() {
6466
- const url = getStorageDatabaseUrl();
6467
- if (!url)
6468
- throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
6469
- return new PgAdapterAsync(url);
6470
- }
6471
- async function runStorageMigrations(remote) {
6472
- await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
6473
- for (const sql of PG_MIGRATIONS)
6474
- await remote.run(sql);
6475
- }
6476
- async function storagePush(options) {
6477
- const remote = await getStoragePg();
6478
- const db = getDatabase();
6479
- try {
6480
- await runStorageMigrations(remote);
6481
- const results = [];
6482
- for (const table of resolveTables(options?.tables))
6483
- results.push(await pushTable(db, remote, table));
6484
- recordSyncMeta(db, "push", results);
6485
- return results;
6486
- } finally {
6487
- await remote.close();
6488
- }
6489
- }
6490
- async function storagePull(options) {
6491
- const remote = await getStoragePg();
6492
- const db = getDatabase();
6493
- try {
6494
- await runStorageMigrations(remote);
6495
- const results = [];
6496
- for (const table of resolveTables(options?.tables))
6497
- results.push(await pullTable(remote, db, table));
6498
- recordSyncMeta(db, "pull", results);
6499
- return results;
6500
- } finally {
6501
- await remote.close();
6502
- }
6503
- }
6504
- async function storageSync(options) {
6505
- const pull = await storagePull(options);
6506
- const push = await storagePush(options);
6507
- return { pull, push };
6508
- }
6509
- function getStorageSyncMetaAll() {
6510
- const db = getDatabase();
6511
- ensureSyncMetaTable(db);
6512
- return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
6513
- }
6514
- function getStorageStatus() {
6515
- return {
6516
- configured: Boolean(getStorageDatabaseUrl()),
6517
- mode: getStorageMode(),
6518
- env: STORAGE_DATABASE_ENV,
6519
- service: "configs",
6520
- tables: STORAGE_TABLES,
6521
- sync: getStorageSyncMetaAll()
6522
- };
6523
- }
6524
- function resolveTables(tables) {
6525
- if (!tables || tables.length === 0)
6526
- return [...STORAGE_TABLES];
6527
- const allowed = new Set(STORAGE_TABLES);
6528
- const requested = tables.map((table) => table.trim()).filter(Boolean);
6529
- const invalid = requested.filter((table) => !allowed.has(table));
6530
- if (invalid.length > 0)
6531
- throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
6532
- return requested;
6533
- }
6534
- async function pushTable(db, remote, table) {
6535
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6536
- try {
6537
- const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
6538
- result.rowsRead = rows.length;
6539
- if (rows.length === 0)
6540
- return result;
6541
- const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
6542
- result.rowsWritten = await upsertPg(remote, table, columns, rows);
6543
- } catch (error) {
6544
- result.errors.push(error instanceof Error ? error.message : String(error));
6545
- }
6546
- return result;
6547
- }
6548
- async function pullTable(remote, db, table) {
6549
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6550
- try {
6551
- const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
6552
- result.rowsRead = rows.length;
6553
- if (rows.length === 0)
6554
- return result;
6555
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6556
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
6557
- } catch (error) {
6558
- result.errors.push(error instanceof Error ? error.message : String(error));
6559
- }
6560
- return result;
6561
- }
6562
- async function filterRemoteColumns(remote, table, columns) {
6563
- const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
6564
- if (rows.length === 0)
6565
- return columns;
6566
- const allowed = new Set(rows.map((row) => row.column_name));
6567
- return columns.filter((column) => allowed.has(column));
6568
- }
6569
- function filterLocalColumns(db, table, columns) {
6570
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
6571
- const allowed = new Set(rows.map((row) => row.name));
6572
- return columns.filter((column) => allowed.has(column));
6573
- }
6574
- async function upsertPg(remote, table, columns, rows) {
6575
- if (columns.length === 0)
6576
- return 0;
6577
- const primaryKeys = PRIMARY_KEYS[table];
6578
- const columnList = columns.map(quoteIdent).join(", ");
6579
- const placeholders = columns.map(() => "?").join(", ");
6580
- const keyList = primaryKeys.map(quoteIdent).join(", ");
6581
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6582
- const fallbackKey = primaryKeys[0];
6583
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
6584
- for (const row of rows) {
6585
- await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
6586
- }
6587
- return rows.length;
6588
- }
6589
- function upsertSqlite(db, table, columns, rows) {
6590
- if (columns.length === 0)
6591
- return 0;
6592
- const primaryKeys = PRIMARY_KEYS[table];
6593
- const columnList = columns.map(quoteIdent).join(", ");
6594
- const placeholders = columns.map(() => "?").join(", ");
6595
- const keyList = primaryKeys.map(quoteIdent).join(", ");
6596
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6597
- const fallbackKey = primaryKeys[0];
6598
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
6599
- const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
6600
- db.transaction((batch) => {
6601
- for (const row of batch)
6602
- statement.run(...columns.map((column) => coerceForSqlite(row[column])));
6603
- })(rows);
6604
- return rows.length;
6605
- }
6606
- function recordSyncMeta(db, direction, results) {
6607
- ensureSyncMetaTable(db);
6608
- const now3 = new Date().toISOString();
6609
- 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");
6610
- for (const result of results) {
6611
- if (result.errors.length > 0)
6612
- continue;
6613
- statement.run(result.table, now3, direction);
6614
- }
6615
- }
6616
- function ensureSyncMetaTable(db) {
6617
- 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))");
6618
- }
6619
- function quoteIdent(identifier) {
6620
- return `"${identifier.replace(/"/g, '""')}"`;
6621
- }
6622
- function coerceForSqlite(value) {
6623
- if (value === undefined || value === null)
6624
- return null;
6625
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
6626
- return value;
6627
- if (value instanceof Date)
6628
- return value.toISOString();
6629
- if (Buffer.isBuffer(value) || value instanceof Uint8Array)
6630
- return value;
6631
- if (typeof value === "object")
6632
- return JSON.stringify(value);
6633
- return String(value);
6634
- }
6635
-
6636
- // src/cli/storage.ts
6637
- function parseTables(value) {
6638
- if (!value)
6639
- return;
6640
- return value.split(",").map((table) => table.trim()).filter(Boolean);
6641
- }
6642
- function printJson(value) {
6643
- console.log(JSON.stringify(value, null, 2));
6644
- }
6645
- function printResults(results, label) {
6646
- const total = results.reduce((sum, result) => sum + result.rowsWritten, 0);
6647
- for (const result of results) {
6648
- const errors = result.errors.length > 0 ? ` (${result.errors.join("; ")})` : "";
6649
- console.log(` ${result.table}: ${result.rowsWritten}/${result.rowsRead} rows ${label}${errors}`);
6650
- }
6651
- console.log(`Done. ${total} rows ${label}.`);
6652
- }
6653
- function registerStorageCommands(program2) {
6654
- const storageCmd = program2.command("storage").description("Storage sync commands");
6655
- storageCmd.command("status").description("Show storage config and local sync state").option("--json", "Output as JSON").action((opts) => {
6656
- const info = getStorageStatus();
6657
- if (opts.json) {
6658
- printJson(info);
6659
- return;
6660
- }
6661
- console.log(`Storage configured: ${info.configured ? "yes" : "no"}`);
6662
- console.log(`Tables: ${info.tables.join(", ")}`);
6663
- if (info.sync.length === 0)
6664
- console.log("Sync: no local sync history");
6665
- for (const entry of info.sync)
6666
- console.log(` ${entry.table_name} ${entry.direction}: ${entry.last_synced_at ?? "never"}`);
6667
- });
6668
- storageCmd.command("push").description("Push local configs data to storage PostgreSQL").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
6669
- try {
6670
- const results = await storagePush({ tables: parseTables(opts.tables) });
6671
- if (opts.json) {
6672
- printJson(results);
6673
- return;
6674
- }
6675
- printResults(results, "pushed");
6676
- } catch (error) {
6677
- console.error(error instanceof Error ? error.message : String(error));
6678
- process.exit(1);
6679
- }
6680
- });
6681
- storageCmd.command("pull").description("Pull configs data from storage PostgreSQL to local SQLite").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
6682
- try {
6683
- const results = await storagePull({ tables: parseTables(opts.tables) });
6684
- if (opts.json) {
6685
- printJson(results);
6686
- return;
6687
- }
6688
- printResults(results, "pulled");
6689
- } catch (error) {
6690
- console.error(error instanceof Error ? error.message : String(error));
6691
- process.exit(1);
6692
- }
6693
- });
6694
- storageCmd.command("sync").description("Bidirectional sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
6695
- try {
6696
- const result = await storageSync({ tables: parseTables(opts.tables) });
6697
- if (opts.json) {
6698
- printJson(result);
6699
- return;
6700
- }
6701
- printResults(result.pull, "pulled");
6702
- printResults(result.push, "pushed");
6703
- } catch (error) {
6704
- console.error(error instanceof Error ? error.message : String(error));
6705
- process.exit(1);
6706
- }
6707
- });
6708
- }
6862
+ // src/cli/index.tsx
6863
+ init_config_store();
6709
6864
 
6710
6865
  // src/lib/compact-output.ts
6711
6866
  var DEFAULT_LIST_LIMIT = 20;
@@ -6784,9 +6939,9 @@ function pageFooter(command, page, detailsHint) {
6784
6939
  function printConfigRows(configs) {
6785
6940
  console.log(`${pad("slug", 32)} ${pad("type", 15)} ${pad("fmt", 8)} ${pad("path", 44)} out v`);
6786
6941
  for (const c of configs) {
6787
- const type = `${c.category}/${c.agent}`;
6942
+ const type2 = `${c.category}/${c.agent}`;
6788
6943
  const path = c.kind === "reference" ? "(ref)" : c.target_path ?? "(no path)";
6789
- console.log(`${pad(c.slug, 32)} ${pad(type, 15)} ${pad(c.format, 8)} ${pad(truncateMiddle(path, 44), 44)} ${String(c.outputs.length).padStart(3)} ${c.version}`);
6944
+ console.log(`${pad(c.slug, 32)} ${pad(type2, 15)} ${pad(c.format, 8)} ${pad(truncateMiddle(path, 44), 44)} ${String(c.outputs.length).padStart(3)} ${c.version}`);
6790
6945
  }
6791
6946
  }
6792
6947
  function splitCsv(value) {
@@ -6821,11 +6976,11 @@ function parseSessionSource(value, order, replaceIds) {
6821
6976
  if (!path)
6822
6977
  throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
6823
6978
  const absPath = resolveSessionPath(path);
6824
- if (!existsSync12(absPath))
6979
+ if (!existsSync13(absPath))
6825
6980
  throw new Error(`Instruction source file not found: ${absPath}`);
6826
- const content = readFileSync8(absPath, "utf-8");
6981
+ const content = readFileSync9(absPath, "utf-8");
6827
6982
  const source = sourceFromFilePath(absPath, content, order);
6828
- const resolvedId = id || source.id || basename5(absPath);
6983
+ const resolvedId = id || source.id || basename6(absPath);
6829
6984
  return {
6830
6985
  ...source,
6831
6986
  id: resolvedId,
@@ -6850,18 +7005,18 @@ function parseLayeredReference(value) {
6850
7005
  throw new Error("Instruction reference cannot be empty.");
6851
7006
  return { id: trimmed };
6852
7007
  }
6853
- function collectSessionSources(opts, tool) {
7008
+ async function collectSessionSources(opts, tool, store) {
6854
7009
  const replaceIds = new Set(opts.replaceSource ?? []);
6855
7010
  const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index, replaceIds));
6856
7011
  for (const value of opts.config ?? []) {
6857
7012
  const { layer, id } = parseLayeredReference(value);
6858
- sources.push(sourceFromConfig(getConfig(id), sources.length, layer));
7013
+ sources.push(sourceFromConfig(await store.getConfig(id), sources.length, layer));
6859
7014
  }
6860
7015
  for (const value of opts.identityExport ?? []) {
6861
7016
  const path = resolveSessionPath(value);
6862
- if (!existsSync12(path))
7017
+ if (!existsSync13(path))
6863
7018
  throw new Error(`Identity instruction export not found: ${path}`);
6864
- const parsed = JSON.parse(readFileSync8(path, "utf-8"));
7019
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
6865
7020
  sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
6866
7021
  }
6867
7022
  return sources.map((source) => replaceIds.has(source.id) ? { ...source, merge: "replace" } : source);
@@ -6893,12 +7048,12 @@ function parseVarArgs(values) {
6893
7048
  function parseProfileSelectors(opts) {
6894
7049
  const selectors = {};
6895
7050
  const os = splitCsv(opts.os);
6896
- const arch = splitCsv(opts.arch);
7051
+ const arch2 = splitCsv(opts.arch);
6897
7052
  const hostnames = splitCsv(opts.hostname);
6898
7053
  if (os)
6899
7054
  selectors.os = os;
6900
- if (arch)
6901
- selectors.arch = arch;
7055
+ if (arch2)
7056
+ selectors.arch = arch2;
6902
7057
  if (hostnames)
6903
7058
  selectors.hostnames = hostnames;
6904
7059
  return Object.keys(selectors).length > 0 ? selectors : undefined;
@@ -6916,14 +7071,14 @@ function formatProfileSelectorSummary(profile) {
6916
7071
  function formatProfileVariables(profile) {
6917
7072
  return Object.entries(profile.variables).map(([key, value]) => `${key}=${value}`).join(", ");
6918
7073
  }
6919
- function getMachineProfileContext(opts) {
7074
+ async function getMachineProfileContext(opts, store) {
6920
7075
  const machine = detectMachineContext({ hostname: opts.hostname, os: opts.os, arch: opts.arch });
6921
- const profile = resolveProfileForMachine(machine);
7076
+ const profile = await store.resolveProfileForMachine(machine);
6922
7077
  return { machine, profile, vars: resolveProfileVariables(profile, machine) };
6923
7078
  }
6924
7079
  program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: compact|table|json", "compact").option("--brief", "shorthand for --format compact").option("--verbose", "show expanded metadata for each listed config").option("--json", "output full matching records as JSON").option("--limit <n>", `max rows for human output (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor for human output").action(async (opts) => {
6925
7080
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6926
- const configs = listConfigs({
7081
+ const configs = await resolveConfigStore().listConfigs({
6927
7082
  category: opts.category,
6928
7083
  agent: opts.agent,
6929
7084
  kind: opts.kind,
@@ -6951,7 +7106,7 @@ program.command("list").alias("ls").description("List stored configs").option("-
6951
7106
  });
6952
7107
  program.command("show <id>").alias("inspect").description("Show a config's content and metadata").option("-f, --format <fmt>", "output format: table|json|content", "table").action(async (id, opts) => {
6953
7108
  try {
6954
- const c = getConfig(id);
7109
+ const c = await resolveConfigStore().getConfig(id);
6955
7110
  if (opts.format === "json") {
6956
7111
  console.log(JSON.stringify(c, null, 2));
6957
7112
  return;
@@ -6971,17 +7126,17 @@ program.command("show <id>").alias("inspect").description("Show a config's conte
6971
7126
  }
6972
7127
  });
6973
7128
  program.command("add <path>").description("Ingest a file into the config DB").option("-n, --name <name>", "config name (defaults to filename)").option("-c, --category <cat>", "category override").option("-a, --agent <agent>", "agent override").option("-k, --kind <kind>", "kind: file|reference", "file").option("--template", "mark as template (has {{VAR}} placeholders)").action(async (filePath, opts) => {
6974
- const abs = resolve6(filePath);
6975
- if (!existsSync12(abs)) {
7129
+ const abs = resolve7(filePath);
7130
+ if (!existsSync13(abs)) {
6976
7131
  console.error(chalk.red(`File not found: ${abs}`));
6977
7132
  process.exit(1);
6978
7133
  }
6979
- const rawContent = readFileSync8(abs, "utf-8");
7134
+ const rawContent = readFileSync9(abs, "utf-8");
6980
7135
  const fmt = detectFormat(abs);
6981
7136
  const { content, redacted, isTemplate: isTemplate2 } = redactContent(rawContent, fmt);
6982
- const targetPath = abs.startsWith(homedir6()) ? abs.replace(homedir6(), "~") : abs;
7137
+ const targetPath = abs.startsWith(homedir7()) ? abs.replace(homedir7(), "~") : abs;
6983
7138
  const name = opts.name || filePath.split("/").pop();
6984
- const config = createConfig({
7139
+ const config = await resolveConfigStore().createConfig({
6985
7140
  name,
6986
7141
  kind: opts.kind ?? "file",
6987
7142
  category: opts.category ?? detectCategory(abs),
@@ -6999,10 +7154,26 @@ program.command("add <path>").description("Ingest a file into the config DB").op
6999
7154
  console.log(chalk.dim(" Config stored as a template. Use `configs template vars` to see placeholders."));
7000
7155
  }
7001
7156
  });
7157
+ program.command("delete <id>").alias("rm").description("Delete a config record (by id or slug)").option("--json", "output result as JSON").action(async (id, opts) => {
7158
+ try {
7159
+ const store = resolveConfigStore();
7160
+ const config = await store.getConfig(id);
7161
+ await store.deleteConfig(config.id);
7162
+ if (opts.json) {
7163
+ console.log(JSON.stringify({ deleted: true, id: config.id, slug: config.slug }, null, 2));
7164
+ return;
7165
+ }
7166
+ console.log(chalk.green("\u2713") + ` Deleted: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
7167
+ } catch (e) {
7168
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
7169
+ process.exit(1);
7170
+ }
7171
+ });
7002
7172
  program.command("apply <id>").description("Apply a config to its target_path and output targets on disk").option("--dry-run", "preview without writing").option("--force", "overwrite even if unchanged").action(async (id, opts) => {
7003
7173
  try {
7004
- const config = getConfig(id);
7005
- const result = await applyConfig(config, { dryRun: opts.dryRun });
7174
+ const store = resolveConfigStore();
7175
+ const config = await store.getConfig(id);
7176
+ const result = await applyConfig(config, { dryRun: opts.dryRun, store });
7006
7177
  const status = opts.dryRun ? chalk.yellow("[dry-run]") : result.changed ? chalk.green("\u2713") : chalk.dim("=");
7007
7178
  const change = result.changed ? "changed" : "unchanged";
7008
7179
  console.log(`${status} ${result.path} ${chalk.dim(`(${change})`)}`);
@@ -7018,17 +7189,18 @@ program.command("apply <id>").description("Apply a config to its target_path and
7018
7189
  });
7019
7190
  program.command("diff [id]").description("Show diff between stored config and disk (omit id for --all)").option("--all", "diff every known config against disk").action(async (id, opts) => {
7020
7191
  try {
7192
+ const store = resolveConfigStore();
7021
7193
  if (id) {
7022
- const config = getConfig(id);
7023
- console.log(diffConfig(config));
7194
+ const config = await store.getConfig(id);
7195
+ console.log(await diffConfig(config, { store }));
7024
7196
  return;
7025
7197
  }
7026
- const configs = listConfigs({ kind: "file" });
7198
+ const configs = await store.listConfigs({ kind: "file" });
7027
7199
  let drifted = 0;
7028
7200
  for (const c of configs) {
7029
7201
  if (!c.target_path)
7030
7202
  continue;
7031
- const diff = diffConfig(c);
7203
+ const diff = await diffConfig(c, { store });
7032
7204
  if (diff.includes("no diff") || diff.includes("not found"))
7033
7205
  continue;
7034
7206
  drifted++;
@@ -7043,6 +7215,7 @@ program.command("diff [id]").description("Show diff between stored config and di
7043
7215
  }
7044
7216
  });
7045
7217
  program.command("sync").description("Sync known AI coding configs from disk into DB (claude, codex, opencode, cursor, codewith, aicopilot, gemini, zsh, git, npm)").option("-a, --agent <agent>", "only sync configs for this agent (claude|codex|opencode|cursor|codewith|aicopilot|gemini|zsh|git|npm)").option("-c, --category <cat>", "only sync configs in this category").option("-p, --project [dir]", "sync project-scoped configs (CLAUDE.md, .mcp.json, etc.) from a project dir").option("--all", "with --project: scan all subdirs for projects to sync").option("--to-disk", "apply DB configs back to disk instead").option("--dry-run", "preview without writing").option("--list", "show which files would be synced without doing anything").option("--limit <n>", `with --list, max rows (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "with --list, zero-based pagination cursor").action(async (opts) => {
7218
+ const store = resolveConfigStore();
7046
7219
  if (opts.list) {
7047
7220
  const targets = KNOWN_CONFIGS.filter((k) => {
7048
7221
  if (opts.agent && k.agent !== opts.agent)
@@ -7063,18 +7236,18 @@ program.command("sync").description("Sync known AI coding configs from disk into
7063
7236
  if (opts.project) {
7064
7237
  const dir = typeof opts.project === "string" ? opts.project : process.cwd();
7065
7238
  if (opts.all) {
7066
- const { readdirSync: readdirSync3, statSync: st } = await import("fs");
7239
+ const { readdirSync: readdirSync4, statSync: st } = await import("fs");
7067
7240
  const absDir = expandPath(dir);
7068
- const entries = readdirSync3(absDir, { withFileTypes: true });
7241
+ const entries = readdirSync4(absDir, { withFileTypes: true });
7069
7242
  let totalAdded = 0, totalUpdated = 0, totalUnchanged = 0, projects = 0;
7070
7243
  for (const entry of entries) {
7071
7244
  if (!entry.isDirectory())
7072
7245
  continue;
7073
- const projDir = join10(absDir, entry.name);
7074
- const hasClaude = existsSync12(join10(projDir, "CLAUDE.md")) || existsSync12(join10(projDir, ".mcp.json")) || existsSync12(join10(projDir, ".claude"));
7246
+ const projDir = join11(absDir, entry.name);
7247
+ const hasClaude = existsSync13(join11(projDir, "CLAUDE.md")) || existsSync13(join11(projDir, ".mcp.json")) || existsSync13(join11(projDir, ".claude"));
7075
7248
  if (!hasClaude)
7076
7249
  continue;
7077
- const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun });
7250
+ const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
7078
7251
  if (result2.added + result2.updated > 0) {
7079
7252
  console.log(` ${chalk.green("\u2713")} ${entry.name}: +${result2.added} updated:${result2.updated}`);
7080
7253
  }
@@ -7086,15 +7259,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
7086
7259
  console.log(chalk.green("\u2713") + ` Synced ${projects} projects: +${totalAdded} updated:${totalUpdated} unchanged:${totalUnchanged}`);
7087
7260
  return;
7088
7261
  }
7089
- const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun });
7262
+ const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun, store });
7090
7263
  console.log(chalk.green("\u2713") + ` Project sync: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
7091
7264
  return;
7092
7265
  }
7093
7266
  if (opts.toDisk) {
7094
- const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
7267
+ const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category, store });
7095
7268
  console.log(chalk.green("\u2713") + ` Written to disk: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
7096
7269
  } else {
7097
- const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
7270
+ const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category, store });
7098
7271
  console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
7099
7272
  if (result.skipped.length > 0) {
7100
7273
  console.log(chalk.dim(" skipped (not found): " + result.skipped.join(", ")));
@@ -7103,13 +7276,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
7103
7276
  });
7104
7277
  program.command("export").description("Export configs as a tar.gz bundle").option("-o, --output <path>", "output file", "./configs-export.tar.gz").option("-c, --category <cat>", "filter by category").action(async (opts) => {
7105
7278
  const result = await exportConfigs(opts.output, {
7106
- filter: opts.category ? { category: opts.category } : undefined
7279
+ filter: opts.category ? { category: opts.category } : undefined,
7280
+ store: resolveConfigStore()
7107
7281
  });
7108
7282
  console.log(chalk.green("\u2713") + ` Exported ${result.count} configs to ${result.path}`);
7109
7283
  });
7110
7284
  program.command("import <file>").description("Import configs from a tar.gz bundle").option("--overwrite", "overwrite existing configs").action(async (file, opts) => {
7111
7285
  const result = await importConfigs(file, {
7112
- conflict: opts.overwrite ? "overwrite" : "skip"
7286
+ conflict: opts.overwrite ? "overwrite" : "skip",
7287
+ store: resolveConfigStore()
7113
7288
  });
7114
7289
  console.log(chalk.green("\u2713") + ` Import complete: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
7115
7290
  if (result.errors.length > 0) {
@@ -7119,10 +7294,11 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
7119
7294
  }
7120
7295
  });
7121
7296
  program.command("whoami").description("Show setup summary").action(async () => {
7122
- const dbPath = process.env["CONFIGS_DB_PATH"] || join10(homedir6(), ".hasna", "configs", "configs.db");
7123
- const stats = getConfigStats();
7297
+ const store = resolveConfigStore();
7298
+ const dbPath = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["CONFIGS_DB_PATH"] || join11(homedir7(), ".hasna", "configs", "configs.db");
7299
+ const stats = await store.getConfigStats();
7124
7300
  console.log(chalk.bold("@hasna/configs") + chalk.dim(" v" + pkg.version));
7125
- console.log(chalk.cyan("DB:") + " " + dbPath);
7301
+ console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
7126
7302
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
7127
7303
  console.log();
7128
7304
  console.log(chalk.bold("By category:"));
@@ -7132,7 +7308,7 @@ program.command("whoami").description("Show setup summary").action(async () => {
7132
7308
  if (count > 0)
7133
7309
  console.log(` ${chalk.cyan(cat.padEnd(16))} ${count}`);
7134
7310
  }
7135
- const profiles = listProfiles();
7311
+ const profiles = await store.listProfiles();
7136
7312
  if (profiles.length > 0) {
7137
7313
  console.log();
7138
7314
  console.log(chalk.bold("Profiles:") + chalk.dim(` (${profiles.length})`));
@@ -7143,7 +7319,8 @@ program.command("whoami").description("Show setup summary").action(async () => {
7143
7319
  var profileCmd = program.command("profile").description("Manage config profiles (named bundles)");
7144
7320
  profileCmd.command("list").description("List all profiles").option("--brief", "compact one-line output").option("-f, --format <fmt>", "compact|table|json", "compact").option("--verbose", "show expanded profile metadata").option("--json", "output full profiles as JSON").option("--limit <n>", `max rows for human output (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor for human output").action(async (opts) => {
7145
7321
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
7146
- const profiles = listProfiles();
7322
+ const store = resolveConfigStore();
7323
+ const profiles = await store.listProfiles();
7147
7324
  if (profiles.length === 0) {
7148
7325
  console.log(chalk.dim("No profiles."));
7149
7326
  return;
@@ -7158,10 +7335,10 @@ profileCmd.command("list").description("List all profiles").option("--brief", "c
7158
7335
  for (const p of page.items) {
7159
7336
  if (fmt === "compact") {
7160
7337
  const selectorSummary2 = formatProfileSelectorSummary(p);
7161
- console.log(`${pad(p.slug, 28)} ${pad(String(getProfileConfigs(p.id).length), 8)} ${pad(selectorSummary2 || "-", 36)} ${Object.keys(p.variables).length}`);
7338
+ console.log(`${pad(p.slug, 28)} ${pad(String((await store.getProfileConfigs(p.id)).length), 8)} ${pad(selectorSummary2 || "-", 36)} ${Object.keys(p.variables).length}`);
7162
7339
  continue;
7163
7340
  }
7164
- const configs = getProfileConfigs(p.id);
7341
+ const configs = await store.getProfileConfigs(p.id);
7165
7342
  console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configs.length} config(s)`);
7166
7343
  if (p.description)
7167
7344
  console.log(` ${chalk.dim(p.description)}`);
@@ -7175,7 +7352,7 @@ profileCmd.command("list").description("List all profiles").option("--brief", "c
7175
7352
  pageFooter("configs profile list", page, "Use --verbose for expanded rows, --json for full records, or `configs profile show <slug>` for details.");
7176
7353
  });
7177
7354
  profileCmd.command("create <name>").description("Create a new profile").option("-d, --description <desc>", "profile description").option("--os <os>", "comma-separated OS matchers (linux, macos, darwin, etc.)").option("--arch <arch>", "comma-separated CPU arch matchers (arm64, x64, etc.)").option("--hostname <hosts>", "comma-separated hostname matchers").option("--var <vars...>", "set profile variable(s) as KEY=VALUE").action(async (name, opts) => {
7178
- const p = createProfile({
7355
+ const p = await resolveConfigStore().createProfile({
7179
7356
  name,
7180
7357
  description: opts.description,
7181
7358
  selectors: parseProfileSelectors(opts),
@@ -7185,8 +7362,9 @@ profileCmd.command("create <name>").description("Create a new profile").option("
7185
7362
  });
7186
7363
  profileCmd.command("show <id>").description("Show profile and its configs").option("--limit <n>", `max config rows (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor").action(async (id, opts) => {
7187
7364
  try {
7188
- const p = getProfile(id);
7189
- const configs = getProfileConfigs(id);
7365
+ const store = resolveConfigStore();
7366
+ const p = await store.getProfile(id);
7367
+ const configs = await store.getProfileConfigs(id);
7190
7368
  console.log(chalk.bold(p.name) + chalk.dim(` (${p.slug})`));
7191
7369
  if (p.description)
7192
7370
  console.log(chalk.dim(p.description));
@@ -7210,8 +7388,9 @@ profileCmd.command("show <id>").description("Show profile and its configs").opti
7210
7388
  });
7211
7389
  profileCmd.command("add <profile> <config>").description("Add a config to a profile").action(async (profile, config) => {
7212
7390
  try {
7213
- const c = getConfig(config);
7214
- addConfigToProfile(profile, c.id);
7391
+ const store = resolveConfigStore();
7392
+ const c = await store.getConfig(config);
7393
+ await store.addConfigToProfile(profile, c.id);
7215
7394
  console.log(chalk.green("\u2713") + ` Added ${c.slug} to profile ${profile}`);
7216
7395
  } catch (e) {
7217
7396
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7220,8 +7399,9 @@ profileCmd.command("add <profile> <config>").description("Add a config to a prof
7220
7399
  });
7221
7400
  profileCmd.command("remove <profile> <config>").description("Remove a config from a profile").action(async (profile, config) => {
7222
7401
  try {
7223
- const c = getConfig(config);
7224
- removeConfigFromProfile(profile, c.id);
7402
+ const store = resolveConfigStore();
7403
+ const c = await store.getConfig(config);
7404
+ await store.removeConfigFromProfile(profile, c.id);
7225
7405
  console.log(chalk.green("\u2713") + ` Removed ${c.slug} from profile ${profile}`);
7226
7406
  } catch (e) {
7227
7407
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7230,15 +7410,16 @@ profileCmd.command("remove <profile> <config>").description("Remove a config fro
7230
7410
  });
7231
7411
  profileCmd.command("apply [id]").description("Apply all configs in a profile to disk").option("--dry-run", "preview without writing").option("--auto", "resolve the matching profile for the current machine").option("--hostname <hostname>", "override detected hostname for auto resolution").option("--os <os>", "override detected OS for auto resolution").option("--arch <arch>", "override detected arch for auto resolution").action(async (id, opts) => {
7232
7412
  try {
7233
- const { machine, profile } = getMachineProfileContext(opts);
7234
- const selected = opts.auto ? profile : id ? getProfile(id) : null;
7413
+ const store = resolveConfigStore();
7414
+ const { machine, profile } = await getMachineProfileContext(opts, store);
7415
+ const selected = opts.auto ? profile : id ? await store.getProfile(id) : null;
7235
7416
  if (!selected) {
7236
7417
  console.error(chalk.red(opts.auto ? "No matching machine-aware profile found." : "Provide a profile id or use --auto."));
7237
7418
  process.exit(1);
7238
7419
  }
7239
- const configs = getProfileConfigs(selected.id);
7420
+ const configs = await store.getProfileConfigs(selected.id);
7240
7421
  const vars = resolveProfileVariables(selected, machine);
7241
- const results = await applyConfigs(configs, { dryRun: opts.dryRun, vars });
7422
+ const results = await applyConfigs(configs, { dryRun: opts.dryRun, vars, store });
7242
7423
  let changed = 0;
7243
7424
  for (const r of results) {
7244
7425
  const status = opts.dryRun ? chalk.yellow("[dry-run]") : r.changed ? chalk.green("\u2713") : chalk.dim("=");
@@ -7254,7 +7435,8 @@ ${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${
7254
7435
  }
7255
7436
  });
7256
7437
  profileCmd.command("resolve").description("Resolve the matching machine-aware profile").option("--hostname <hostname>", "override detected hostname").option("--os <os>", "override detected OS").option("--arch <arch>", "override detected arch").action(async (opts) => {
7257
- const { machine, profile, vars } = getMachineProfileContext(opts);
7438
+ const store = resolveConfigStore();
7439
+ const { machine, profile, vars } = await getMachineProfileContext(opts, store);
7258
7440
  if (!profile) {
7259
7441
  console.log(chalk.yellow(`No matching profile for ${machine.hostname} ${machine.os_family}/${machine.arch}`));
7260
7442
  process.exit(1);
@@ -7271,8 +7453,9 @@ profileCmd.command("resolve").description("Resolve the matching machine-aware pr
7271
7453
  });
7272
7454
  profileCmd.command("delete <id>").description("Delete a profile").action(async (id) => {
7273
7455
  try {
7274
- const p = getProfile(id);
7275
- deleteProfile(id);
7456
+ const store = resolveConfigStore();
7457
+ const p = await store.getProfile(id);
7458
+ await store.deleteProfile(p.id);
7276
7459
  console.log(chalk.green("\u2713") + ` Deleted profile: ${p.name}`);
7277
7460
  } catch (e) {
7278
7461
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7287,7 +7470,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
7287
7470
  console.error(chalk.red(`Unsupported tool: ${opts.tool}`));
7288
7471
  process.exit(1);
7289
7472
  }
7290
- const sources = collectSessionSources(opts, tool);
7473
+ const sources = await collectSessionSources(opts, tool, resolveConfigStore());
7291
7474
  const plan = planSessionRender({
7292
7475
  tool,
7293
7476
  profile: opts.profile,
@@ -7331,7 +7514,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7331
7514
  console.error(chalk.red(`Unsupported tool: ${opts.tool}`));
7332
7515
  process.exit(1);
7333
7516
  }
7334
- const sources = collectSessionSources(opts, tool);
7517
+ const sources = await collectSessionSources(opts, tool, resolveConfigStore());
7335
7518
  const plan = planSessionRender({
7336
7519
  tool,
7337
7520
  profile: opts.profile,
@@ -7378,8 +7561,9 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7378
7561
  var snapshotCmd = program.command("snapshot").description("Manage config version history");
7379
7562
  snapshotCmd.command("list <config>").description("List snapshots for a config").option("--limit <n>", `max rows (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor").action(async (configId, opts) => {
7380
7563
  try {
7381
- const c = getConfig(configId);
7382
- const snaps = listSnapshots(c.id);
7564
+ const store = resolveConfigStore();
7565
+ const c = await store.getConfig(configId);
7566
+ const snaps = await store.listSnapshots(c.id);
7383
7567
  if (snaps.length === 0) {
7384
7568
  console.log(chalk.dim("No snapshots."));
7385
7569
  return;
@@ -7395,7 +7579,7 @@ snapshotCmd.command("list <config>").description("List snapshots for a config").
7395
7579
  }
7396
7580
  });
7397
7581
  snapshotCmd.command("show <id>").description("Show a snapshot's content").action(async (id) => {
7398
- const snap = getSnapshot(id);
7582
+ const snap = await resolveConfigStore().getSnapshot(id);
7399
7583
  if (!snap) {
7400
7584
  console.error(chalk.red("Snapshot not found: " + id));
7401
7585
  process.exit(1);
@@ -7404,12 +7588,13 @@ snapshotCmd.command("show <id>").description("Show a snapshot's content").action
7404
7588
  });
7405
7589
  snapshotCmd.command("restore <config> <snapshot-id>").description("Restore a config to a snapshot version").action(async (configId, snapId) => {
7406
7590
  try {
7407
- const snap = getSnapshot(snapId);
7591
+ const store = resolveConfigStore();
7592
+ const snap = await store.getSnapshot(snapId);
7408
7593
  if (!snap) {
7409
7594
  console.error(chalk.red("Snapshot not found: " + snapId));
7410
7595
  process.exit(1);
7411
7596
  }
7412
- updateConfig(configId, { content: snap.content });
7597
+ await store.updateConfig(configId, { content: snap.content });
7413
7598
  console.log(chalk.green("\u2713") + ` Restored ${configId} to snapshot v${snap.version}`);
7414
7599
  } catch (e) {
7415
7600
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7419,7 +7604,7 @@ snapshotCmd.command("restore <config> <snapshot-id>").description("Restore a con
7419
7604
  var templateCmd = program.command("template").description("Work with template configs");
7420
7605
  templateCmd.command("vars <id>").description("Show template variables").action(async (id) => {
7421
7606
  try {
7422
- const c = getConfig(id);
7607
+ const c = await resolveConfigStore().getConfig(id);
7423
7608
  const vars = extractTemplateVars(c.content);
7424
7609
  if (vars.length === 0) {
7425
7610
  console.log(chalk.dim("No template variables found."));
@@ -7436,7 +7621,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
7436
7621
  templateCmd.command("render <id>").description("Render a template config with variables and optionally apply to disk").option("--var <vars...>", "set variables as KEY=VALUE pairs").option("--env", "use environment variables to fill template vars").option("--apply", "write rendered output to target_path").option("--dry-run", "preview rendered output without writing").action(async (id, opts) => {
7437
7622
  try {
7438
7623
  const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
7439
- const c = getConfig(id);
7624
+ const c = await resolveConfigStore().getConfig(id);
7440
7625
  const vars = {};
7441
7626
  if (opts.var) {
7442
7627
  for (const kv of opts.var) {
@@ -7467,9 +7652,9 @@ templateCmd.command("render <id>").description("Render a template config with va
7467
7652
  console.log(rendered);
7468
7653
  } else {
7469
7654
  const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync6 } = await import("fs");
7470
- const { dirname: dirname4 } = await import("path");
7655
+ const { dirname: dirname5 } = await import("path");
7471
7656
  const path = expandPath(c.target_path);
7472
- mkdirSync6(dirname4(path), { recursive: true });
7657
+ mkdirSync6(dirname5(path), { recursive: true });
7473
7658
  writeFileSync4(path, rendered, "utf-8");
7474
7659
  console.log(chalk.green("\u2713") + ` Rendered and applied to ${path}`);
7475
7660
  }
@@ -7482,11 +7667,12 @@ templateCmd.command("render <id>").description("Render a template config with va
7482
7667
  }
7483
7668
  });
7484
7669
  program.command("scan [id]").description("Scan configs for secrets. Defaults to known configs only.").option("--fix", "redact found secrets in-place").option("--all", "scan every config in the DB (slow on large DBs)").option("-c, --category <cat>", "scan only a specific category").option("--limit <n>", `max findings to print (default ${DEFAULT_LIST_LIMIT})`).action(async (id, opts) => {
7670
+ const store = resolveConfigStore();
7485
7671
  let configs;
7486
7672
  if (id) {
7487
- configs = [getConfig(id)];
7673
+ configs = [await store.getConfig(id)];
7488
7674
  } else if (opts.all) {
7489
- configs = listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
7675
+ configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
7490
7676
  } else {
7491
7677
  const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
7492
7678
  const slugs = [
@@ -7495,10 +7681,10 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7495
7681
  const fetched = [];
7496
7682
  for (const slug2 of slugs) {
7497
7683
  try {
7498
- fetched.push(getConfig(slug2));
7684
+ fetched.push(await store.getConfig(slug2));
7499
7685
  } catch {}
7500
7686
  }
7501
- const rules = listConfigs({ category: "rules", agent: "claude" });
7687
+ const rules = await store.listConfigs({ category: "rules", agent: "claude" });
7502
7688
  for (const r of rules)
7503
7689
  if (!fetched.find((c) => c.id === r.id))
7504
7690
  fetched.push(r);
@@ -7528,7 +7714,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7528
7714
  }
7529
7715
  if (opts.fix) {
7530
7716
  const { content, isTemplate: isTemplate2 } = redactContent(c.content, fmt);
7531
- updateConfig(c.id, { content, is_template: isTemplate2 });
7717
+ await store.updateConfig(c.id, { content, is_template: isTemplate2 });
7532
7718
  if (visible.length > 0)
7533
7719
  console.log(chalk.green(" \u2713 Redacted."));
7534
7720
  }
@@ -7547,6 +7733,32 @@ Run with --fix to redact in-place.`));
7547
7733
  Redacted all ${total} finding(s); printed ${printed}. Re-run without --fix and a higher --limit for full details.`));
7548
7734
  }
7549
7735
  });
7736
+ program.command("package-manager-scan [paths...]").description("Scan package-manager config for literal token ingress without printing values").option("--home", "also scan home .npmrc, Bun config, and shell profiles").option("--fail-on-findings", "exit nonzero when any finding is detected").option("--json", "output machine-readable JSON").option("--limit <n>", `max findings to print (default ${DEFAULT_LIST_LIMIT})`).action(async (paths, opts) => {
7737
+ const { scanPackageManagerSecrets: scanPackageManagerSecrets2 } = await Promise.resolve().then(() => (init_package_manager_guard(), exports_package_manager_guard));
7738
+ const roots = paths && paths.length > 0 ? paths : [process.cwd()];
7739
+ const result = scanPackageManagerSecrets2({ roots, includeHome: !!opts.home });
7740
+ const maxPrinted = parseLimit(opts.limit, DEFAULT_LIST_LIMIT);
7741
+ const visible = result.findings.slice(0, maxPrinted);
7742
+ const omitted = Math.max(0, result.findings.length - visible.length);
7743
+ if (opts.json) {
7744
+ console.log(JSON.stringify(result, null, 2));
7745
+ } else if (result.findings.length === 0) {
7746
+ console.log(chalk.green("\u2713") + ` Package-manager scan clean (${result.scannedFiles} file(s)).`);
7747
+ } else {
7748
+ console.log(chalk.red(`\u2717 ${result.findings.length} package-manager finding(s) detected.`));
7749
+ for (const finding of visible) {
7750
+ const tracked = finding.tracked ? "tracked" : "untracked";
7751
+ const color = finding.severity === "error" ? chalk.red : chalk.yellow;
7752
+ console.log(color(` ${finding.path}:${finding.line} ${finding.rule}`) + chalk.dim(` [${finding.surface}, ${tracked}] ${finding.detail}`));
7753
+ }
7754
+ if (omitted > 0)
7755
+ console.log(chalk.dim(` Omitted ${omitted} finding(s). Re-run with --limit ${result.findings.length} or --json.`));
7756
+ console.log(chalk.dim(" Secret values are never printed by this command."));
7757
+ }
7758
+ if (opts.failOnFindings && result.findings.length > 0) {
7759
+ process.exitCode = 1;
7760
+ }
7761
+ });
7550
7762
  var mcpCmd = program.command("mcp").description("Install/remove MCP server for AI agents");
7551
7763
  mcpCmd.command("install").alias("add").description("Install configs MCP server into an agent").option("--claude", "install into Claude Code").option("--codex", "install into Codex").option("--gemini", "install into Gemini").option("--all", "install into all agents").option("--profile <level>", "set CONFIGS_PROFILE (minimal|standard|full)", "standard").action(async (opts) => {
7552
7764
  const targets = opts.all ? ["claude", "codex", "gemini"] : [
@@ -7560,7 +7772,7 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
7560
7772
  }
7561
7773
  for (const target of targets) {
7562
7774
  try {
7563
- const { vars } = getMachineProfileContext({});
7775
+ const { vars } = await getMachineProfileContext({}, resolveConfigStore());
7564
7776
  const mcpBinary = `${vars["BUN_BIN_DIR"]}/configs-mcp`;
7565
7777
  if (target === "claude") {
7566
7778
  const cmd = opts.profile && opts.profile !== "full" ? ["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", "configs", "--", "env", `CONFIGS_PROFILE=${opts.profile}`, mcpBinary] : ["claude", "mcp", "add", "--transport", "stdio", "--scope", "user", "configs", "--", mcpBinary];
@@ -7570,14 +7782,14 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
7570
7782
  } else if (target === "codex") {
7571
7783
  const { appendFileSync, existsSync: ex } = await import("fs");
7572
7784
  const { join: j } = await import("path");
7573
- const configPath = j(homedir6(), ".codex", "config.toml");
7785
+ const configPath = j(homedir7(), ".codex", "config.toml");
7574
7786
  const block = `
7575
7787
  [mcp_servers.configs]
7576
7788
  command = "${mcpBinary}"
7577
7789
  args = []
7578
7790
  `;
7579
7791
  if (ex(configPath)) {
7580
- const content = readFileSync8(configPath, "utf-8");
7792
+ const content = readFileSync9(configPath, "utf-8");
7581
7793
  if (content.includes("[mcp_servers.configs]")) {
7582
7794
  console.log(chalk.dim("= Already installed in Codex"));
7583
7795
  continue;
@@ -7588,7 +7800,7 @@ args = []
7588
7800
  } else if (target === "gemini") {
7589
7801
  const { readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
7590
7802
  const { join: j } = await import("path");
7591
- const configPath = j(homedir6(), ".gemini", "settings.json");
7803
+ const configPath = j(homedir7(), ".gemini", "settings.json");
7592
7804
  let settings = {};
7593
7805
  if (ex(configPath)) {
7594
7806
  try {
@@ -7615,16 +7827,14 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
7615
7827
  }
7616
7828
  });
7617
7829
  program.command("init").description("First-time setup: sync all known configs, create default profile").option("--force", "delete existing DB and start fresh").action(async (opts) => {
7618
- const dbPath = join10(homedir6(), ".hasna", "configs", "configs.db");
7619
- if (opts.force && existsSync12(dbPath)) {
7620
- const { rmSync: rmSync4 } = await import("fs");
7621
- rmSync4(dbPath);
7622
- console.log(chalk.dim("Deleted existing DB."));
7623
- resetDatabase();
7830
+ const store = resolveConfigStore();
7831
+ if (opts.force) {
7832
+ await store.reset();
7833
+ console.log(chalk.dim("Reset local store."));
7624
7834
  }
7625
7835
  console.log(chalk.bold(`@hasna/configs \u2014 initializing
7626
7836
  `));
7627
- const result = await syncKnown({});
7837
+ const result = await syncKnown({ store });
7628
7838
  console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
7629
7839
  if (result.skipped.length > 0) {
7630
7840
  console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
@@ -7642,35 +7852,36 @@ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`,
7642
7852
  ];
7643
7853
  for (const ref of refs) {
7644
7854
  try {
7645
- getConfig(ref.slug);
7855
+ await store.getConfig(ref.slug);
7646
7856
  } catch {
7647
- createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
7857
+ await store.createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
7648
7858
  }
7649
7859
  }
7650
- ensureProjectDashboardStandardConfig();
7860
+ await ensureProjectDashboardStandardConfig(store);
7651
7861
  try {
7652
- getProfile("my-setup");
7862
+ await store.getProfile("my-setup");
7653
7863
  } catch {
7654
- const p = createProfile({ name: "my-setup", description: "Default profile with all known configs" });
7655
- const allConfigs = listConfigs();
7864
+ const p = await store.createProfile({ name: "my-setup", description: "Default profile with all known configs" });
7865
+ const allConfigs = await store.listConfigs();
7656
7866
  for (const c of allConfigs)
7657
- addConfigToProfile(p.id, c.id);
7867
+ await store.addConfigToProfile(p.id, c.id);
7658
7868
  console.log(chalk.green("\u2713") + ` Created profile "my-setup" with ${allConfigs.length} configs`);
7659
7869
  }
7660
- const machineProfiles = ensurePlatformProfiles();
7870
+ const machineProfiles = await ensurePlatformProfiles(store);
7661
7871
  console.log(chalk.green("\u2713") + ` Ensured ${machineProfiles.length} machine-aware profile(s)`);
7662
- const stats = getConfigStats();
7872
+ const stats = await store.getConfigStats();
7663
7873
  console.log(chalk.bold(`
7664
7874
  DB stats:`));
7665
7875
  for (const [key, count] of Object.entries(stats)) {
7666
7876
  if (count > 0)
7667
7877
  console.log(` ${key.padEnd(18)} ${count}`);
7668
7878
  }
7879
+ const location = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["CONFIGS_DB_PATH"] || join11(homedir7(), ".hasna", "configs", "configs.db");
7669
7880
  console.log(chalk.dim(`
7670
- DB: ${dbPath}`));
7881
+ ${isCloudMode() ? "API" : "DB"}: ${location}`));
7671
7882
  });
7672
7883
  program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
7673
- const status = getConfigsStatus();
7884
+ const status = await getConfigsStatus(resolveConfigStore());
7674
7885
  if (opts.json) {
7675
7886
  console.log(JSON.stringify(status, null, 2));
7676
7887
  return;
@@ -7686,17 +7897,17 @@ program.command("status").description("Health check: total configs, drift from d
7686
7897
  });
7687
7898
  program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
7688
7899
  const { mkdirSync: mk } = await import("fs");
7689
- const backupDir = join10(homedir6(), ".hasna", "configs", "backups");
7900
+ const backupDir = join11(homedir7(), ".hasna", "configs", "backups");
7690
7901
  mk(backupDir, { recursive: true });
7691
7902
  const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
7692
- const outPath = join10(backupDir, `configs-${ts}.tar.gz`);
7693
- const result = await exportConfigs(outPath);
7903
+ const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
7904
+ const result = await exportConfigs(outPath, { store: resolveConfigStore() });
7694
7905
  const { statSync: st } = await import("fs");
7695
7906
  const size = st(outPath).size;
7696
7907
  console.log(chalk.green("\u2713") + ` Backup: ${result.count} configs \u2192 ${outPath} (${(size / 1024).toFixed(1)}KB)`);
7697
7908
  });
7698
7909
  program.command("restore <file>").description("Restore configs from a backup file").option("--overwrite", "overwrite existing configs (default: skip)").action(async (file, opts) => {
7699
- const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip" });
7910
+ const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip", store: resolveConfigStore() });
7700
7911
  console.log(chalk.green("\u2713") + ` Restored: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
7701
7912
  if (result.errors.length > 0) {
7702
7913
  for (const e of result.errors)
@@ -7704,6 +7915,7 @@ program.command("restore <file>").description("Restore configs from a backup fil
7704
7915
  }
7705
7916
  });
7706
7917
  program.command("doctor").description("Validate configs: syntax, permissions, missing files, secrets").action(async () => {
7918
+ const store = resolveConfigStore();
7707
7919
  let issues = 0;
7708
7920
  const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
7709
7921
  const fail = (msg) => {
@@ -7716,12 +7928,12 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
7716
7928
  console.log(chalk.cyan("Known files on disk:"));
7717
7929
  for (const k of KNOWN_CONFIGS) {
7718
7930
  if (k.rulesDir) {
7719
- existsSync12(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
7931
+ existsSync13(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
7720
7932
  } else {
7721
- existsSync12(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
7933
+ existsSync13(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
7722
7934
  }
7723
7935
  }
7724
- const allConfigs = listConfigs();
7936
+ const allConfigs = await store.listConfigs();
7725
7937
  console.log(chalk.cyan(`
7726
7938
  Stored configs (${allConfigs.length}):`));
7727
7939
  let validCount = 0;
@@ -7791,8 +8003,9 @@ complete -F _configs_completions configs`);
7791
8003
  });
7792
8004
  program.command("compare <a> <b>").description("Diff two stored configs against each other").action(async (a, b) => {
7793
8005
  try {
7794
- const configA = getConfig(a);
7795
- const configB = getConfig(b);
8006
+ const store = resolveConfigStore();
8007
+ const configA = await store.getConfig(a);
8008
+ const configB = await store.getConfig(b);
7796
8009
  console.log(chalk.bold(`${configA.slug}`) + chalk.dim(` (${configA.category}/${configA.agent})`));
7797
8010
  console.log(chalk.bold(`${configB.slug}`) + chalk.dim(` (${configB.category}/${configB.agent})`));
7798
8011
  console.log();
@@ -7831,6 +8044,7 @@ ${diffs} difference(s)`));
7831
8044
  }
7832
8045
  });
7833
8046
  program.command("watch").description("Watch known config files for changes and auto-sync to DB").option("-i, --interval <ms>", "poll interval in milliseconds", "3000").action(async (opts) => {
8047
+ const store = resolveConfigStore();
7834
8048
  const interval = Number(opts.interval);
7835
8049
  const { statSync: st } = await import("fs");
7836
8050
  const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
@@ -7841,16 +8055,16 @@ program.command("watch").description("Watch known config files for changes and a
7841
8055
  for (const k of KNOWN_CONFIGS) {
7842
8056
  if (k.rulesDir) {
7843
8057
  const absDir = expandPath2(k.rulesDir);
7844
- if (!existsSync12(absDir))
8058
+ if (!existsSync13(absDir))
7845
8059
  continue;
7846
- const { readdirSync: readdirSync3 } = await import("fs");
7847
- for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
7848
- const abs = join10(absDir, f);
8060
+ const { readdirSync: readdirSync4 } = await import("fs");
8061
+ for (const f of readdirSync4(absDir).filter((f2) => f2.endsWith(".md"))) {
8062
+ const abs = join11(absDir, f);
7849
8063
  mtimes.set(abs, st(abs).mtimeMs);
7850
8064
  }
7851
8065
  } else {
7852
8066
  const abs = expandPath2(k.path);
7853
- if (existsSync12(abs))
8067
+ if (existsSync13(abs))
7854
8068
  mtimes.set(abs, st(abs).mtimeMs);
7855
8069
  }
7856
8070
  }
@@ -7858,7 +8072,7 @@ program.command("watch").description("Watch known config files for changes and a
7858
8072
  const tick = async () => {
7859
8073
  let changed = 0;
7860
8074
  for (const [abs, oldMtime] of mtimes) {
7861
- if (!existsSync12(abs))
8075
+ if (!existsSync13(abs))
7862
8076
  continue;
7863
8077
  const newMtime = st(abs).mtimeMs;
7864
8078
  if (newMtime !== oldMtime) {
@@ -7870,10 +8084,10 @@ program.command("watch").description("Watch known config files for changes and a
7870
8084
  for (const k of KNOWN_CONFIGS) {
7871
8085
  if (k.rulesDir) {
7872
8086
  const absDir = expandPath2(k.rulesDir);
7873
- if (!existsSync12(absDir))
8087
+ if (!existsSync13(absDir))
7874
8088
  continue;
7875
8089
  for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
7876
- const abs = join10(absDir, f);
8090
+ const abs = join11(absDir, f);
7877
8091
  if (!mtimes.has(abs)) {
7878
8092
  mtimes.set(abs, st(abs).mtimeMs);
7879
8093
  changed++;
@@ -7881,14 +8095,14 @@ program.command("watch").description("Watch known config files for changes and a
7881
8095
  }
7882
8096
  } else {
7883
8097
  const abs = expandPath2(k.path);
7884
- if (existsSync12(abs) && !mtimes.has(abs)) {
8098
+ if (existsSync13(abs) && !mtimes.has(abs)) {
7885
8099
  mtimes.set(abs, st(abs).mtimeMs);
7886
8100
  changed++;
7887
8101
  }
7888
8102
  }
7889
8103
  }
7890
8104
  if (changed > 0) {
7891
- const result = await syncKnown({});
8105
+ const result = await syncKnown({ store });
7892
8106
  const ts = new Date().toLocaleTimeString();
7893
8107
  console.log(`${chalk.dim(ts)} ${chalk.green("\u2713")} ${changed} file(s) changed/new \u2192 synced +${result.added} updated:${result.updated}`);
7894
8108
  }
@@ -7897,22 +8111,23 @@ program.command("watch").description("Watch known config files for changes and a
7897
8111
  await new Promise(() => {});
7898
8112
  });
7899
8113
  program.command("report").description("Summary of stored configs, drift, and ecosystem health").option("--json", "output as JSON").option("--markdown", "output as markdown").action(async () => {
7900
- const stats = getConfigStats();
7901
- const allConfigs = listConfigs();
8114
+ const store = resolveConfigStore();
8115
+ const stats = await store.getConfigStats();
8116
+ const allConfigs = await store.listConfigs();
7902
8117
  const fileConfigs = allConfigs.filter((c) => c.kind === "file");
7903
8118
  const refConfigs = allConfigs.filter((c) => c.kind === "reference");
7904
8119
  const templates = allConfigs.filter((c) => c.is_template);
7905
- const profiles = listProfiles();
8120
+ const profiles = await store.listProfiles();
7906
8121
  let drifted = 0, missing = 0;
7907
8122
  for (const c of fileConfigs) {
7908
8123
  if (!c.target_path)
7909
8124
  continue;
7910
8125
  const abs = expandPath(c.target_path);
7911
- if (!existsSync12(abs)) {
8126
+ if (!existsSync13(abs)) {
7912
8127
  missing++;
7913
8128
  continue;
7914
8129
  }
7915
- const disk = readFileSync8(abs, "utf-8");
8130
+ const disk = readFileSync9(abs, "utf-8");
7916
8131
  const { content: redactedDisk } = redactContent(disk, c.format);
7917
8132
  if (redactedDisk !== c.content)
7918
8133
  drifted++;
@@ -7944,7 +8159,8 @@ program.command("report").description("Summary of stored configs, drift, and eco
7944
8159
  }
7945
8160
  });
7946
8161
  program.command("clean").description("Remove configs from DB whose target files no longer exist on disk").option("--dry-run", "show what would be removed").option("--limit <n>", `max orphan rows to print (default ${DEFAULT_LIST_LIMIT})`).action(async (opts) => {
7947
- const configs = listConfigs({ kind: "file" });
8162
+ const store = resolveConfigStore();
8163
+ const configs = await store.listConfigs({ kind: "file" });
7948
8164
  let removed = 0;
7949
8165
  let printed = 0;
7950
8166
  const maxPrinted = parseLimit(opts.limit, DEFAULT_LIST_LIMIT);
@@ -7952,7 +8168,7 @@ program.command("clean").description("Remove configs from DB whose target files
7952
8168
  if (!c.target_path)
7953
8169
  continue;
7954
8170
  const abs = expandPath(c.target_path);
7955
- if (!existsSync12(abs)) {
8171
+ if (!existsSync13(abs)) {
7956
8172
  if (printed < maxPrinted) {
7957
8173
  if (opts.dryRun) {
7958
8174
  console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);
@@ -7962,7 +8178,7 @@ program.command("clean").description("Remove configs from DB whose target files
7962
8178
  printed++;
7963
8179
  }
7964
8180
  if (!opts.dryRun)
7965
- deleteConfig(c.id);
8181
+ await store.deleteConfig(c.id);
7966
8182
  removed++;
7967
8183
  }
7968
8184
  }
@@ -7977,6 +8193,7 @@ ${removed} orphaned config(s) ${opts.dryRun ? "found" : "removed"}${omitted > 0
7977
8193
  }
7978
8194
  });
7979
8195
  program.command("bootstrap").description("Install the full @hasna ecosystem: CLI tools + MCP servers + configs").option("--dry-run", "show what would be installed without doing it").option("--skip-mcp", "skip MCP server registration").action(async (opts) => {
8196
+ const store = resolveConfigStore();
7980
8197
  const packages = [
7981
8198
  { name: "@hasna/todos", bin: "todos", mcp: "todos-mcp" },
7982
8199
  { name: "@hasna/mementos", bin: "mementos", mcp: "mementos-mcp" },
@@ -8033,7 +8250,7 @@ Registering MCP servers in Claude Code:`));
8033
8250
  console.log(chalk.cyan(`
8034
8251
  Initializing configs:`));
8035
8252
  if (!opts.dryRun) {
8036
- const result = await syncKnown({});
8253
+ const result = await syncKnown({ store });
8037
8254
  console.log(chalk.green(" \u2713 ") + `Synced ${result.added + result.updated + result.unchanged} known configs`);
8038
8255
  } else {
8039
8256
  console.log(chalk.dim(" would run: configs init"));
@@ -8042,11 +8259,11 @@ Initializing configs:`));
8042
8259
  \u2713 Bootstrap complete.`) + chalk.dim(" Restart Claude Code for MCP servers to activate."));
8043
8260
  });
8044
8261
  program.command("pull").description("Alias for sync (read from disk into DB)").option("-a, --agent <agent>", "only sync this agent").option("--dry-run", "preview without writing").action(async (opts) => {
8045
- const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent });
8262
+ const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, store: resolveConfigStore() });
8046
8263
  console.log(chalk.green("\u2713") + ` Pulled: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
8047
8264
  });
8048
8265
  program.command("push").description("Alias for sync --to-disk (write DB configs to disk)").option("-a, --agent <agent>", "only push this agent").option("--dry-run", "preview without writing").action(async (opts) => {
8049
- const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent });
8266
+ const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, store: resolveConfigStore() });
8050
8267
  console.log(chalk.green("\u2713") + ` Pushed: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
8051
8268
  });
8052
8269
  program.command("update").description("Check for updates and install latest version").option("--check", "only check, don't install").action(async (opts) => {
@@ -8070,11 +8287,14 @@ program.command("update").description("Check for updates and install latest vers
8070
8287
  }
8071
8288
  });
8072
8289
  program.command("feedback <message>").description("Send feedback about this service").option("-e, --email <email>", "Contact email").option("-c, --category <cat>", "Category: bug, feature, general", "general").action(async (message, opts) => {
8073
- const db = getDatabase();
8074
- db.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [message, opts.email || null, opts.category || "general", pkg.version]);
8290
+ await resolveConfigStore().sendFeedback({
8291
+ message,
8292
+ email: opts.email || null,
8293
+ category: opts.category || "general",
8294
+ version: pkg.version
8295
+ });
8075
8296
  console.log(chalk.green("\u2713") + " Feedback saved. Thank you!");
8076
8297
  });
8077
8298
  program.version(pkg.version).name("instructions");
8078
- registerStorageCommands(program);
8079
8299
  registerEventsCommands(program, { source: "configs" });
8080
8300
  program.parse(process.argv);