@hasna/instructions 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1271 -841
  4. package/dist/data/config-store.d.ts +134 -0
  5. package/dist/data/config-store.d.ts.map +1 -0
  6. package/dist/data/config-store.test.d.ts +2 -0
  7. package/dist/data/config-store.test.d.ts.map +1 -0
  8. package/dist/db/database.d.ts +15 -0
  9. package/dist/db/database.d.ts.map +1 -1
  10. package/dist/index.d.ts +6 -8
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +896 -492
  13. package/dist/lib/apply.d.ts +2 -2
  14. package/dist/lib/apply.d.ts.map +1 -1
  15. package/dist/lib/export.d.ts +2 -2
  16. package/dist/lib/export.d.ts.map +1 -1
  17. package/dist/lib/import.d.ts +2 -2
  18. package/dist/lib/import.d.ts.map +1 -1
  19. package/dist/lib/package-manager-guard.d.ts +24 -0
  20. package/dist/lib/package-manager-guard.d.ts.map +1 -0
  21. package/dist/lib/package-manager-guard.test.d.ts +2 -0
  22. package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
  23. package/dist/lib/platform-profiles.d.ts +2 -2
  24. package/dist/lib/platform-profiles.d.ts.map +1 -1
  25. package/dist/lib/project-dashboard-standard.d.ts +2 -2
  26. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  27. package/dist/lib/redact.d.ts.map +1 -1
  28. package/dist/lib/sync-dir.d.ts +3 -3
  29. package/dist/lib/sync-dir.d.ts.map +1 -1
  30. package/dist/lib/sync.d.ts +6 -6
  31. package/dist/lib/sync.d.ts.map +1 -1
  32. package/dist/mcp/http.d.ts +0 -13
  33. package/dist/mcp/http.d.ts.map +1 -1
  34. package/dist/mcp/index.js +649 -574
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +1757 -17541
  38. package/dist/server/v1.d.ts.map +1 -1
  39. package/dist/status.d.ts +2 -2
  40. package/dist/status.d.ts.map +1 -1
  41. package/dist/storage/cloud-store.d.ts +21 -1
  42. package/dist/storage/cloud-store.d.ts.map +1 -1
  43. package/dist/storage/schema.d.ts.map +1 -1
  44. package/package.json +4 -7
  45. package/dashboard/dist/assets/index-D7p6fFQw.js +0 -11
  46. package/dashboard/dist/assets/index-DQ3P1g1z.css +0 -1
  47. package/dashboard/dist/index.html +0 -14
  48. package/dashboard/dist/vite.svg +0 -1
  49. package/dist/cli/storage.d.ts +0 -3
  50. package/dist/cli/storage.d.ts.map +0 -1
  51. package/dist/cli/storage.test.d.ts +0 -2
  52. package/dist/cli/storage.test.d.ts.map +0 -1
  53. package/dist/db/remote-storage.d.ts +0 -13
  54. package/dist/db/remote-storage.d.ts.map +0 -1
  55. package/dist/db/storage-sync.d.ts +0 -53
  56. package/dist/db/storage-sync.d.ts.map +0 -1
  57. package/dist/db/storage-sync.test.d.ts +0 -2
  58. package/dist/db/storage-sync.test.d.ts.map +0 -1
  59. package/dist/server/server.test.d.ts +0 -2
  60. package/dist/server/server.test.d.ts.map +0 -1
  61. package/dist/storage.d.ts +0 -5
  62. package/dist/storage.d.ts.map +0 -1
  63. package/dist/storage.js +0 -537
package/dist/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,6 +4103,399 @@ var init_sync = __esm(() => {
3581
4103
  ];
3582
4104
  });
3583
4105
 
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;
4133
+ }
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
4166
+ };
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;
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));
4239
+ } else {
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
+ });
4401
+ }
4402
+ }
4403
+ return findings;
4404
+ }
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
+
3584
4499
  // node_modules/@hasna/events/dist/commander.js
3585
4500
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
3586
4501
  import { existsSync } from "fs";
@@ -4225,211 +5140,78 @@ function registerEventCommands(program, options) {
4225
5140
  rows = rows.filter((event) => event.source === actionOptions.source);
4226
5141
  if (actionOptions.type)
4227
5142
  rows = rows.filter((event) => event.type === actionOptions.type);
4228
- if (actionOptions.limit)
4229
- rows = rows.slice(-actionOptions.limit);
4230
- if (wantsJson(actionOptions, command)) {
4231
- console.log(JSON.stringify(rows, null, 2));
4232
- return;
4233
- }
4234
- if (!rows.length) {
4235
- console.log("No events recorded.");
4236
- return;
4237
- }
4238
- for (const event of rows)
4239
- console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
4240
- });
4241
- 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) => {
4242
- const result = await createClient(options).replay({
4243
- eventId: actionOptions.id,
4244
- source: actionOptions.source,
4245
- type: actionOptions.type,
4246
- dryRun: actionOptions.dryRun
4247
- });
4248
- print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
4249
- });
4250
- return events;
4251
- }
4252
- function registerEventsCommands(program, options) {
4253
- registerWebhookCommands(program, options);
4254
- registerEventCommands(program, options);
4255
- }
4256
- function parseNumber(value) {
4257
- const parsed = Number(value);
4258
- if (!Number.isFinite(parsed))
4259
- throw new Error(`Expected a number, got ${value}`);
4260
- return parsed;
4261
- }
4262
- function collectValues(value, previous) {
4263
- previous.push(value);
4264
- return previous;
4265
- }
4266
-
4267
- // node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
4268
- var import__ = __toESM(require_commander(), 1);
4269
- var {
4270
- program,
4271
- createCommand,
4272
- createArgument,
4273
- createOption,
4274
- CommanderError,
4275
- InvalidArgumentError,
4276
- InvalidOptionArgumentError,
4277
- Command,
4278
- Argument,
4279
- Option,
4280
- Help
4281
- } = import__.default;
4282
-
4283
- // src/cli/index.tsx
4284
- init_configs();
4285
- import chalk from "chalk";
4286
- import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
4287
- import { homedir as homedir6 } from "os";
4288
- import { basename as basename5, join as join10, resolve as resolve6 } from "path";
4289
-
4290
- // src/db/profiles.ts
4291
- init_types();
4292
- init_database();
4293
- init_configs();
4294
- init_machine();
4295
- function rowToProfile(row) {
4296
- return {
4297
- ...row,
4298
- selectors: JSON.parse(row.selectors || "{}"),
4299
- variables: JSON.parse(row.variables || "{}")
4300
- };
4301
- }
4302
- function uniqueProfileSlug(name, db, excludeId) {
4303
- const base = slugify(name);
4304
- let slug = base;
4305
- let i = 1;
4306
- while (true) {
4307
- const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
4308
- if (!existing || existing.id === excludeId)
4309
- return slug;
4310
- slug = `${base}-${i++}`;
4311
- }
4312
- }
4313
- function createProfile(input, db) {
4314
- const d = db || getDatabase();
4315
- const id = uuid();
4316
- const ts = now2();
4317
- const slug = uniqueProfileSlug(input.name, d);
4318
- d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
4319
- id,
4320
- input.name,
4321
- slug,
4322
- input.description ?? null,
4323
- JSON.stringify(input.selectors ?? {}),
4324
- JSON.stringify(input.variables ?? {}),
4325
- ts,
4326
- ts
4327
- ]);
4328
- return getProfile(id, d);
4329
- }
4330
- function getProfile(idOrSlug, db) {
4331
- const d = db || getDatabase();
4332
- const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
4333
- if (!row)
4334
- throw new ProfileNotFoundError(idOrSlug);
4335
- return rowToProfile(row);
4336
- }
4337
- function listProfiles(db) {
4338
- const d = db || getDatabase();
4339
- return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
4340
- }
4341
- function updateProfile(idOrSlug, input, db) {
4342
- const d = db || getDatabase();
4343
- const existing = getProfile(idOrSlug, d);
4344
- const ts = now2();
4345
- const updates = ["updated_at = ?"];
4346
- const params = [ts];
4347
- if (input.name !== undefined) {
4348
- updates.push("name = ?", "slug = ?");
4349
- params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
4350
- }
4351
- if (input.description !== undefined) {
4352
- updates.push("description = ?");
4353
- params.push(input.description);
4354
- }
4355
- if (input.selectors !== undefined) {
4356
- updates.push("selectors = ?");
4357
- params.push(JSON.stringify(input.selectors));
4358
- }
4359
- if (input.variables !== undefined) {
4360
- updates.push("variables = ?");
4361
- params.push(JSON.stringify(input.variables));
4362
- }
4363
- params.push(existing.id);
4364
- d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
4365
- return getProfile(existing.id, d);
4366
- }
4367
- function deleteProfile(idOrSlug, db) {
4368
- const d = db || getDatabase();
4369
- const existing = getProfile(idOrSlug, d);
4370
- d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
4371
- }
4372
- function addConfigToProfile(profileIdOrSlug, configId, db) {
4373
- const d = db || getDatabase();
4374
- const profile = getProfile(profileIdOrSlug, d);
4375
- const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
4376
- const order = (maxRow?.max_order ?? -1) + 1;
4377
- d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
4378
- }
4379
- function removeConfigFromProfile(profileIdOrSlug, configId, db) {
4380
- const d = db || getDatabase();
4381
- const profile = getProfile(profileIdOrSlug, d);
4382
- d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
4383
- }
4384
- function getProfileConfigs(profileIdOrSlug, db) {
4385
- const d = db || getDatabase();
4386
- const profile = getProfile(profileIdOrSlug, d);
4387
- const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
4388
- if (rows.length === 0)
4389
- return [];
4390
- const ids = rows.map((r) => r.config_id);
4391
- return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
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;
4392
5166
  }
4393
- function profileHasSelectors(profile) {
4394
- const selectors = profile.selectors ?? {};
4395
- 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);
4396
5170
  }
4397
- function profileMatchesMachine(profile, machine) {
4398
- const selectors = profile.selectors ?? {};
4399
- const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
4400
- const value = candidate.trim().toLowerCase();
4401
- return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
4402
- });
4403
- const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
4404
- const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
4405
- 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;
4406
5176
  }
4407
- function resolveProfileForMachine(machine = detectMachineContext(), db) {
4408
- const profiles = listProfiles(db).filter(profileHasSelectors);
4409
- const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
4410
- const selectors = profile.selectors;
4411
- const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
4412
- return { profile, score };
4413
- }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
4414
- return matches[0]?.profile ?? null;
5177
+ function collectValues(value, previous) {
5178
+ previous.push(value);
5179
+ return previous;
4415
5180
  }
4416
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
+
4417
5198
  // src/cli/index.tsx
4418
- init_snapshots();
4419
- init_database();
4420
5199
  init_apply();
4421
5200
  init_sync();
4422
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";
4423
5206
 
4424
5207
  // src/lib/export.ts
4425
- init_database();
4426
- init_configs();
4427
- 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";
4428
5210
  import { join as join6, resolve as resolve2 } from "path";
4429
5211
  import { tmpdir } from "os";
4430
5212
  async function exportConfigs(outputPath, opts = {}) {
4431
- const d = opts.db || getDatabase();
4432
- const configs = listConfigs(opts.filter, d);
5213
+ const store = opts.store ?? resolveConfigStore();
5214
+ const configs = await store.listConfigs(opts.filter);
4433
5215
  const absOutput = resolve2(outputPath);
4434
5216
  const tmpDir = join6(tmpdir(), `configs-export-${Date.now()}`);
4435
5217
  const contentsDir = join6(tmpDir, "contents");
@@ -4437,7 +5219,7 @@ async function exportConfigs(outputPath, opts = {}) {
4437
5219
  mkdirSync3(contentsDir, { recursive: true });
4438
5220
  const manifest = {
4439
5221
  version: "1.0.0",
4440
- exported_at: now2(),
5222
+ exported_at: new Date().toISOString(),
4441
5223
  configs: configs.map(({ content: _content, ...meta }) => meta)
4442
5224
  };
4443
5225
  writeFileSync2(join6(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
@@ -4457,19 +5239,18 @@ async function exportConfigs(outputPath, opts = {}) {
4457
5239
  return { path: absOutput, count: configs.length };
4458
5240
  } finally {
4459
5241
  if (existsSync7(tmpDir)) {
4460
- rmSync(tmpDir, { recursive: true, force: true });
5242
+ rmSync2(tmpDir, { recursive: true, force: true });
4461
5243
  }
4462
5244
  }
4463
5245
  }
4464
5246
 
4465
5247
  // src/lib/import.ts
4466
- init_database();
4467
- init_configs();
4468
- 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";
4469
5250
  import { join as join7, resolve as resolve3 } from "path";
4470
5251
  import { tmpdir as tmpdir2 } from "os";
4471
5252
  async function importConfigs(bundlePath, opts = {}) {
4472
- const d = opts.db || getDatabase();
5253
+ const store = opts.store ?? resolveConfigStore();
4473
5254
  const conflict = opts.conflict ?? "skip";
4474
5255
  const absPath = resolve3(bundlePath);
4475
5256
  const tmpDir = join7(tmpdir2(), `configs-import-${Date.now()}`);
@@ -4496,17 +5277,17 @@ async function importConfigs(bundlePath, opts = {}) {
4496
5277
  const content = existsSync8(contentFile) ? readFileSync4(contentFile, "utf-8") : "";
4497
5278
  let existing = null;
4498
5279
  try {
4499
- existing = getConfig(meta.slug, d);
5280
+ existing = await store.getConfig(meta.slug);
4500
5281
  } catch {}
4501
5282
  if (existing) {
4502
5283
  if (conflict === "skip") {
4503
5284
  result.skipped++;
4504
5285
  } else if (conflict === "overwrite" || conflict === "version") {
4505
- 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 });
4506
5287
  result.updated++;
4507
5288
  }
4508
5289
  } else {
4509
- createConfig({
5290
+ await store.createConfig({
4510
5291
  name: meta.name,
4511
5292
  kind: meta.kind,
4512
5293
  category: meta.category,
@@ -4518,7 +5299,7 @@ async function importConfigs(bundlePath, opts = {}) {
4518
5299
  description: meta.description ?? undefined,
4519
5300
  tags: meta.tags,
4520
5301
  is_template: meta.is_template
4521
- }, d);
5302
+ });
4522
5303
  result.created++;
4523
5304
  }
4524
5305
  } catch (err) {
@@ -4528,7 +5309,7 @@ async function importConfigs(bundlePath, opts = {}) {
4528
5309
  return result;
4529
5310
  } finally {
4530
5311
  if (existsSync8(tmpDir)) {
4531
- rmSync2(tmpDir, { recursive: true, force: true });
5312
+ rmSync3(tmpDir, { recursive: true, force: true });
4532
5313
  }
4533
5314
  }
4534
5315
  }
@@ -4538,14 +5319,14 @@ init_template();
4538
5319
  init_machine();
4539
5320
 
4540
5321
  // src/lib/session-apply.ts
4541
- import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
5322
+ import { createHash as createHash2, randomUUID as randomUUID6 } from "crypto";
4542
5323
  import {
4543
5324
  existsSync as existsSync10,
4544
5325
  lstatSync,
4545
5326
  mkdirSync as mkdirSync5,
4546
5327
  readFileSync as readFileSync6,
4547
5328
  renameSync,
4548
- rmSync as rmSync3,
5329
+ rmSync as rmSync4,
4549
5330
  writeFileSync as writeFileSync3
4550
5331
  } from "fs";
4551
5332
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join9, parse as parse2, relative as relative3, resolve as resolve5 } from "path";
@@ -5478,7 +6259,7 @@ function applySessionRender(plan, options = {}) {
5478
6259
  continue;
5479
6260
  assertNoSymlinkSegments(targetHome, result.path);
5480
6261
  if (existsSync10(result.path))
5481
- rmSync3(result.path);
6262
+ rmSync4(result.path);
5482
6263
  }
5483
6264
  }
5484
6265
  return {
@@ -5712,7 +6493,7 @@ function writePlannedFile(path, content, targetHome) {
5712
6493
  const dir = dirname3(path);
5713
6494
  mkdirSync5(dir, { recursive: true });
5714
6495
  assertNoSymlinkSegments(targetHome, path);
5715
- const tmp = join9(dir, `.session-${randomUUID5()}.tmp`);
6496
+ const tmp = join9(dir, `.session-${randomUUID6()}.tmp`);
5716
6497
  writeFileSync3(tmp, content, "utf-8");
5717
6498
  renameSync(tmp, path);
5718
6499
  }
@@ -5730,7 +6511,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
5730
6511
  if (!previousManifest && existingFiles.length === 0)
5731
6512
  return null;
5732
6513
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
5733
- const snapshotPath = resolve5(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID5()}.json`);
6514
+ const snapshotPath = resolve5(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID6()}.json`);
5734
6515
  const snapshot = {
5735
6516
  schema: "hasna.configs.session-render-snapshot/v1",
5736
6517
  createdAt: new Date().toISOString(),
@@ -5788,10 +6569,10 @@ function sha2562(content) {
5788
6569
  }
5789
6570
 
5790
6571
  // src/lib/platform-profiles.ts
5791
- init_configs();
6572
+ init_config_store();
5792
6573
 
5793
6574
  // src/lib/project-dashboard-standard.ts
5794
- init_configs();
6575
+ init_config_store();
5795
6576
  var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
5796
6577
  var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
5797
6578
  PROJECT_DASHBOARD_DIR: ".hasna/project",
@@ -5869,7 +6650,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
5869
6650
  ids, passport numbers, credentials, and contract clauses unless an explicit
5870
6651
  approved storage policy exists.
5871
6652
  `;
5872
- function ensureProjectDashboardStandardConfig(db) {
6653
+ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
5873
6654
  const input = {
5874
6655
  name: "Agent Managed Project Dashboard Standard",
5875
6656
  category: "workspace",
@@ -5881,17 +6662,21 @@ function ensureProjectDashboardStandardConfig(db) {
5881
6662
  tags: ["projects-dashboard", "agent-projects", "json-render"]
5882
6663
  };
5883
6664
  try {
5884
- const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG, db);
6665
+ const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
5885
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) {
5886
- return updateConfig(existing.id, input, db);
6667
+ return await store.updateConfig(existing.id, input);
5887
6668
  }
5888
6669
  return existing;
5889
6670
  } catch {
5890
- return createConfig(input, db);
6671
+ return await store.createConfig(input);
5891
6672
  }
5892
6673
  }
5893
6674
 
5894
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
+ }
5895
6680
  var PLATFORM_PROFILE_PRESETS = [
5896
6681
  {
5897
6682
  name: "linux-arm64",
@@ -5918,25 +6703,25 @@ var PLATFORM_PROFILE_PRESETS = [
5918
6703
  }
5919
6704
  }
5920
6705
  ];
5921
- function ensurePlatformProfiles(db) {
5922
- const configs = listConfigs(undefined, db);
6706
+ async function ensurePlatformProfiles(store = resolveConfigStore()) {
6707
+ const configs = await store.listConfigs();
5923
6708
  const ensured = [];
5924
6709
  for (const preset of PLATFORM_PROFILE_PRESETS) {
5925
6710
  let profile;
5926
6711
  try {
5927
- profile = getProfile(preset.name, db);
5928
- if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
5929
- 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, {
5930
6715
  description: profile.description ?? preset.description,
5931
- selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
6716
+ selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
5932
6717
  variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
5933
- }, db);
6718
+ });
5934
6719
  }
5935
6720
  } catch {
5936
- profile = createProfile(preset, db);
6721
+ profile = await store.createProfile(preset);
5937
6722
  }
5938
6723
  for (const config of configs) {
5939
- addConfigToProfile(profile.id, config.id, db);
6724
+ await store.addConfigToProfile(profile.id, config.id);
5940
6725
  }
5941
6726
  ensured.push(profile);
5942
6727
  }
@@ -5944,20 +6729,10 @@ function ensurePlatformProfiles(db) {
5944
6729
  }
5945
6730
 
5946
6731
  // src/status.ts
5947
- init_database();
5948
- init_configs();
5949
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
5950
-
5951
- // src/db/machines.ts
5952
- init_database();
5953
- function listMachines(db) {
5954
- const d = db || getDatabase();
5955
- return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
5956
- }
5957
-
5958
- // src/status.ts
6732
+ init_config_store();
5959
6733
  init_apply();
5960
6734
  init_redact();
6735
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
5961
6736
  var PACKAGE_NAME = "@hasna/instructions";
5962
6737
  var PACKAGE_VERSION = "0.3.0";
5963
6738
  function activeDatabaseEnv() {
@@ -5981,21 +6756,13 @@ function countBy(items, getValue) {
5981
6756
  }
5982
6757
  return counts;
5983
6758
  }
5984
- function tableCount(db, table) {
5985
- try {
5986
- const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
5987
- return Number(row?.count ?? 0);
5988
- } catch {
5989
- return 0;
5990
- }
5991
- }
5992
- function getConfigsStatus(db = getDatabase()) {
6759
+ async function getConfigsStatus(store = resolveConfigStore()) {
5993
6760
  let databaseReachable = true;
5994
6761
  let configs = [];
5995
6762
  let categoryStats = { total: 0 };
5996
6763
  try {
5997
- configs = listConfigs(undefined, db);
5998
- categoryStats = getConfigStats(db);
6764
+ configs = await store.listConfigs();
6765
+ categoryStats = await store.getConfigStats();
5999
6766
  } catch {
6000
6767
  databaseReachable = false;
6001
6768
  }
@@ -6020,10 +6787,25 @@ function getConfigsStatus(db = getDatabase()) {
6020
6787
  driftedTargets += 1;
6021
6788
  }
6022
6789
  }
6023
- const profiles = databaseReachable ? listProfiles(db).length : 0;
6024
- const machines = databaseReachable ? listMachines(db).length : 0;
6025
- const profileLinks = databaseReachable ? tableCount(db, "profile_configs") : 0;
6026
- 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
+ }
6027
6809
  const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
6028
6810
  const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
6029
6811
  return {
@@ -6077,425 +6859,8 @@ function getConfigsStatus(db = getDatabase()) {
6077
6859
  };
6078
6860
  }
6079
6861
 
6080
- // src/db/storage-sync.ts
6081
- init_database();
6082
-
6083
- // src/db/pg-migrations.ts
6084
- var PG_MIGRATIONS = [
6085
- `CREATE TABLE IF NOT EXISTS configs (
6086
- id TEXT PRIMARY KEY,
6087
- name TEXT NOT NULL,
6088
- slug TEXT NOT NULL UNIQUE,
6089
- kind TEXT NOT NULL DEFAULT 'file',
6090
- category TEXT NOT NULL,
6091
- agent TEXT NOT NULL DEFAULT 'global',
6092
- target_path TEXT,
6093
- outputs TEXT NOT NULL DEFAULT '[]',
6094
- format TEXT NOT NULL DEFAULT 'text',
6095
- content TEXT NOT NULL DEFAULT '',
6096
- description TEXT,
6097
- tags TEXT NOT NULL DEFAULT '[]',
6098
- is_template BOOLEAN NOT NULL DEFAULT FALSE,
6099
- version INTEGER NOT NULL DEFAULT 1,
6100
- created_at TEXT NOT NULL,
6101
- updated_at TEXT NOT NULL,
6102
- synced_at TEXT
6103
- )`,
6104
- `CREATE TABLE IF NOT EXISTS config_snapshots (
6105
- id TEXT PRIMARY KEY,
6106
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
6107
- content TEXT NOT NULL,
6108
- version INTEGER NOT NULL,
6109
- created_at TEXT NOT NULL
6110
- )`,
6111
- `CREATE TABLE IF NOT EXISTS profiles (
6112
- id TEXT PRIMARY KEY,
6113
- name TEXT NOT NULL,
6114
- slug TEXT NOT NULL UNIQUE,
6115
- description TEXT,
6116
- selectors TEXT NOT NULL DEFAULT '{}',
6117
- variables TEXT NOT NULL DEFAULT '{}',
6118
- created_at TEXT NOT NULL,
6119
- updated_at TEXT NOT NULL
6120
- )`,
6121
- `CREATE TABLE IF NOT EXISTS profile_configs (
6122
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
6123
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
6124
- sort_order INTEGER NOT NULL DEFAULT 0,
6125
- PRIMARY KEY (profile_id, config_id)
6126
- )`,
6127
- `CREATE TABLE IF NOT EXISTS machines (
6128
- id TEXT PRIMARY KEY,
6129
- hostname TEXT NOT NULL UNIQUE,
6130
- os TEXT,
6131
- arch TEXT,
6132
- last_applied_at TEXT,
6133
- created_at TEXT NOT NULL
6134
- )`,
6135
- `CREATE TABLE IF NOT EXISTS feedback (
6136
- id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
6137
- message TEXT NOT NULL,
6138
- email TEXT,
6139
- category TEXT DEFAULT 'general',
6140
- version TEXT,
6141
- machine_id TEXT,
6142
- created_at TEXT NOT NULL DEFAULT NOW()::text
6143
- )`,
6144
- `ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
6145
- ];
6146
-
6147
- // src/db/remote-storage.ts
6148
- import pg from "pg";
6149
- var DISABLED_SSL_MODE = "disable";
6150
- function translatePlaceholders(sql) {
6151
- let index = 0;
6152
- return sql.replace(/\?/g, () => `$${++index}`);
6153
- }
6154
- function normalizeParams(params) {
6155
- const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
6156
- return flat.map((value) => value === undefined ? null : value);
6157
- }
6158
- function normalizeHost(hostname) {
6159
- const stripped = hostname.replace(/^\[/, "").replace(/\]$/, "");
6160
- try {
6161
- return decodeURIComponent(stripped).toLowerCase();
6162
- } catch {
6163
- return stripped.toLowerCase();
6164
- }
6165
- }
6166
- function isLocalPostgresHost(hostname) {
6167
- const host = normalizeHost(hostname);
6168
- return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
6169
- }
6170
- function effectivePgHost(url) {
6171
- const hosts = url.searchParams.getAll("host");
6172
- const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
6173
- return finalHost?.trim() ? finalHost : url.hostname;
6174
- }
6175
- function buildPgPoolConfig(connectionString) {
6176
- let url;
6177
- try {
6178
- url = new URL(connectionString);
6179
- } catch {
6180
- throw new Error("Invalid PostgreSQL connection string");
6181
- }
6182
- const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
6183
- const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
6184
- const isLocal = isLocalPostgresHost(effectivePgHost(url));
6185
- const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
6186
- if (!isLocal && hasDisabledSsl) {
6187
- throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
6188
- }
6189
- const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
6190
- url.searchParams.delete("sslmode");
6191
- url.searchParams.delete("ssl");
6192
- return {
6193
- connectionString: url.toString(),
6194
- ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
6195
- };
6196
- }
6197
-
6198
- class PgAdapterAsync {
6199
- pool;
6200
- constructor(connectionString) {
6201
- this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
6202
- }
6203
- async run(sql, ...params) {
6204
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6205
- return { changes: result.rowCount ?? 0 };
6206
- }
6207
- async all(sql, ...params) {
6208
- const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
6209
- return result.rows;
6210
- }
6211
- async close() {
6212
- await this.pool.end();
6213
- }
6214
- }
6215
-
6216
- // src/db/storage-sync.ts
6217
- var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
6218
- var PRIMARY_KEYS = {
6219
- configs: ["id"],
6220
- config_snapshots: ["id"],
6221
- profiles: ["id"],
6222
- profile_configs: ["profile_id", "config_id"],
6223
- machines: ["id"],
6224
- feedback: ["id"]
6225
- };
6226
- var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
6227
- var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
6228
- var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
6229
- var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
6230
- var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
6231
- var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
6232
- function firstEnv(names) {
6233
- for (const name of names) {
6234
- const value = process.env[name];
6235
- if (value)
6236
- return value;
6237
- }
6238
- return null;
6239
- }
6240
- function normalizeStorageMode(value) {
6241
- const normalized = value?.trim().toLowerCase();
6242
- if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
6243
- return normalized;
6244
- return;
6245
- }
6246
- function getStorageDatabaseUrl() {
6247
- return firstEnv(STORAGE_DATABASE_ENV);
6248
- }
6249
- function getStorageMode() {
6250
- const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
6251
- if (mode)
6252
- return mode;
6253
- return getStorageDatabaseUrl() ? "hybrid" : "local";
6254
- }
6255
- async function getStoragePg() {
6256
- const url = getStorageDatabaseUrl();
6257
- if (!url)
6258
- throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
6259
- return new PgAdapterAsync(url);
6260
- }
6261
- async function runStorageMigrations(remote) {
6262
- await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
6263
- for (const sql of PG_MIGRATIONS)
6264
- await remote.run(sql);
6265
- }
6266
- async function storagePush(options) {
6267
- const remote = await getStoragePg();
6268
- const db = getDatabase();
6269
- try {
6270
- await runStorageMigrations(remote);
6271
- const results = [];
6272
- for (const table of resolveTables(options?.tables))
6273
- results.push(await pushTable(db, remote, table));
6274
- recordSyncMeta(db, "push", results);
6275
- return results;
6276
- } finally {
6277
- await remote.close();
6278
- }
6279
- }
6280
- async function storagePull(options) {
6281
- const remote = await getStoragePg();
6282
- const db = getDatabase();
6283
- try {
6284
- await runStorageMigrations(remote);
6285
- const results = [];
6286
- for (const table of resolveTables(options?.tables))
6287
- results.push(await pullTable(remote, db, table));
6288
- recordSyncMeta(db, "pull", results);
6289
- return results;
6290
- } finally {
6291
- await remote.close();
6292
- }
6293
- }
6294
- async function storageSync(options) {
6295
- const pull = await storagePull(options);
6296
- const push = await storagePush(options);
6297
- return { pull, push };
6298
- }
6299
- function getStorageSyncMetaAll() {
6300
- const db = getDatabase();
6301
- ensureSyncMetaTable(db);
6302
- return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
6303
- }
6304
- function getStorageStatus() {
6305
- return {
6306
- configured: Boolean(getStorageDatabaseUrl()),
6307
- mode: getStorageMode(),
6308
- env: STORAGE_DATABASE_ENV,
6309
- service: "configs",
6310
- tables: STORAGE_TABLES,
6311
- sync: getStorageSyncMetaAll()
6312
- };
6313
- }
6314
- function resolveTables(tables) {
6315
- if (!tables || tables.length === 0)
6316
- return [...STORAGE_TABLES];
6317
- const allowed = new Set(STORAGE_TABLES);
6318
- const requested = tables.map((table) => table.trim()).filter(Boolean);
6319
- const invalid = requested.filter((table) => !allowed.has(table));
6320
- if (invalid.length > 0)
6321
- throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
6322
- return requested;
6323
- }
6324
- async function pushTable(db, remote, table) {
6325
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6326
- try {
6327
- const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
6328
- result.rowsRead = rows.length;
6329
- if (rows.length === 0)
6330
- return result;
6331
- const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
6332
- result.rowsWritten = await upsertPg(remote, table, columns, rows);
6333
- } catch (error) {
6334
- result.errors.push(error instanceof Error ? error.message : String(error));
6335
- }
6336
- return result;
6337
- }
6338
- async function pullTable(remote, db, table) {
6339
- const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
6340
- try {
6341
- const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
6342
- result.rowsRead = rows.length;
6343
- if (rows.length === 0)
6344
- return result;
6345
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
6346
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
6347
- } catch (error) {
6348
- result.errors.push(error instanceof Error ? error.message : String(error));
6349
- }
6350
- return result;
6351
- }
6352
- async function filterRemoteColumns(remote, table, columns) {
6353
- const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
6354
- if (rows.length === 0)
6355
- return columns;
6356
- const allowed = new Set(rows.map((row) => row.column_name));
6357
- return columns.filter((column) => allowed.has(column));
6358
- }
6359
- function filterLocalColumns(db, table, columns) {
6360
- const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
6361
- const allowed = new Set(rows.map((row) => row.name));
6362
- return columns.filter((column) => allowed.has(column));
6363
- }
6364
- async function upsertPg(remote, table, columns, rows) {
6365
- if (columns.length === 0)
6366
- return 0;
6367
- const primaryKeys = PRIMARY_KEYS[table];
6368
- const columnList = columns.map(quoteIdent).join(", ");
6369
- const placeholders = columns.map(() => "?").join(", ");
6370
- const keyList = primaryKeys.map(quoteIdent).join(", ");
6371
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6372
- const fallbackKey = primaryKeys[0];
6373
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
6374
- for (const row of rows) {
6375
- await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
6376
- }
6377
- return rows.length;
6378
- }
6379
- function upsertSqlite(db, table, columns, rows) {
6380
- if (columns.length === 0)
6381
- return 0;
6382
- const primaryKeys = PRIMARY_KEYS[table];
6383
- const columnList = columns.map(quoteIdent).join(", ");
6384
- const placeholders = columns.map(() => "?").join(", ");
6385
- const keyList = primaryKeys.map(quoteIdent).join(", ");
6386
- const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
6387
- const fallbackKey = primaryKeys[0];
6388
- const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
6389
- const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
6390
- db.transaction((batch) => {
6391
- for (const row of batch)
6392
- statement.run(...columns.map((column) => coerceForSqlite(row[column])));
6393
- })(rows);
6394
- return rows.length;
6395
- }
6396
- function recordSyncMeta(db, direction, results) {
6397
- ensureSyncMetaTable(db);
6398
- const now3 = new Date().toISOString();
6399
- 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");
6400
- for (const result of results) {
6401
- if (result.errors.length > 0)
6402
- continue;
6403
- statement.run(result.table, now3, direction);
6404
- }
6405
- }
6406
- function ensureSyncMetaTable(db) {
6407
- 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))");
6408
- }
6409
- function quoteIdent(identifier) {
6410
- return `"${identifier.replace(/"/g, '""')}"`;
6411
- }
6412
- function coerceForSqlite(value) {
6413
- if (value === undefined || value === null)
6414
- return null;
6415
- if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
6416
- return value;
6417
- if (value instanceof Date)
6418
- return value.toISOString();
6419
- if (Buffer.isBuffer(value) || value instanceof Uint8Array)
6420
- return value;
6421
- if (typeof value === "object")
6422
- return JSON.stringify(value);
6423
- return String(value);
6424
- }
6425
-
6426
- // src/cli/storage.ts
6427
- function parseTables(value) {
6428
- if (!value)
6429
- return;
6430
- return value.split(",").map((table) => table.trim()).filter(Boolean);
6431
- }
6432
- function printJson(value) {
6433
- console.log(JSON.stringify(value, null, 2));
6434
- }
6435
- function printResults(results, label) {
6436
- const total = results.reduce((sum, result) => sum + result.rowsWritten, 0);
6437
- for (const result of results) {
6438
- const errors = result.errors.length > 0 ? ` (${result.errors.join("; ")})` : "";
6439
- console.log(` ${result.table}: ${result.rowsWritten}/${result.rowsRead} rows ${label}${errors}`);
6440
- }
6441
- console.log(`Done. ${total} rows ${label}.`);
6442
- }
6443
- function registerStorageCommands(program2) {
6444
- const storageCmd = program2.command("storage").description("Storage sync commands");
6445
- storageCmd.command("status").description("Show storage config and local sync state").option("--json", "Output as JSON").action((opts) => {
6446
- const info = getStorageStatus();
6447
- if (opts.json) {
6448
- printJson(info);
6449
- return;
6450
- }
6451
- console.log(`Storage configured: ${info.configured ? "yes" : "no"}`);
6452
- console.log(`Tables: ${info.tables.join(", ")}`);
6453
- if (info.sync.length === 0)
6454
- console.log("Sync: no local sync history");
6455
- for (const entry of info.sync)
6456
- console.log(` ${entry.table_name} ${entry.direction}: ${entry.last_synced_at ?? "never"}`);
6457
- });
6458
- 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) => {
6459
- try {
6460
- const results = await storagePush({ tables: parseTables(opts.tables) });
6461
- if (opts.json) {
6462
- printJson(results);
6463
- return;
6464
- }
6465
- printResults(results, "pushed");
6466
- } catch (error) {
6467
- console.error(error instanceof Error ? error.message : String(error));
6468
- process.exit(1);
6469
- }
6470
- });
6471
- 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) => {
6472
- try {
6473
- const results = await storagePull({ tables: parseTables(opts.tables) });
6474
- if (opts.json) {
6475
- printJson(results);
6476
- return;
6477
- }
6478
- printResults(results, "pulled");
6479
- } catch (error) {
6480
- console.error(error instanceof Error ? error.message : String(error));
6481
- process.exit(1);
6482
- }
6483
- });
6484
- storageCmd.command("sync").description("Bidirectional sync: pull then push").option("--tables <tables>", "Comma-separated table names").option("--json", "Output as JSON").action(async (opts) => {
6485
- try {
6486
- const result = await storageSync({ tables: parseTables(opts.tables) });
6487
- if (opts.json) {
6488
- printJson(result);
6489
- return;
6490
- }
6491
- printResults(result.pull, "pulled");
6492
- printResults(result.push, "pushed");
6493
- } catch (error) {
6494
- console.error(error instanceof Error ? error.message : String(error));
6495
- process.exit(1);
6496
- }
6497
- });
6498
- }
6862
+ // src/cli/index.tsx
6863
+ init_config_store();
6499
6864
 
6500
6865
  // src/lib/compact-output.ts
6501
6866
  var DEFAULT_LIST_LIMIT = 20;
@@ -6574,9 +6939,9 @@ function pageFooter(command, page, detailsHint) {
6574
6939
  function printConfigRows(configs) {
6575
6940
  console.log(`${pad("slug", 32)} ${pad("type", 15)} ${pad("fmt", 8)} ${pad("path", 44)} out v`);
6576
6941
  for (const c of configs) {
6577
- const type = `${c.category}/${c.agent}`;
6942
+ const type2 = `${c.category}/${c.agent}`;
6578
6943
  const path = c.kind === "reference" ? "(ref)" : c.target_path ?? "(no path)";
6579
- 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}`);
6580
6945
  }
6581
6946
  }
6582
6947
  function splitCsv(value) {
@@ -6611,11 +6976,11 @@ function parseSessionSource(value, order, replaceIds) {
6611
6976
  if (!path)
6612
6977
  throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
6613
6978
  const absPath = resolveSessionPath(path);
6614
- if (!existsSync12(absPath))
6979
+ if (!existsSync13(absPath))
6615
6980
  throw new Error(`Instruction source file not found: ${absPath}`);
6616
- const content = readFileSync8(absPath, "utf-8");
6981
+ const content = readFileSync9(absPath, "utf-8");
6617
6982
  const source = sourceFromFilePath(absPath, content, order);
6618
- const resolvedId = id || source.id || basename5(absPath);
6983
+ const resolvedId = id || source.id || basename6(absPath);
6619
6984
  return {
6620
6985
  ...source,
6621
6986
  id: resolvedId,
@@ -6640,18 +7005,18 @@ function parseLayeredReference(value) {
6640
7005
  throw new Error("Instruction reference cannot be empty.");
6641
7006
  return { id: trimmed };
6642
7007
  }
6643
- function collectSessionSources(opts, tool) {
7008
+ async function collectSessionSources(opts, tool, store) {
6644
7009
  const replaceIds = new Set(opts.replaceSource ?? []);
6645
7010
  const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index, replaceIds));
6646
7011
  for (const value of opts.config ?? []) {
6647
7012
  const { layer, id } = parseLayeredReference(value);
6648
- sources.push(sourceFromConfig(getConfig(id), sources.length, layer));
7013
+ sources.push(sourceFromConfig(await store.getConfig(id), sources.length, layer));
6649
7014
  }
6650
7015
  for (const value of opts.identityExport ?? []) {
6651
7016
  const path = resolveSessionPath(value);
6652
- if (!existsSync12(path))
7017
+ if (!existsSync13(path))
6653
7018
  throw new Error(`Identity instruction export not found: ${path}`);
6654
- const parsed = JSON.parse(readFileSync8(path, "utf-8"));
7019
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
6655
7020
  sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
6656
7021
  }
6657
7022
  return sources.map((source) => replaceIds.has(source.id) ? { ...source, merge: "replace" } : source);
@@ -6683,12 +7048,12 @@ function parseVarArgs(values) {
6683
7048
  function parseProfileSelectors(opts) {
6684
7049
  const selectors = {};
6685
7050
  const os = splitCsv(opts.os);
6686
- const arch = splitCsv(opts.arch);
7051
+ const arch2 = splitCsv(opts.arch);
6687
7052
  const hostnames = splitCsv(opts.hostname);
6688
7053
  if (os)
6689
7054
  selectors.os = os;
6690
- if (arch)
6691
- selectors.arch = arch;
7055
+ if (arch2)
7056
+ selectors.arch = arch2;
6692
7057
  if (hostnames)
6693
7058
  selectors.hostnames = hostnames;
6694
7059
  return Object.keys(selectors).length > 0 ? selectors : undefined;
@@ -6706,14 +7071,14 @@ function formatProfileSelectorSummary(profile) {
6706
7071
  function formatProfileVariables(profile) {
6707
7072
  return Object.entries(profile.variables).map(([key, value]) => `${key}=${value}`).join(", ");
6708
7073
  }
6709
- function getMachineProfileContext(opts) {
7074
+ async function getMachineProfileContext(opts, store) {
6710
7075
  const machine = detectMachineContext({ hostname: opts.hostname, os: opts.os, arch: opts.arch });
6711
- const profile = resolveProfileForMachine(machine);
7076
+ const profile = await store.resolveProfileForMachine(machine);
6712
7077
  return { machine, profile, vars: resolveProfileVariables(profile, machine) };
6713
7078
  }
6714
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) => {
6715
7080
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6716
- const configs = listConfigs({
7081
+ const configs = await resolveConfigStore().listConfigs({
6717
7082
  category: opts.category,
6718
7083
  agent: opts.agent,
6719
7084
  kind: opts.kind,
@@ -6741,7 +7106,7 @@ program.command("list").alias("ls").description("List stored configs").option("-
6741
7106
  });
6742
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) => {
6743
7108
  try {
6744
- const c = getConfig(id);
7109
+ const c = await resolveConfigStore().getConfig(id);
6745
7110
  if (opts.format === "json") {
6746
7111
  console.log(JSON.stringify(c, null, 2));
6747
7112
  return;
@@ -6761,17 +7126,17 @@ program.command("show <id>").alias("inspect").description("Show a config's conte
6761
7126
  }
6762
7127
  });
6763
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) => {
6764
- const abs = resolve6(filePath);
6765
- if (!existsSync12(abs)) {
7129
+ const abs = resolve7(filePath);
7130
+ if (!existsSync13(abs)) {
6766
7131
  console.error(chalk.red(`File not found: ${abs}`));
6767
7132
  process.exit(1);
6768
7133
  }
6769
- const rawContent = readFileSync8(abs, "utf-8");
7134
+ const rawContent = readFileSync9(abs, "utf-8");
6770
7135
  const fmt = detectFormat(abs);
6771
7136
  const { content, redacted, isTemplate: isTemplate2 } = redactContent(rawContent, fmt);
6772
- const targetPath = abs.startsWith(homedir6()) ? abs.replace(homedir6(), "~") : abs;
7137
+ const targetPath = abs.startsWith(homedir7()) ? abs.replace(homedir7(), "~") : abs;
6773
7138
  const name = opts.name || filePath.split("/").pop();
6774
- const config = createConfig({
7139
+ const config = await resolveConfigStore().createConfig({
6775
7140
  name,
6776
7141
  kind: opts.kind ?? "file",
6777
7142
  category: opts.category ?? detectCategory(abs),
@@ -6789,10 +7154,26 @@ program.command("add <path>").description("Ingest a file into the config DB").op
6789
7154
  console.log(chalk.dim(" Config stored as a template. Use `configs template vars` to see placeholders."));
6790
7155
  }
6791
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
+ });
6792
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) => {
6793
7173
  try {
6794
- const config = getConfig(id);
6795
- 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 });
6796
7177
  const status = opts.dryRun ? chalk.yellow("[dry-run]") : result.changed ? chalk.green("\u2713") : chalk.dim("=");
6797
7178
  const change = result.changed ? "changed" : "unchanged";
6798
7179
  console.log(`${status} ${result.path} ${chalk.dim(`(${change})`)}`);
@@ -6808,17 +7189,18 @@ program.command("apply <id>").description("Apply a config to its target_path and
6808
7189
  });
6809
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) => {
6810
7191
  try {
7192
+ const store = resolveConfigStore();
6811
7193
  if (id) {
6812
- const config = getConfig(id);
6813
- console.log(diffConfig(config));
7194
+ const config = await store.getConfig(id);
7195
+ console.log(await diffConfig(config, { store }));
6814
7196
  return;
6815
7197
  }
6816
- const configs = listConfigs({ kind: "file" });
7198
+ const configs = await store.listConfigs({ kind: "file" });
6817
7199
  let drifted = 0;
6818
7200
  for (const c of configs) {
6819
7201
  if (!c.target_path)
6820
7202
  continue;
6821
- const diff = diffConfig(c);
7203
+ const diff = await diffConfig(c, { store });
6822
7204
  if (diff.includes("no diff") || diff.includes("not found"))
6823
7205
  continue;
6824
7206
  drifted++;
@@ -6833,6 +7215,7 @@ program.command("diff [id]").description("Show diff between stored config and di
6833
7215
  }
6834
7216
  });
6835
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();
6836
7219
  if (opts.list) {
6837
7220
  const targets = KNOWN_CONFIGS.filter((k) => {
6838
7221
  if (opts.agent && k.agent !== opts.agent)
@@ -6853,18 +7236,18 @@ program.command("sync").description("Sync known AI coding configs from disk into
6853
7236
  if (opts.project) {
6854
7237
  const dir = typeof opts.project === "string" ? opts.project : process.cwd();
6855
7238
  if (opts.all) {
6856
- const { readdirSync: readdirSync3, statSync: st } = await import("fs");
7239
+ const { readdirSync: readdirSync4, statSync: st } = await import("fs");
6857
7240
  const absDir = expandPath(dir);
6858
- const entries = readdirSync3(absDir, { withFileTypes: true });
7241
+ const entries = readdirSync4(absDir, { withFileTypes: true });
6859
7242
  let totalAdded = 0, totalUpdated = 0, totalUnchanged = 0, projects = 0;
6860
7243
  for (const entry of entries) {
6861
7244
  if (!entry.isDirectory())
6862
7245
  continue;
6863
- const projDir = join10(absDir, entry.name);
6864
- 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"));
6865
7248
  if (!hasClaude)
6866
7249
  continue;
6867
- const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun });
7250
+ const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
6868
7251
  if (result2.added + result2.updated > 0) {
6869
7252
  console.log(` ${chalk.green("\u2713")} ${entry.name}: +${result2.added} updated:${result2.updated}`);
6870
7253
  }
@@ -6876,15 +7259,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
6876
7259
  console.log(chalk.green("\u2713") + ` Synced ${projects} projects: +${totalAdded} updated:${totalUpdated} unchanged:${totalUnchanged}`);
6877
7260
  return;
6878
7261
  }
6879
- const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun });
7262
+ const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun, store });
6880
7263
  console.log(chalk.green("\u2713") + ` Project sync: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
6881
7264
  return;
6882
7265
  }
6883
7266
  if (opts.toDisk) {
6884
- 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 });
6885
7268
  console.log(chalk.green("\u2713") + ` Written to disk: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
6886
7269
  } else {
6887
- 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 });
6888
7271
  console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
6889
7272
  if (result.skipped.length > 0) {
6890
7273
  console.log(chalk.dim(" skipped (not found): " + result.skipped.join(", ")));
@@ -6893,13 +7276,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
6893
7276
  });
6894
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) => {
6895
7278
  const result = await exportConfigs(opts.output, {
6896
- filter: opts.category ? { category: opts.category } : undefined
7279
+ filter: opts.category ? { category: opts.category } : undefined,
7280
+ store: resolveConfigStore()
6897
7281
  });
6898
7282
  console.log(chalk.green("\u2713") + ` Exported ${result.count} configs to ${result.path}`);
6899
7283
  });
6900
7284
  program.command("import <file>").description("Import configs from a tar.gz bundle").option("--overwrite", "overwrite existing configs").action(async (file, opts) => {
6901
7285
  const result = await importConfigs(file, {
6902
- conflict: opts.overwrite ? "overwrite" : "skip"
7286
+ conflict: opts.overwrite ? "overwrite" : "skip",
7287
+ store: resolveConfigStore()
6903
7288
  });
6904
7289
  console.log(chalk.green("\u2713") + ` Import complete: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
6905
7290
  if (result.errors.length > 0) {
@@ -6909,10 +7294,11 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
6909
7294
  }
6910
7295
  });
6911
7296
  program.command("whoami").description("Show setup summary").action(async () => {
6912
- const dbPath = process.env["CONFIGS_DB_PATH"] || join10(homedir6(), ".hasna", "configs", "configs.db");
6913
- 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();
6914
7300
  console.log(chalk.bold("@hasna/configs") + chalk.dim(" v" + pkg.version));
6915
- console.log(chalk.cyan("DB:") + " " + dbPath);
7301
+ console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
6916
7302
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
6917
7303
  console.log();
6918
7304
  console.log(chalk.bold("By category:"));
@@ -6922,7 +7308,7 @@ program.command("whoami").description("Show setup summary").action(async () => {
6922
7308
  if (count > 0)
6923
7309
  console.log(` ${chalk.cyan(cat.padEnd(16))} ${count}`);
6924
7310
  }
6925
- const profiles = listProfiles();
7311
+ const profiles = await store.listProfiles();
6926
7312
  if (profiles.length > 0) {
6927
7313
  console.log();
6928
7314
  console.log(chalk.bold("Profiles:") + chalk.dim(` (${profiles.length})`));
@@ -6933,7 +7319,8 @@ program.command("whoami").description("Show setup summary").action(async () => {
6933
7319
  var profileCmd = program.command("profile").description("Manage config profiles (named bundles)");
6934
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) => {
6935
7321
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6936
- const profiles = listProfiles();
7322
+ const store = resolveConfigStore();
7323
+ const profiles = await store.listProfiles();
6937
7324
  if (profiles.length === 0) {
6938
7325
  console.log(chalk.dim("No profiles."));
6939
7326
  return;
@@ -6948,10 +7335,10 @@ profileCmd.command("list").description("List all profiles").option("--brief", "c
6948
7335
  for (const p of page.items) {
6949
7336
  if (fmt === "compact") {
6950
7337
  const selectorSummary2 = formatProfileSelectorSummary(p);
6951
- 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}`);
6952
7339
  continue;
6953
7340
  }
6954
- const configs = getProfileConfigs(p.id);
7341
+ const configs = await store.getProfileConfigs(p.id);
6955
7342
  console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configs.length} config(s)`);
6956
7343
  if (p.description)
6957
7344
  console.log(` ${chalk.dim(p.description)}`);
@@ -6965,7 +7352,7 @@ profileCmd.command("list").description("List all profiles").option("--brief", "c
6965
7352
  pageFooter("configs profile list", page, "Use --verbose for expanded rows, --json for full records, or `configs profile show <slug>` for details.");
6966
7353
  });
6967
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) => {
6968
- const p = createProfile({
7355
+ const p = await resolveConfigStore().createProfile({
6969
7356
  name,
6970
7357
  description: opts.description,
6971
7358
  selectors: parseProfileSelectors(opts),
@@ -6975,8 +7362,9 @@ profileCmd.command("create <name>").description("Create a new profile").option("
6975
7362
  });
6976
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) => {
6977
7364
  try {
6978
- const p = getProfile(id);
6979
- const configs = getProfileConfigs(id);
7365
+ const store = resolveConfigStore();
7366
+ const p = await store.getProfile(id);
7367
+ const configs = await store.getProfileConfigs(id);
6980
7368
  console.log(chalk.bold(p.name) + chalk.dim(` (${p.slug})`));
6981
7369
  if (p.description)
6982
7370
  console.log(chalk.dim(p.description));
@@ -7000,8 +7388,9 @@ profileCmd.command("show <id>").description("Show profile and its configs").opti
7000
7388
  });
7001
7389
  profileCmd.command("add <profile> <config>").description("Add a config to a profile").action(async (profile, config) => {
7002
7390
  try {
7003
- const c = getConfig(config);
7004
- addConfigToProfile(profile, c.id);
7391
+ const store = resolveConfigStore();
7392
+ const c = await store.getConfig(config);
7393
+ await store.addConfigToProfile(profile, c.id);
7005
7394
  console.log(chalk.green("\u2713") + ` Added ${c.slug} to profile ${profile}`);
7006
7395
  } catch (e) {
7007
7396
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7010,8 +7399,9 @@ profileCmd.command("add <profile> <config>").description("Add a config to a prof
7010
7399
  });
7011
7400
  profileCmd.command("remove <profile> <config>").description("Remove a config from a profile").action(async (profile, config) => {
7012
7401
  try {
7013
- const c = getConfig(config);
7014
- removeConfigFromProfile(profile, c.id);
7402
+ const store = resolveConfigStore();
7403
+ const c = await store.getConfig(config);
7404
+ await store.removeConfigFromProfile(profile, c.id);
7015
7405
  console.log(chalk.green("\u2713") + ` Removed ${c.slug} from profile ${profile}`);
7016
7406
  } catch (e) {
7017
7407
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7020,15 +7410,16 @@ profileCmd.command("remove <profile> <config>").description("Remove a config fro
7020
7410
  });
7021
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) => {
7022
7412
  try {
7023
- const { machine, profile } = getMachineProfileContext(opts);
7024
- 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;
7025
7416
  if (!selected) {
7026
7417
  console.error(chalk.red(opts.auto ? "No matching machine-aware profile found." : "Provide a profile id or use --auto."));
7027
7418
  process.exit(1);
7028
7419
  }
7029
- const configs = getProfileConfigs(selected.id);
7420
+ const configs = await store.getProfileConfigs(selected.id);
7030
7421
  const vars = resolveProfileVariables(selected, machine);
7031
- const results = await applyConfigs(configs, { dryRun: opts.dryRun, vars });
7422
+ const results = await applyConfigs(configs, { dryRun: opts.dryRun, vars, store });
7032
7423
  let changed = 0;
7033
7424
  for (const r of results) {
7034
7425
  const status = opts.dryRun ? chalk.yellow("[dry-run]") : r.changed ? chalk.green("\u2713") : chalk.dim("=");
@@ -7044,7 +7435,8 @@ ${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${
7044
7435
  }
7045
7436
  });
7046
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) => {
7047
- const { machine, profile, vars } = getMachineProfileContext(opts);
7438
+ const store = resolveConfigStore();
7439
+ const { machine, profile, vars } = await getMachineProfileContext(opts, store);
7048
7440
  if (!profile) {
7049
7441
  console.log(chalk.yellow(`No matching profile for ${machine.hostname} ${machine.os_family}/${machine.arch}`));
7050
7442
  process.exit(1);
@@ -7061,8 +7453,9 @@ profileCmd.command("resolve").description("Resolve the matching machine-aware pr
7061
7453
  });
7062
7454
  profileCmd.command("delete <id>").description("Delete a profile").action(async (id) => {
7063
7455
  try {
7064
- const p = getProfile(id);
7065
- deleteProfile(id);
7456
+ const store = resolveConfigStore();
7457
+ const p = await store.getProfile(id);
7458
+ await store.deleteProfile(p.id);
7066
7459
  console.log(chalk.green("\u2713") + ` Deleted profile: ${p.name}`);
7067
7460
  } catch (e) {
7068
7461
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7077,7 +7470,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
7077
7470
  console.error(chalk.red(`Unsupported tool: ${opts.tool}`));
7078
7471
  process.exit(1);
7079
7472
  }
7080
- const sources = collectSessionSources(opts, tool);
7473
+ const sources = await collectSessionSources(opts, tool, resolveConfigStore());
7081
7474
  const plan = planSessionRender({
7082
7475
  tool,
7083
7476
  profile: opts.profile,
@@ -7121,7 +7514,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7121
7514
  console.error(chalk.red(`Unsupported tool: ${opts.tool}`));
7122
7515
  process.exit(1);
7123
7516
  }
7124
- const sources = collectSessionSources(opts, tool);
7517
+ const sources = await collectSessionSources(opts, tool, resolveConfigStore());
7125
7518
  const plan = planSessionRender({
7126
7519
  tool,
7127
7520
  profile: opts.profile,
@@ -7168,8 +7561,9 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7168
7561
  var snapshotCmd = program.command("snapshot").description("Manage config version history");
7169
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) => {
7170
7563
  try {
7171
- const c = getConfig(configId);
7172
- const snaps = listSnapshots(c.id);
7564
+ const store = resolveConfigStore();
7565
+ const c = await store.getConfig(configId);
7566
+ const snaps = await store.listSnapshots(c.id);
7173
7567
  if (snaps.length === 0) {
7174
7568
  console.log(chalk.dim("No snapshots."));
7175
7569
  return;
@@ -7185,7 +7579,7 @@ snapshotCmd.command("list <config>").description("List snapshots for a config").
7185
7579
  }
7186
7580
  });
7187
7581
  snapshotCmd.command("show <id>").description("Show a snapshot's content").action(async (id) => {
7188
- const snap = getSnapshot(id);
7582
+ const snap = await resolveConfigStore().getSnapshot(id);
7189
7583
  if (!snap) {
7190
7584
  console.error(chalk.red("Snapshot not found: " + id));
7191
7585
  process.exit(1);
@@ -7194,12 +7588,13 @@ snapshotCmd.command("show <id>").description("Show a snapshot's content").action
7194
7588
  });
7195
7589
  snapshotCmd.command("restore <config> <snapshot-id>").description("Restore a config to a snapshot version").action(async (configId, snapId) => {
7196
7590
  try {
7197
- const snap = getSnapshot(snapId);
7591
+ const store = resolveConfigStore();
7592
+ const snap = await store.getSnapshot(snapId);
7198
7593
  if (!snap) {
7199
7594
  console.error(chalk.red("Snapshot not found: " + snapId));
7200
7595
  process.exit(1);
7201
7596
  }
7202
- updateConfig(configId, { content: snap.content });
7597
+ await store.updateConfig(configId, { content: snap.content });
7203
7598
  console.log(chalk.green("\u2713") + ` Restored ${configId} to snapshot v${snap.version}`);
7204
7599
  } catch (e) {
7205
7600
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7209,7 +7604,7 @@ snapshotCmd.command("restore <config> <snapshot-id>").description("Restore a con
7209
7604
  var templateCmd = program.command("template").description("Work with template configs");
7210
7605
  templateCmd.command("vars <id>").description("Show template variables").action(async (id) => {
7211
7606
  try {
7212
- const c = getConfig(id);
7607
+ const c = await resolveConfigStore().getConfig(id);
7213
7608
  const vars = extractTemplateVars(c.content);
7214
7609
  if (vars.length === 0) {
7215
7610
  console.log(chalk.dim("No template variables found."));
@@ -7226,7 +7621,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
7226
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) => {
7227
7622
  try {
7228
7623
  const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
7229
- const c = getConfig(id);
7624
+ const c = await resolveConfigStore().getConfig(id);
7230
7625
  const vars = {};
7231
7626
  if (opts.var) {
7232
7627
  for (const kv of opts.var) {
@@ -7257,9 +7652,9 @@ templateCmd.command("render <id>").description("Render a template config with va
7257
7652
  console.log(rendered);
7258
7653
  } else {
7259
7654
  const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync6 } = await import("fs");
7260
- const { dirname: dirname4 } = await import("path");
7655
+ const { dirname: dirname5 } = await import("path");
7261
7656
  const path = expandPath(c.target_path);
7262
- mkdirSync6(dirname4(path), { recursive: true });
7657
+ mkdirSync6(dirname5(path), { recursive: true });
7263
7658
  writeFileSync4(path, rendered, "utf-8");
7264
7659
  console.log(chalk.green("\u2713") + ` Rendered and applied to ${path}`);
7265
7660
  }
@@ -7272,11 +7667,12 @@ templateCmd.command("render <id>").description("Render a template config with va
7272
7667
  }
7273
7668
  });
7274
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();
7275
7671
  let configs;
7276
7672
  if (id) {
7277
- configs = [getConfig(id)];
7673
+ configs = [await store.getConfig(id)];
7278
7674
  } else if (opts.all) {
7279
- 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" });
7280
7676
  } else {
7281
7677
  const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
7282
7678
  const slugs = [
@@ -7285,10 +7681,10 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7285
7681
  const fetched = [];
7286
7682
  for (const slug2 of slugs) {
7287
7683
  try {
7288
- fetched.push(getConfig(slug2));
7684
+ fetched.push(await store.getConfig(slug2));
7289
7685
  } catch {}
7290
7686
  }
7291
- const rules = listConfigs({ category: "rules", agent: "claude" });
7687
+ const rules = await store.listConfigs({ category: "rules", agent: "claude" });
7292
7688
  for (const r of rules)
7293
7689
  if (!fetched.find((c) => c.id === r.id))
7294
7690
  fetched.push(r);
@@ -7318,7 +7714,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7318
7714
  }
7319
7715
  if (opts.fix) {
7320
7716
  const { content, isTemplate: isTemplate2 } = redactContent(c.content, fmt);
7321
- updateConfig(c.id, { content, is_template: isTemplate2 });
7717
+ await store.updateConfig(c.id, { content, is_template: isTemplate2 });
7322
7718
  if (visible.length > 0)
7323
7719
  console.log(chalk.green(" \u2713 Redacted."));
7324
7720
  }
@@ -7337,6 +7733,32 @@ Run with --fix to redact in-place.`));
7337
7733
  Redacted all ${total} finding(s); printed ${printed}. Re-run without --fix and a higher --limit for full details.`));
7338
7734
  }
7339
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
+ });
7340
7762
  var mcpCmd = program.command("mcp").description("Install/remove MCP server for AI agents");
7341
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) => {
7342
7764
  const targets = opts.all ? ["claude", "codex", "gemini"] : [
@@ -7350,7 +7772,7 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
7350
7772
  }
7351
7773
  for (const target of targets) {
7352
7774
  try {
7353
- const { vars } = getMachineProfileContext({});
7775
+ const { vars } = await getMachineProfileContext({}, resolveConfigStore());
7354
7776
  const mcpBinary = `${vars["BUN_BIN_DIR"]}/configs-mcp`;
7355
7777
  if (target === "claude") {
7356
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];
@@ -7360,14 +7782,14 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
7360
7782
  } else if (target === "codex") {
7361
7783
  const { appendFileSync, existsSync: ex } = await import("fs");
7362
7784
  const { join: j } = await import("path");
7363
- const configPath = j(homedir6(), ".codex", "config.toml");
7785
+ const configPath = j(homedir7(), ".codex", "config.toml");
7364
7786
  const block = `
7365
7787
  [mcp_servers.configs]
7366
7788
  command = "${mcpBinary}"
7367
7789
  args = []
7368
7790
  `;
7369
7791
  if (ex(configPath)) {
7370
- const content = readFileSync8(configPath, "utf-8");
7792
+ const content = readFileSync9(configPath, "utf-8");
7371
7793
  if (content.includes("[mcp_servers.configs]")) {
7372
7794
  console.log(chalk.dim("= Already installed in Codex"));
7373
7795
  continue;
@@ -7378,7 +7800,7 @@ args = []
7378
7800
  } else if (target === "gemini") {
7379
7801
  const { readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
7380
7802
  const { join: j } = await import("path");
7381
- const configPath = j(homedir6(), ".gemini", "settings.json");
7803
+ const configPath = j(homedir7(), ".gemini", "settings.json");
7382
7804
  let settings = {};
7383
7805
  if (ex(configPath)) {
7384
7806
  try {
@@ -7405,16 +7827,14 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
7405
7827
  }
7406
7828
  });
7407
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) => {
7408
- const dbPath = join10(homedir6(), ".hasna", "configs", "configs.db");
7409
- if (opts.force && existsSync12(dbPath)) {
7410
- const { rmSync: rmSync4 } = await import("fs");
7411
- rmSync4(dbPath);
7412
- console.log(chalk.dim("Deleted existing DB."));
7413
- resetDatabase();
7830
+ const store = resolveConfigStore();
7831
+ if (opts.force) {
7832
+ await store.reset();
7833
+ console.log(chalk.dim("Reset local store."));
7414
7834
  }
7415
7835
  console.log(chalk.bold(`@hasna/configs \u2014 initializing
7416
7836
  `));
7417
- const result = await syncKnown({});
7837
+ const result = await syncKnown({ store });
7418
7838
  console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
7419
7839
  if (result.skipped.length > 0) {
7420
7840
  console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
@@ -7432,35 +7852,36 @@ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`,
7432
7852
  ];
7433
7853
  for (const ref of refs) {
7434
7854
  try {
7435
- getConfig(ref.slug);
7855
+ await store.getConfig(ref.slug);
7436
7856
  } catch {
7437
- 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 });
7438
7858
  }
7439
7859
  }
7440
- ensureProjectDashboardStandardConfig();
7860
+ await ensureProjectDashboardStandardConfig(store);
7441
7861
  try {
7442
- getProfile("my-setup");
7862
+ await store.getProfile("my-setup");
7443
7863
  } catch {
7444
- const p = createProfile({ name: "my-setup", description: "Default profile with all known configs" });
7445
- 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();
7446
7866
  for (const c of allConfigs)
7447
- addConfigToProfile(p.id, c.id);
7867
+ await store.addConfigToProfile(p.id, c.id);
7448
7868
  console.log(chalk.green("\u2713") + ` Created profile "my-setup" with ${allConfigs.length} configs`);
7449
7869
  }
7450
- const machineProfiles = ensurePlatformProfiles();
7870
+ const machineProfiles = await ensurePlatformProfiles(store);
7451
7871
  console.log(chalk.green("\u2713") + ` Ensured ${machineProfiles.length} machine-aware profile(s)`);
7452
- const stats = getConfigStats();
7872
+ const stats = await store.getConfigStats();
7453
7873
  console.log(chalk.bold(`
7454
7874
  DB stats:`));
7455
7875
  for (const [key, count] of Object.entries(stats)) {
7456
7876
  if (count > 0)
7457
7877
  console.log(` ${key.padEnd(18)} ${count}`);
7458
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");
7459
7880
  console.log(chalk.dim(`
7460
- DB: ${dbPath}`));
7881
+ ${isCloudMode() ? "API" : "DB"}: ${location}`));
7461
7882
  });
7462
7883
  program.command("status").description("Health check: total configs, drift from disk, unredacted secrets").option("--json", "output metadata-only JSON").action(async (opts) => {
7463
- const status = getConfigsStatus();
7884
+ const status = await getConfigsStatus(resolveConfigStore());
7464
7885
  if (opts.json) {
7465
7886
  console.log(JSON.stringify(status, null, 2));
7466
7887
  return;
@@ -7476,17 +7897,17 @@ program.command("status").description("Health check: total configs, drift from d
7476
7897
  });
7477
7898
  program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
7478
7899
  const { mkdirSync: mk } = await import("fs");
7479
- const backupDir = join10(homedir6(), ".hasna", "configs", "backups");
7900
+ const backupDir = join11(homedir7(), ".hasna", "configs", "backups");
7480
7901
  mk(backupDir, { recursive: true });
7481
7902
  const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "-").slice(0, 19);
7482
- const outPath = join10(backupDir, `configs-${ts}.tar.gz`);
7483
- const result = await exportConfigs(outPath);
7903
+ const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
7904
+ const result = await exportConfigs(outPath, { store: resolveConfigStore() });
7484
7905
  const { statSync: st } = await import("fs");
7485
7906
  const size = st(outPath).size;
7486
7907
  console.log(chalk.green("\u2713") + ` Backup: ${result.count} configs \u2192 ${outPath} (${(size / 1024).toFixed(1)}KB)`);
7487
7908
  });
7488
7909
  program.command("restore <file>").description("Restore configs from a backup file").option("--overwrite", "overwrite existing configs (default: skip)").action(async (file, opts) => {
7489
- const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip" });
7910
+ const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip", store: resolveConfigStore() });
7490
7911
  console.log(chalk.green("\u2713") + ` Restored: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
7491
7912
  if (result.errors.length > 0) {
7492
7913
  for (const e of result.errors)
@@ -7494,6 +7915,7 @@ program.command("restore <file>").description("Restore configs from a backup fil
7494
7915
  }
7495
7916
  });
7496
7917
  program.command("doctor").description("Validate configs: syntax, permissions, missing files, secrets").action(async () => {
7918
+ const store = resolveConfigStore();
7497
7919
  let issues = 0;
7498
7920
  const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
7499
7921
  const fail = (msg) => {
@@ -7506,12 +7928,12 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
7506
7928
  console.log(chalk.cyan("Known files on disk:"));
7507
7929
  for (const k of KNOWN_CONFIGS) {
7508
7930
  if (k.rulesDir) {
7509
- 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`);
7510
7932
  } else {
7511
- 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`);
7512
7934
  }
7513
7935
  }
7514
- const allConfigs = listConfigs();
7936
+ const allConfigs = await store.listConfigs();
7515
7937
  console.log(chalk.cyan(`
7516
7938
  Stored configs (${allConfigs.length}):`));
7517
7939
  let validCount = 0;
@@ -7581,8 +8003,9 @@ complete -F _configs_completions configs`);
7581
8003
  });
7582
8004
  program.command("compare <a> <b>").description("Diff two stored configs against each other").action(async (a, b) => {
7583
8005
  try {
7584
- const configA = getConfig(a);
7585
- const configB = getConfig(b);
8006
+ const store = resolveConfigStore();
8007
+ const configA = await store.getConfig(a);
8008
+ const configB = await store.getConfig(b);
7586
8009
  console.log(chalk.bold(`${configA.slug}`) + chalk.dim(` (${configA.category}/${configA.agent})`));
7587
8010
  console.log(chalk.bold(`${configB.slug}`) + chalk.dim(` (${configB.category}/${configB.agent})`));
7588
8011
  console.log();
@@ -7621,6 +8044,7 @@ ${diffs} difference(s)`));
7621
8044
  }
7622
8045
  });
7623
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();
7624
8048
  const interval = Number(opts.interval);
7625
8049
  const { statSync: st } = await import("fs");
7626
8050
  const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
@@ -7631,16 +8055,16 @@ program.command("watch").description("Watch known config files for changes and a
7631
8055
  for (const k of KNOWN_CONFIGS) {
7632
8056
  if (k.rulesDir) {
7633
8057
  const absDir = expandPath2(k.rulesDir);
7634
- if (!existsSync12(absDir))
8058
+ if (!existsSync13(absDir))
7635
8059
  continue;
7636
- const { readdirSync: readdirSync3 } = await import("fs");
7637
- for (const f of readdirSync3(absDir).filter((f2) => f2.endsWith(".md"))) {
7638
- 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);
7639
8063
  mtimes.set(abs, st(abs).mtimeMs);
7640
8064
  }
7641
8065
  } else {
7642
8066
  const abs = expandPath2(k.path);
7643
- if (existsSync12(abs))
8067
+ if (existsSync13(abs))
7644
8068
  mtimes.set(abs, st(abs).mtimeMs);
7645
8069
  }
7646
8070
  }
@@ -7648,7 +8072,7 @@ program.command("watch").description("Watch known config files for changes and a
7648
8072
  const tick = async () => {
7649
8073
  let changed = 0;
7650
8074
  for (const [abs, oldMtime] of mtimes) {
7651
- if (!existsSync12(abs))
8075
+ if (!existsSync13(abs))
7652
8076
  continue;
7653
8077
  const newMtime = st(abs).mtimeMs;
7654
8078
  if (newMtime !== oldMtime) {
@@ -7660,10 +8084,10 @@ program.command("watch").description("Watch known config files for changes and a
7660
8084
  for (const k of KNOWN_CONFIGS) {
7661
8085
  if (k.rulesDir) {
7662
8086
  const absDir = expandPath2(k.rulesDir);
7663
- if (!existsSync12(absDir))
8087
+ if (!existsSync13(absDir))
7664
8088
  continue;
7665
8089
  for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
7666
- const abs = join10(absDir, f);
8090
+ const abs = join11(absDir, f);
7667
8091
  if (!mtimes.has(abs)) {
7668
8092
  mtimes.set(abs, st(abs).mtimeMs);
7669
8093
  changed++;
@@ -7671,14 +8095,14 @@ program.command("watch").description("Watch known config files for changes and a
7671
8095
  }
7672
8096
  } else {
7673
8097
  const abs = expandPath2(k.path);
7674
- if (existsSync12(abs) && !mtimes.has(abs)) {
8098
+ if (existsSync13(abs) && !mtimes.has(abs)) {
7675
8099
  mtimes.set(abs, st(abs).mtimeMs);
7676
8100
  changed++;
7677
8101
  }
7678
8102
  }
7679
8103
  }
7680
8104
  if (changed > 0) {
7681
- const result = await syncKnown({});
8105
+ const result = await syncKnown({ store });
7682
8106
  const ts = new Date().toLocaleTimeString();
7683
8107
  console.log(`${chalk.dim(ts)} ${chalk.green("\u2713")} ${changed} file(s) changed/new \u2192 synced +${result.added} updated:${result.updated}`);
7684
8108
  }
@@ -7687,22 +8111,23 @@ program.command("watch").description("Watch known config files for changes and a
7687
8111
  await new Promise(() => {});
7688
8112
  });
7689
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 () => {
7690
- const stats = getConfigStats();
7691
- const allConfigs = listConfigs();
8114
+ const store = resolveConfigStore();
8115
+ const stats = await store.getConfigStats();
8116
+ const allConfigs = await store.listConfigs();
7692
8117
  const fileConfigs = allConfigs.filter((c) => c.kind === "file");
7693
8118
  const refConfigs = allConfigs.filter((c) => c.kind === "reference");
7694
8119
  const templates = allConfigs.filter((c) => c.is_template);
7695
- const profiles = listProfiles();
8120
+ const profiles = await store.listProfiles();
7696
8121
  let drifted = 0, missing = 0;
7697
8122
  for (const c of fileConfigs) {
7698
8123
  if (!c.target_path)
7699
8124
  continue;
7700
8125
  const abs = expandPath(c.target_path);
7701
- if (!existsSync12(abs)) {
8126
+ if (!existsSync13(abs)) {
7702
8127
  missing++;
7703
8128
  continue;
7704
8129
  }
7705
- const disk = readFileSync8(abs, "utf-8");
8130
+ const disk = readFileSync9(abs, "utf-8");
7706
8131
  const { content: redactedDisk } = redactContent(disk, c.format);
7707
8132
  if (redactedDisk !== c.content)
7708
8133
  drifted++;
@@ -7734,7 +8159,8 @@ program.command("report").description("Summary of stored configs, drift, and eco
7734
8159
  }
7735
8160
  });
7736
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) => {
7737
- const configs = listConfigs({ kind: "file" });
8162
+ const store = resolveConfigStore();
8163
+ const configs = await store.listConfigs({ kind: "file" });
7738
8164
  let removed = 0;
7739
8165
  let printed = 0;
7740
8166
  const maxPrinted = parseLimit(opts.limit, DEFAULT_LIST_LIMIT);
@@ -7742,7 +8168,7 @@ program.command("clean").description("Remove configs from DB whose target files
7742
8168
  if (!c.target_path)
7743
8169
  continue;
7744
8170
  const abs = expandPath(c.target_path);
7745
- if (!existsSync12(abs)) {
8171
+ if (!existsSync13(abs)) {
7746
8172
  if (printed < maxPrinted) {
7747
8173
  if (opts.dryRun) {
7748
8174
  console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);
@@ -7752,7 +8178,7 @@ program.command("clean").description("Remove configs from DB whose target files
7752
8178
  printed++;
7753
8179
  }
7754
8180
  if (!opts.dryRun)
7755
- deleteConfig(c.id);
8181
+ await store.deleteConfig(c.id);
7756
8182
  removed++;
7757
8183
  }
7758
8184
  }
@@ -7767,6 +8193,7 @@ ${removed} orphaned config(s) ${opts.dryRun ? "found" : "removed"}${omitted > 0
7767
8193
  }
7768
8194
  });
7769
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();
7770
8197
  const packages = [
7771
8198
  { name: "@hasna/todos", bin: "todos", mcp: "todos-mcp" },
7772
8199
  { name: "@hasna/mementos", bin: "mementos", mcp: "mementos-mcp" },
@@ -7823,7 +8250,7 @@ Registering MCP servers in Claude Code:`));
7823
8250
  console.log(chalk.cyan(`
7824
8251
  Initializing configs:`));
7825
8252
  if (!opts.dryRun) {
7826
- const result = await syncKnown({});
8253
+ const result = await syncKnown({ store });
7827
8254
  console.log(chalk.green(" \u2713 ") + `Synced ${result.added + result.updated + result.unchanged} known configs`);
7828
8255
  } else {
7829
8256
  console.log(chalk.dim(" would run: configs init"));
@@ -7832,11 +8259,11 @@ Initializing configs:`));
7832
8259
  \u2713 Bootstrap complete.`) + chalk.dim(" Restart Claude Code for MCP servers to activate."));
7833
8260
  });
7834
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) => {
7835
- const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent });
8262
+ const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, store: resolveConfigStore() });
7836
8263
  console.log(chalk.green("\u2713") + ` Pulled: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
7837
8264
  });
7838
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) => {
7839
- const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent });
8266
+ const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, store: resolveConfigStore() });
7840
8267
  console.log(chalk.green("\u2713") + ` Pushed: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
7841
8268
  });
7842
8269
  program.command("update").description("Check for updates and install latest version").option("--check", "only check, don't install").action(async (opts) => {
@@ -7860,11 +8287,14 @@ program.command("update").description("Check for updates and install latest vers
7860
8287
  }
7861
8288
  });
7862
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) => {
7863
- const db = getDatabase();
7864
- 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
+ });
7865
8296
  console.log(chalk.green("\u2713") + " Feedback saved. Thank you!");
7866
8297
  });
7867
8298
  program.version(pkg.version).name("instructions");
7868
- registerStorageCommands(program);
7869
8299
  registerEventsCommands(program, { source: "configs" });
7870
8300
  program.parse(process.argv);