@hasna/instructions 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +43 -12
  2. package/dashboard/README.md +73 -0
  3. package/dist/cli/index.js +1305 -837
  4. package/dist/data/config-store.d.ts +134 -0
  5. package/dist/data/config-store.d.ts.map +1 -0
  6. package/dist/data/config-store.test.d.ts +2 -0
  7. package/dist/data/config-store.test.d.ts.map +1 -0
  8. package/dist/db/database.d.ts +15 -0
  9. package/dist/db/database.d.ts.map +1 -1
  10. package/dist/index.d.ts +6 -8
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +908 -492
  13. package/dist/lib/apply.d.ts +2 -2
  14. package/dist/lib/apply.d.ts.map +1 -1
  15. package/dist/lib/export.d.ts +2 -2
  16. package/dist/lib/export.d.ts.map +1 -1
  17. package/dist/lib/import.d.ts +2 -2
  18. package/dist/lib/import.d.ts.map +1 -1
  19. package/dist/lib/package-manager-guard.d.ts +24 -0
  20. package/dist/lib/package-manager-guard.d.ts.map +1 -0
  21. package/dist/lib/package-manager-guard.test.d.ts +2 -0
  22. package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
  23. package/dist/lib/platform-profiles.d.ts +2 -2
  24. package/dist/lib/platform-profiles.d.ts.map +1 -1
  25. package/dist/lib/project-dashboard-standard.d.ts +2 -2
  26. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  27. package/dist/lib/redact.d.ts.map +1 -1
  28. package/dist/lib/sync-dir.d.ts +3 -3
  29. package/dist/lib/sync-dir.d.ts.map +1 -1
  30. package/dist/lib/sync.d.ts +6 -6
  31. package/dist/lib/sync.d.ts.map +1 -1
  32. package/dist/mcp/http.d.ts +0 -13
  33. package/dist/mcp/http.d.ts.map +1 -1
  34. package/dist/mcp/index.js +661 -574
  35. package/dist/mcp/server.d.ts.map +1 -1
  36. package/dist/server/index.d.ts.map +1 -1
  37. package/dist/server/index.js +1757 -17541
  38. package/dist/server/v1.d.ts.map +1 -1
  39. package/dist/status.d.ts +2 -2
  40. package/dist/status.d.ts.map +1 -1
  41. package/dist/storage/cloud-store.d.ts +21 -1
  42. package/dist/storage/cloud-store.d.ts.map +1 -1
  43. package/dist/storage/schema.d.ts.map +1 -1
  44. package/package.json +4 -7
  45. package/dashboard/dist/assets/index-D7p6fFQw.js +0 -11
  46. package/dashboard/dist/assets/index-DQ3P1g1z.css +0 -1
  47. package/dashboard/dist/index.html +0 -14
  48. package/dashboard/dist/vite.svg +0 -1
  49. package/dist/cli/storage.d.ts +0 -3
  50. package/dist/cli/storage.d.ts.map +0 -1
  51. package/dist/cli/storage.test.d.ts +0 -2
  52. package/dist/cli/storage.test.d.ts.map +0 -1
  53. package/dist/db/remote-storage.d.ts +0 -13
  54. package/dist/db/remote-storage.d.ts.map +0 -1
  55. package/dist/db/storage-sync.d.ts +0 -53
  56. package/dist/db/storage-sync.d.ts.map +0 -1
  57. package/dist/db/storage-sync.test.d.ts +0 -2
  58. package/dist/db/storage-sync.test.d.ts.map +0 -1
  59. package/dist/server/server.test.d.ts +0 -2
  60. package/dist/server/server.test.d.ts.map +0 -1
  61. package/dist/storage.d.ts +0 -5
  62. package/dist/storage.d.ts.map +0 -1
  63. package/dist/storage.js +0 -537
package/dist/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 {
@@ -2182,6 +2195,22 @@ function ensureFeedbackTable(db) {
2182
2195
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
2183
2196
  )
2184
2197
  `);
2198
+ const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
2199
+ const required = [
2200
+ ["email", "TEXT"],
2201
+ ["category", "TEXT DEFAULT 'general'"],
2202
+ ["version", "TEXT"],
2203
+ ["machine_id", "TEXT"],
2204
+ ["created_at", "TEXT"]
2205
+ ];
2206
+ for (const [name, def] of required) {
2207
+ if (!existing.has(name))
2208
+ db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
2209
+ }
2210
+ }
2211
+ function insertFeedback(input, db) {
2212
+ const d = db || getDatabase();
2213
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
2185
2214
  }
2186
2215
  function migrateDotfile() {
2187
2216
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
@@ -2613,6 +2642,135 @@ var init_machine = __esm(() => {
2613
2642
  init_template();
2614
2643
  });
2615
2644
 
2645
+ // src/db/profiles.ts
2646
+ function rowToProfile(row) {
2647
+ return {
2648
+ ...row,
2649
+ selectors: JSON.parse(row.selectors || "{}"),
2650
+ variables: JSON.parse(row.variables || "{}")
2651
+ };
2652
+ }
2653
+ function uniqueProfileSlug(name, db, excludeId) {
2654
+ const base = slugify(name);
2655
+ let slug = base;
2656
+ let i = 1;
2657
+ while (true) {
2658
+ const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
2659
+ if (!existing || existing.id === excludeId)
2660
+ return slug;
2661
+ slug = `${base}-${i++}`;
2662
+ }
2663
+ }
2664
+ function createProfile(input, db) {
2665
+ const d = db || getDatabase();
2666
+ const id = uuid();
2667
+ const ts = now2();
2668
+ const slug = uniqueProfileSlug(input.name, d);
2669
+ d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
2670
+ id,
2671
+ input.name,
2672
+ slug,
2673
+ input.description ?? null,
2674
+ JSON.stringify(input.selectors ?? {}),
2675
+ JSON.stringify(input.variables ?? {}),
2676
+ ts,
2677
+ ts
2678
+ ]);
2679
+ return getProfile(id, d);
2680
+ }
2681
+ function getProfile(idOrSlug, db) {
2682
+ const d = db || getDatabase();
2683
+ const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
2684
+ if (!row)
2685
+ throw new ProfileNotFoundError(idOrSlug);
2686
+ return rowToProfile(row);
2687
+ }
2688
+ function listProfiles(db) {
2689
+ const d = db || getDatabase();
2690
+ return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
2691
+ }
2692
+ function updateProfile(idOrSlug, input, db) {
2693
+ const d = db || getDatabase();
2694
+ const existing = getProfile(idOrSlug, d);
2695
+ const ts = now2();
2696
+ const updates = ["updated_at = ?"];
2697
+ const params = [ts];
2698
+ if (input.name !== undefined) {
2699
+ updates.push("name = ?", "slug = ?");
2700
+ params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
2701
+ }
2702
+ if (input.description !== undefined) {
2703
+ updates.push("description = ?");
2704
+ params.push(input.description);
2705
+ }
2706
+ if (input.selectors !== undefined) {
2707
+ updates.push("selectors = ?");
2708
+ params.push(JSON.stringify(input.selectors));
2709
+ }
2710
+ if (input.variables !== undefined) {
2711
+ updates.push("variables = ?");
2712
+ params.push(JSON.stringify(input.variables));
2713
+ }
2714
+ params.push(existing.id);
2715
+ d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
2716
+ return getProfile(existing.id, d);
2717
+ }
2718
+ function deleteProfile(idOrSlug, db) {
2719
+ const d = db || getDatabase();
2720
+ const existing = getProfile(idOrSlug, d);
2721
+ d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
2722
+ }
2723
+ function addConfigToProfile(profileIdOrSlug, configId, db) {
2724
+ const d = db || getDatabase();
2725
+ const profile = getProfile(profileIdOrSlug, d);
2726
+ const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
2727
+ const order = (maxRow?.max_order ?? -1) + 1;
2728
+ d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
2729
+ }
2730
+ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
2731
+ const d = db || getDatabase();
2732
+ const profile = getProfile(profileIdOrSlug, d);
2733
+ d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
2734
+ }
2735
+ function getProfileConfigs(profileIdOrSlug, db) {
2736
+ const d = db || getDatabase();
2737
+ const profile = getProfile(profileIdOrSlug, d);
2738
+ const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
2739
+ if (rows.length === 0)
2740
+ return [];
2741
+ const ids = rows.map((r) => r.config_id);
2742
+ return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
2743
+ }
2744
+ function profileHasSelectors(profile) {
2745
+ const selectors = profile.selectors ?? {};
2746
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
2747
+ }
2748
+ function profileMatchesMachine(profile, machine) {
2749
+ const selectors = profile.selectors ?? {};
2750
+ const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
2751
+ const value = candidate.trim().toLowerCase();
2752
+ return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
2753
+ });
2754
+ const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
2755
+ const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
2756
+ return osMatches && archMatches && hostnameMatches;
2757
+ }
2758
+ function resolveProfileForMachine(machine = detectMachineContext(), db) {
2759
+ const profiles = listProfiles(db).filter(profileHasSelectors);
2760
+ const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
2761
+ const selectors = profile.selectors;
2762
+ const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
2763
+ return { profile, score };
2764
+ }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
2765
+ return matches[0]?.profile ?? null;
2766
+ }
2767
+ var init_profiles = __esm(() => {
2768
+ init_types();
2769
+ init_database();
2770
+ init_configs();
2771
+ init_machine();
2772
+ });
2773
+
2616
2774
  // src/db/snapshots.ts
2617
2775
  function createSnapshot(configId, content, version, db) {
2618
2776
  const d = db || getDatabase();
@@ -2629,10 +2787,385 @@ function getSnapshot(id, db) {
2629
2787
  const d = db || getDatabase();
2630
2788
  return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
2631
2789
  }
2790
+ function getSnapshotByVersion(configId, version, db) {
2791
+ const d = db || getDatabase();
2792
+ return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
2793
+ }
2794
+ function pruneSnapshots(configId, keep = 10, db) {
2795
+ const d = db || getDatabase();
2796
+ const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
2797
+ SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
2798
+ )`, [configId, configId, keep]);
2799
+ return result.changes;
2800
+ }
2632
2801
  var init_snapshots = __esm(() => {
2633
2802
  init_database();
2634
2803
  });
2635
2804
 
2805
+ // src/db/machines.ts
2806
+ import { arch, hostname, type } from "os";
2807
+ function currentHostname2() {
2808
+ return hostname();
2809
+ }
2810
+ function currentOs() {
2811
+ return type();
2812
+ }
2813
+ function currentArch2() {
2814
+ return arch();
2815
+ }
2816
+ function registerMachine(hostnameStr, os, archStr, db) {
2817
+ const d = db || getDatabase();
2818
+ const h = hostnameStr ?? currentHostname2();
2819
+ const o = os ?? currentOs();
2820
+ const a = archStr ?? currentArch2();
2821
+ const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
2822
+ if (existing) {
2823
+ if (existing.os !== o || existing.arch !== a) {
2824
+ d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
2825
+ return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
2826
+ }
2827
+ return existing;
2828
+ }
2829
+ const id = uuid();
2830
+ const ts = now2();
2831
+ d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
2832
+ return d.query("SELECT * FROM machines WHERE id = ?").get(id);
2833
+ }
2834
+ function updateMachineApplied(hostnameStr, db) {
2835
+ const d = db || getDatabase();
2836
+ const h = hostnameStr ?? currentHostname2();
2837
+ d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now2(), h]);
2838
+ }
2839
+ function listMachines(db) {
2840
+ const d = db || getDatabase();
2841
+ return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
2842
+ }
2843
+ var init_machines = __esm(() => {
2844
+ init_database();
2845
+ });
2846
+
2847
+ // src/data/config-store.ts
2848
+ import { randomUUID as randomUUID5 } from "crypto";
2849
+ function resolveCloudConfig(env = process.env) {
2850
+ const apiUrl = env[API_URL_ENV]?.trim();
2851
+ const apiKey = env[API_KEY_ENV]?.trim();
2852
+ if (!apiUrl && !apiKey)
2853
+ return null;
2854
+ if (!apiUrl || !apiKey) {
2855
+ 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.`);
2856
+ }
2857
+ return { apiUrl, apiKey };
2858
+ }
2859
+ function isCloudMode(env = process.env) {
2860
+ return resolveCloudConfig(env) !== null;
2861
+ }
2862
+
2863
+ class LocalConfigStore {
2864
+ db;
2865
+ mode = "local";
2866
+ constructor(db) {
2867
+ this.db = db;
2868
+ }
2869
+ async listConfigs(filter) {
2870
+ return listConfigs(filter, this.db);
2871
+ }
2872
+ async getConfig(idOrSlug) {
2873
+ return getConfig(idOrSlug, this.db);
2874
+ }
2875
+ async getConfigById(id) {
2876
+ return getConfigById(id, this.db);
2877
+ }
2878
+ async createConfig(input) {
2879
+ return createConfig(input, this.db);
2880
+ }
2881
+ async updateConfig(idOrSlug, input) {
2882
+ return updateConfig(idOrSlug, input, this.db);
2883
+ }
2884
+ async deleteConfig(idOrSlug) {
2885
+ deleteConfig(idOrSlug, this.db);
2886
+ }
2887
+ async getConfigStats() {
2888
+ return getConfigStats(this.db);
2889
+ }
2890
+ async listSnapshots(configId) {
2891
+ return listSnapshots(configId, this.db);
2892
+ }
2893
+ async getSnapshot(id) {
2894
+ return getSnapshot(id, this.db);
2895
+ }
2896
+ async getSnapshotByVersion(configId, version) {
2897
+ return getSnapshotByVersion(configId, version, this.db);
2898
+ }
2899
+ async createSnapshot(configId, content, version) {
2900
+ return createSnapshot(configId, content, version, this.db);
2901
+ }
2902
+ async pruneSnapshots(configId, keep = 10) {
2903
+ return pruneSnapshots(configId, keep, this.db);
2904
+ }
2905
+ async listProfiles() {
2906
+ return listProfiles(this.db);
2907
+ }
2908
+ async getProfile(idOrSlug) {
2909
+ return getProfile(idOrSlug, this.db);
2910
+ }
2911
+ async getProfileConfigs(idOrSlug) {
2912
+ return getProfileConfigs(idOrSlug, this.db);
2913
+ }
2914
+ async createProfile(input) {
2915
+ return createProfile(input, this.db);
2916
+ }
2917
+ async updateProfile(idOrSlug, input) {
2918
+ return updateProfile(idOrSlug, input, this.db);
2919
+ }
2920
+ async deleteProfile(idOrSlug) {
2921
+ deleteProfile(idOrSlug, this.db);
2922
+ }
2923
+ async addConfigToProfile(profileIdOrSlug, configId) {
2924
+ addConfigToProfile(profileIdOrSlug, configId, this.db);
2925
+ }
2926
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
2927
+ removeConfigFromProfile(profileIdOrSlug, configId, this.db);
2928
+ }
2929
+ async resolveProfileForMachine(machine) {
2930
+ return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
2931
+ }
2932
+ async registerMachine(hostname2, os, arch2) {
2933
+ return registerMachine(hostname2, os, arch2, this.db);
2934
+ }
2935
+ async updateMachineApplied(hostname2) {
2936
+ updateMachineApplied(hostname2, this.db);
2937
+ }
2938
+ async listMachines() {
2939
+ return listMachines(this.db);
2940
+ }
2941
+ async sendFeedback(input) {
2942
+ insertFeedback(input, this.db);
2943
+ }
2944
+ async reset() {
2945
+ resetLocalDatabase();
2946
+ }
2947
+ }
2948
+
2949
+ class CloudConfigStore {
2950
+ mode = "api";
2951
+ base;
2952
+ apiKey;
2953
+ timeoutMs;
2954
+ constructor(config) {
2955
+ this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
2956
+ this.apiKey = config.apiKey;
2957
+ this.timeoutMs = config.timeoutMs ?? 30000;
2958
+ }
2959
+ async request(method, path, body, opts = {}) {
2960
+ const controller = new AbortController;
2961
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
2962
+ const headers = {
2963
+ Authorization: `Bearer ${this.apiKey}`,
2964
+ Accept: "application/json"
2965
+ };
2966
+ if (body !== undefined)
2967
+ headers["Content-Type"] = "application/json";
2968
+ if (opts.idempotent)
2969
+ headers["Idempotency-Key"] = randomUUID5();
2970
+ try {
2971
+ const res = await fetch(`${this.base}${path}`, {
2972
+ method,
2973
+ headers,
2974
+ body: body === undefined ? undefined : JSON.stringify(body),
2975
+ signal: controller.signal
2976
+ });
2977
+ if (res.status === 404 && opts.allow404)
2978
+ return { status: 404, data: null };
2979
+ const text = await res.text();
2980
+ let parsed = null;
2981
+ if (text) {
2982
+ try {
2983
+ parsed = JSON.parse(text);
2984
+ } catch {
2985
+ parsed = text;
2986
+ }
2987
+ }
2988
+ if (!res.ok) {
2989
+ const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
2990
+ throw new CloudHttpError(res.status, message, parsed);
2991
+ }
2992
+ return { status: res.status, data: parsed };
2993
+ } finally {
2994
+ clearTimeout(timer);
2995
+ }
2996
+ }
2997
+ async listConfigs(filter = {}) {
2998
+ const params = new URLSearchParams;
2999
+ if (filter.category)
3000
+ params.set("category", filter.category);
3001
+ if (filter.agent)
3002
+ params.set("agent", filter.agent);
3003
+ if (filter.kind)
3004
+ params.set("kind", filter.kind);
3005
+ if (filter.search)
3006
+ params.set("search", filter.search);
3007
+ const qs = params.toString();
3008
+ const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
3009
+ let configs = data?.configs ?? [];
3010
+ if (filter.tags && filter.tags.length > 0) {
3011
+ configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
3012
+ }
3013
+ if (filter.is_template !== undefined) {
3014
+ configs = configs.filter((c) => c.is_template === filter.is_template);
3015
+ }
3016
+ return configs;
3017
+ }
3018
+ async getConfig(idOrSlug) {
3019
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3020
+ if (status === 404 || !data?.config)
3021
+ throw new ConfigNotFoundError(idOrSlug);
3022
+ return data.config;
3023
+ }
3024
+ async getConfigById(id) {
3025
+ return this.getConfig(id);
3026
+ }
3027
+ async createConfig(input) {
3028
+ const { data } = await this.request("POST", "/configs", input, {
3029
+ idempotent: true
3030
+ });
3031
+ return data.config;
3032
+ }
3033
+ async updateConfig(idOrSlug, input) {
3034
+ const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
3035
+ return data.config;
3036
+ }
3037
+ async deleteConfig(idOrSlug) {
3038
+ const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3039
+ if (status === 404)
3040
+ throw new ConfigNotFoundError(idOrSlug);
3041
+ }
3042
+ async getConfigStats() {
3043
+ const { data } = await this.request("GET", "/stats");
3044
+ return data ?? { total: 0 };
3045
+ }
3046
+ async listSnapshots(configId) {
3047
+ const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
3048
+ return data?.snapshots ?? [];
3049
+ }
3050
+ async getSnapshot(id) {
3051
+ const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
3052
+ if (status === 404 || !data?.snapshot)
3053
+ return null;
3054
+ return data.snapshot;
3055
+ }
3056
+ async getSnapshotByVersion(configId, version) {
3057
+ const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
3058
+ if (status === 404 || !data?.snapshot)
3059
+ return null;
3060
+ return data.snapshot;
3061
+ }
3062
+ async createSnapshot(configId, content, version) {
3063
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
3064
+ return data.snapshot;
3065
+ }
3066
+ async pruneSnapshots(configId, keep = 10) {
3067
+ const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
3068
+ return data?.pruned ?? 0;
3069
+ }
3070
+ async listProfiles() {
3071
+ const { data } = await this.request("GET", "/profiles");
3072
+ return data?.profiles ?? [];
3073
+ }
3074
+ async getProfile(idOrSlug) {
3075
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3076
+ if (status === 404 || !data?.profile)
3077
+ throw new ProfileNotFoundError(idOrSlug);
3078
+ const { configs: _configs, ...profile } = data.profile;
3079
+ return profile;
3080
+ }
3081
+ async getProfileConfigs(idOrSlug) {
3082
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3083
+ if (status === 404 || !data?.profile)
3084
+ throw new ProfileNotFoundError(idOrSlug);
3085
+ return data.profile.configs ?? [];
3086
+ }
3087
+ async createProfile(input) {
3088
+ const { data } = await this.request("POST", "/profiles", input, {
3089
+ idempotent: true
3090
+ });
3091
+ return data.profile;
3092
+ }
3093
+ async updateProfile(idOrSlug, input) {
3094
+ const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
3095
+ return data.profile;
3096
+ }
3097
+ async deleteProfile(idOrSlug) {
3098
+ const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3099
+ if (status === 404)
3100
+ throw new ProfileNotFoundError(idOrSlug);
3101
+ }
3102
+ async addConfigToProfile(profileIdOrSlug, configId) {
3103
+ await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
3104
+ }
3105
+ async removeConfigFromProfile(profileIdOrSlug, configId) {
3106
+ await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
3107
+ }
3108
+ async resolveProfileForMachine(machine) {
3109
+ const params = new URLSearchParams;
3110
+ if (machine?.hostname)
3111
+ params.set("hostname", machine.hostname);
3112
+ if (machine?.os)
3113
+ params.set("os", machine.os);
3114
+ if (machine?.arch)
3115
+ params.set("arch", machine.arch);
3116
+ const qs = params.toString();
3117
+ const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
3118
+ if (status === 404 || !data?.profile)
3119
+ return null;
3120
+ return data.profile;
3121
+ }
3122
+ async registerMachine(hostname2, os, arch2) {
3123
+ const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
3124
+ return data.machine;
3125
+ }
3126
+ async updateMachineApplied(hostname2) {
3127
+ await this.request("POST", "/machines/applied", { hostname: hostname2 });
3128
+ }
3129
+ async listMachines() {
3130
+ const { data } = await this.request("GET", "/machines");
3131
+ return data?.machines ?? [];
3132
+ }
3133
+ async sendFeedback(input) {
3134
+ await this.request("POST", "/feedback", {
3135
+ message: input.message,
3136
+ email: input.email ?? undefined,
3137
+ category: input.category ?? undefined,
3138
+ version: input.version ?? undefined
3139
+ });
3140
+ }
3141
+ async reset() {
3142
+ 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.");
3143
+ }
3144
+ }
3145
+ function resolveConfigStore(env = process.env) {
3146
+ const cloud = resolveCloudConfig(env);
3147
+ return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
3148
+ }
3149
+ var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
3150
+ var init_config_store = __esm(() => {
3151
+ init_configs();
3152
+ init_profiles();
3153
+ init_snapshots();
3154
+ init_machines();
3155
+ init_database();
3156
+ init_types();
3157
+ CloudHttpError = class CloudHttpError extends Error {
3158
+ status;
3159
+ body;
3160
+ constructor(status, message, body) {
3161
+ super(message);
3162
+ this.status = status;
3163
+ this.body = body;
3164
+ this.name = "CloudHttpError";
3165
+ }
3166
+ };
3167
+ });
3168
+
2636
3169
  // src/lib/transforms.ts
2637
3170
  import { basename, extname } from "path";
2638
3171
  function ensureTrailingNewline(content) {
@@ -2800,8 +3333,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
2800
3333
  mkdirSync2(dir, { recursive: true });
2801
3334
  }
2802
3335
  if (previousContent !== null && changed) {
2803
- const db = opts.db || getDatabase();
2804
- createSnapshot(config.id, previousContent, config.version, db);
3336
+ const store = opts.store ?? resolveConfigStore();
3337
+ await store.createSnapshot(config.id, previousContent, config.version);
2805
3338
  }
2806
3339
  writeFileSync(path, renderedContent, "utf-8");
2807
3340
  }
@@ -2827,8 +3360,8 @@ async function applyConfig(config, opts = {}) {
2827
3360
  if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
2828
3361
  throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
2829
3362
  }
2830
- const db = opts.db || getDatabase();
2831
- const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
3363
+ const store = opts.store ?? resolveConfigStore();
3364
+ const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
2832
3365
  if (isGeneratedOutputTarget(config, contextConfigs)) {
2833
3366
  throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
2834
3367
  }
@@ -2849,7 +3382,7 @@ async function applyConfig(config, opts = {}) {
2849
3382
  };
2850
3383
  }
2851
3384
  if (!opts.dryRun) {
2852
- updateConfig(config.id, { synced_at: now2() }, db);
3385
+ await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
2853
3386
  }
2854
3387
  return result;
2855
3388
  }
@@ -2871,9 +3404,7 @@ async function applyConfigs(configs, opts = {}) {
2871
3404
  }
2872
3405
  var init_apply = __esm(() => {
2873
3406
  init_types();
2874
- init_database();
2875
- init_configs();
2876
- init_snapshots();
3407
+ init_config_store();
2877
3408
  init_machine();
2878
3409
  init_transforms();
2879
3410
  });
@@ -2961,9 +3492,9 @@ function redactIni(content) {
2961
3492
  for (let i = 0;i < lines.length; i++) {
2962
3493
  const line = lines[i];
2963
3494
  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}}`);
3495
+ if (authM && !isReferenceValue(authM[2].trim())) {
3496
+ redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
3497
+ out.push(`${authM[1]}\${NPM_TOKEN}`);
2967
3498
  continue;
2968
3499
  }
2969
3500
  const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
@@ -3003,6 +3534,8 @@ function redactGeneric(content) {
3003
3534
  function shouldRedactKeyValue(key, value) {
3004
3535
  if (!value || value.startsWith("{{"))
3005
3536
  return false;
3537
+ if (isReferenceValue(value.trim()))
3538
+ return false;
3006
3539
  if (value.length < MIN_SECRET_VALUE_LEN)
3007
3540
  return false;
3008
3541
  if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
@@ -3024,6 +3557,9 @@ function reasonFor(key, value) {
3024
3557
  }
3025
3558
  return "secret value pattern";
3026
3559
  }
3560
+ function isReferenceValue(value) {
3561
+ 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);
3562
+ }
3027
3563
  function redactContent(content, format) {
3028
3564
  switch (format) {
3029
3565
  case "shell":
@@ -3065,14 +3601,14 @@ function shouldSkip(p) {
3065
3601
  return SKIP.some((s) => p.includes(s));
3066
3602
  }
3067
3603
  async function syncFromDir(dir, opts = {}) {
3068
- const d = opts.db || getDatabase();
3604
+ const store = opts.store ?? resolveConfigStore();
3069
3605
  const absDir = expandPath(dir);
3070
3606
  if (!existsSync5(absDir))
3071
3607
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
3072
3608
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
3073
3609
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3074
3610
  const home = homedir4();
3075
- const allConfigs = listConfigs(undefined, d);
3611
+ const allConfigs = await store.listConfigs();
3076
3612
  for (const file of files) {
3077
3613
  if (shouldSkip(file)) {
3078
3614
  result.skipped.push(file);
@@ -3088,11 +3624,11 @@ async function syncFromDir(dir, opts = {}) {
3088
3624
  const existing = allConfigs.find((c) => c.target_path === targetPath);
3089
3625
  if (!existing) {
3090
3626
  if (!opts.dryRun)
3091
- createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }, d);
3627
+ await store.createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
3092
3628
  result.added++;
3093
3629
  } else if (existing.content !== content) {
3094
3630
  if (!opts.dryRun)
3095
- updateConfig(existing.id, { content }, d);
3631
+ await store.updateConfig(existing.id, { content });
3096
3632
  result.updated++;
3097
3633
  } else {
3098
3634
  result.unchanged++;
@@ -3104,17 +3640,17 @@ async function syncFromDir(dir, opts = {}) {
3104
3640
  return result;
3105
3641
  }
3106
3642
  async function syncToDir(dir, opts = {}) {
3107
- const d = opts.db || getDatabase();
3643
+ const store = opts.store ?? resolveConfigStore();
3108
3644
  const home = homedir4();
3109
3645
  const absDir = expandPath(dir);
3110
3646
  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)));
3647
+ const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
3112
3648
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3113
3649
  for (const config of configs) {
3114
3650
  if (config.kind === "reference")
3115
3651
  continue;
3116
3652
  try {
3117
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d });
3653
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store });
3118
3654
  r.changed ? result.updated++ : result.unchanged++;
3119
3655
  } catch {
3120
3656
  result.skipped.push(config.target_path || config.id);
@@ -3136,8 +3672,7 @@ function walkDir(dir, files = []) {
3136
3672
  }
3137
3673
  var SKIP;
3138
3674
  var init_sync_dir = __esm(() => {
3139
- init_database();
3140
- init_configs();
3675
+ init_config_store();
3141
3676
  init_apply();
3142
3677
  init_sync();
3143
3678
  SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
@@ -3210,11 +3745,11 @@ function isKnownGeneratedTargetPath(targetPath) {
3210
3745
  return hasClaudeRuleSourceForCursorTarget(targetPath);
3211
3746
  }
3212
3747
  async function syncProject(opts) {
3213
- const d = opts.db || getDatabase();
3748
+ const store = opts.store ?? resolveConfigStore();
3214
3749
  const absDir = expandPath(opts.projectDir);
3215
3750
  const projectName = absDir.split("/").pop() || "project";
3216
3751
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3217
- const allConfigs = listConfigs(undefined, d);
3752
+ const allConfigs = await store.listConfigs();
3218
3753
  const machine = detectMachineContext();
3219
3754
  for (const pf of PROJECT_CONFIG_FILES) {
3220
3755
  const abs = join5(absDir, pf.file);
@@ -3236,11 +3771,11 @@ async function syncProject(opts) {
3236
3771
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
3237
3772
  if (!existing) {
3238
3773
  if (!opts.dryRun)
3239
- createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }, d);
3774
+ await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
3240
3775
  result.added++;
3241
3776
  } else if (existing.content !== content) {
3242
3777
  if (!opts.dryRun)
3243
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3778
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3244
3779
  result.updated++;
3245
3780
  } else {
3246
3781
  result.unchanged++;
@@ -3265,11 +3800,11 @@ async function syncProject(opts) {
3265
3800
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
3266
3801
  if (!existing) {
3267
3802
  if (!opts.dryRun)
3268
- createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }, d);
3803
+ await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
3269
3804
  result.added++;
3270
3805
  } else if (existing.content !== content) {
3271
3806
  if (!opts.dryRun)
3272
- updateConfig(existing.id, { content, is_template: isTemplate2 }, d);
3807
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
3273
3808
  result.updated++;
3274
3809
  } else {
3275
3810
  result.unchanged++;
@@ -3279,7 +3814,7 @@ async function syncProject(opts) {
3279
3814
  return result;
3280
3815
  }
3281
3816
  async function syncKnown(opts = {}) {
3282
- const d = opts.db || getDatabase();
3817
+ const store = opts.store ?? resolveConfigStore();
3283
3818
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3284
3819
  const home = getConfigHome();
3285
3820
  const machine = detectMachineContext();
@@ -3288,7 +3823,7 @@ async function syncKnown(opts = {}) {
3288
3823
  targets = targets.filter((k) => k.agent === opts.agent);
3289
3824
  if (opts.category)
3290
3825
  targets = targets.filter((k) => k.category === opts.category);
3291
- const allConfigs = listConfigs(undefined, d);
3826
+ const allConfigs = await store.listConfigs();
3292
3827
  const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
3293
3828
  for (const known of targets) {
3294
3829
  if (known.rulesDir) {
@@ -3317,15 +3852,15 @@ async function syncKnown(opts = {}) {
3317
3852
  const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
3318
3853
  if (!existing) {
3319
3854
  if (!opts.dryRun)
3320
- createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }, d);
3855
+ await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
3321
3856
  result.added++;
3322
3857
  } else if (existing.content !== content) {
3323
3858
  if (!opts.dryRun)
3324
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs }, d);
3859
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
3325
3860
  result.updated++;
3326
3861
  } else if (!outputsEqual(existing.outputs, outputs)) {
3327
3862
  if (!opts.dryRun)
3328
- updateConfig(existing.id, { outputs }, d);
3863
+ await store.updateConfig(existing.id, { outputs });
3329
3864
  result.updated++;
3330
3865
  } else {
3331
3866
  result.unchanged++;
@@ -3357,7 +3892,7 @@ async function syncKnown(opts = {}) {
3357
3892
  const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
3358
3893
  if (!existing) {
3359
3894
  if (!opts.dryRun) {
3360
- createConfig({
3895
+ await store.createConfig({
3361
3896
  name: known.name,
3362
3897
  category: known.category,
3363
3898
  agent: known.agent,
@@ -3368,16 +3903,16 @@ async function syncKnown(opts = {}) {
3368
3903
  description: known.description,
3369
3904
  is_template: isTemplate2,
3370
3905
  outputs: known.outputs
3371
- }, d);
3906
+ });
3372
3907
  }
3373
3908
  result.added++;
3374
3909
  } else if (existing.content !== content) {
3375
3910
  if (!opts.dryRun)
3376
- updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }, d);
3911
+ await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
3377
3912
  result.updated++;
3378
3913
  } else if (!outputsEqual(existing.outputs, known.outputs)) {
3379
3914
  if (!opts.dryRun)
3380
- updateConfig(existing.id, { outputs: known.outputs }, d);
3915
+ await store.updateConfig(existing.id, { outputs: known.outputs });
3381
3916
  result.updated++;
3382
3917
  } else {
3383
3918
  result.unchanged++;
@@ -3389,9 +3924,9 @@ async function syncKnown(opts = {}) {
3389
3924
  return result;
3390
3925
  }
3391
3926
  async function syncToDisk(opts = {}) {
3392
- const d = opts.db || getDatabase();
3927
+ const store = opts.store ?? resolveConfigStore();
3393
3928
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
3394
- const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }, d);
3929
+ const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
3395
3930
  const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
3396
3931
  let configs = allFileConfigs.filter((config) => {
3397
3932
  return !isGeneratedOutputTarget2(config, outputOwners);
@@ -3404,7 +3939,7 @@ async function syncToDisk(opts = {}) {
3404
3939
  if (!config.target_path && config.outputs.length === 0)
3405
3940
  continue;
3406
3941
  try {
3407
- const r = await applyConfig(config, { dryRun: opts.dryRun, db: d, outputAgent: opts.agent });
3942
+ const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
3408
3943
  r.changed ? result.updated++ : result.unchanged++;
3409
3944
  } catch {
3410
3945
  result.skipped.push(config.target_path ?? config.id);
@@ -3441,12 +3976,12 @@ function buildDiff(expectedContent, targetPath) {
3441
3976
  return lines.join(`
3442
3977
  `);
3443
3978
  }
3444
- function diffConfig(config, opts = {}) {
3979
+ async function diffConfig(config, opts = {}) {
3445
3980
  if (!config.target_path && config.outputs.length === 0)
3446
3981
  return "(reference \u2014 no target path)";
3447
3982
  const diffs = [];
3448
- const db = opts.db || getDatabase();
3449
- const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(undefined, db) : [config];
3983
+ const store = opts.store ?? resolveConfigStore();
3984
+ const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
3450
3985
  if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
3451
3986
  return "(generated output \u2014 managed by fan-out)";
3452
3987
  }
@@ -3525,8 +4060,7 @@ function detectFormat(filePath) {
3525
4060
  }
3526
4061
  var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
3527
4062
  var init_sync = __esm(() => {
3528
- init_database();
3529
- init_configs();
4063
+ init_config_store();
3530
4064
  init_apply();
3531
4065
  init_redact();
3532
4066
  init_machine();
@@ -3581,6 +4115,399 @@ var init_sync = __esm(() => {
3581
4115
  ];
3582
4116
  });
3583
4117
 
4118
+ // src/lib/package-manager-guard.ts
4119
+ var exports_package_manager_guard = {};
4120
+ __export(exports_package_manager_guard, {
4121
+ scanPackageManagerSecrets: () => scanPackageManagerSecrets
4122
+ });
4123
+ import { execFileSync } from "child_process";
4124
+ import { existsSync as existsSync12, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
4125
+ import { homedir as homedir6 } from "os";
4126
+ import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join10, relative as relative4, resolve as resolve6 } from "path";
4127
+ function scanPackageManagerSecrets(options = {}) {
4128
+ const cwd = options.cwd ? resolve6(options.cwd) : process.cwd();
4129
+ const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve6(cwd, root));
4130
+ const findings = [];
4131
+ let scannedFiles = 0;
4132
+ for (const root of roots) {
4133
+ if (!existsSync12(root))
4134
+ continue;
4135
+ const stat = lstatSync2(root);
4136
+ if (stat.isFile()) {
4137
+ if (!shouldScanRepoFile(root))
4138
+ continue;
4139
+ const text = readTextFile(root);
4140
+ if (text === null)
4141
+ continue;
4142
+ scannedFiles++;
4143
+ findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
4144
+ continue;
4145
+ }
4146
+ if (!stat.isDirectory())
4147
+ continue;
4148
+ const tracked = trackedFiles(root);
4149
+ for (const file of collectRepoFiles(root)) {
4150
+ const rel = toPosix(relative4(root, file));
4151
+ const isTracked = tracked.has(rel);
4152
+ const text = readTextFile(file);
4153
+ if (text === null)
4154
+ continue;
4155
+ scannedFiles++;
4156
+ findings.push(...scanFile(file, text, classifyRepoFile(file), isTracked, root));
4157
+ }
4158
+ }
4159
+ if (options.includeHome) {
4160
+ const home = homedir6();
4161
+ for (const name of HOME_FILES) {
4162
+ const file = join10(home, name);
4163
+ if (!existsSync12(file))
4164
+ continue;
4165
+ const text = readTextFile(file);
4166
+ if (text === null)
4167
+ continue;
4168
+ scannedFiles++;
4169
+ findings.push(...scanFile(file, text, classifyHomeFile(name), false, home));
4170
+ }
4171
+ }
4172
+ findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
4173
+ return {
4174
+ clean: findings.length === 0,
4175
+ scannedFiles,
4176
+ scannedRoots: roots,
4177
+ findings
4178
+ };
4179
+ }
4180
+ function collectRepoFiles(root) {
4181
+ const out = [];
4182
+ const visit = (dir) => {
4183
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
4184
+ if (entry.isDirectory()) {
4185
+ if (SKIP_DIRS.has(entry.name))
4186
+ continue;
4187
+ visit(join10(dir, entry.name));
4188
+ continue;
4189
+ }
4190
+ if (!entry.isFile())
4191
+ continue;
4192
+ const file = join10(dir, entry.name);
4193
+ if (shouldScanRepoFile(file))
4194
+ out.push(file);
4195
+ }
4196
+ };
4197
+ visit(root);
4198
+ return out;
4199
+ }
4200
+ function shouldScanRepoFile(file) {
4201
+ const name = basename5(file);
4202
+ return isNpmrcName(name) || isBunConfigName(name) || LOCKFILE_NAMES.has(name);
4203
+ }
4204
+ function classifyRepoFile(file) {
4205
+ const name = basename5(file);
4206
+ if (isNpmrcName(name))
4207
+ return "repo-npmrc";
4208
+ if (isBunConfigName(name))
4209
+ return "bun-config";
4210
+ return "lockfile";
4211
+ }
4212
+ function classifyHomeFile(name) {
4213
+ if (name === ".npmrc")
4214
+ return "home-npmrc";
4215
+ if (isBunConfigName(name))
4216
+ return "bun-config";
4217
+ return "shell-profile";
4218
+ }
4219
+ function isBunConfigName(name) {
4220
+ return name === "bunfig.toml" || name === ".bunfig.toml";
4221
+ }
4222
+ function isNpmrcName(name) {
4223
+ return name === ".npmrc" || name.startsWith(".npmrc.") || name.endsWith(".npmrc");
4224
+ }
4225
+ function readTextFile(file) {
4226
+ try {
4227
+ const stat = lstatSync2(file);
4228
+ if (!stat.isFile() || stat.size > 5000000)
4229
+ return null;
4230
+ const buf = readFileSync8(file);
4231
+ if (buf.includes(0))
4232
+ return null;
4233
+ return buf.toString("utf-8");
4234
+ } catch {
4235
+ return null;
4236
+ }
4237
+ }
4238
+ function scanFile(file, text, surface, tracked, root) {
4239
+ const findings = [];
4240
+ const path = displayPath(file, root);
4241
+ if (surface === "bun-config")
4242
+ return scanBunConfigFile(text, path, tracked);
4243
+ const lines = text.split(/\r?\n/);
4244
+ for (let i = 0;i < lines.length; i++) {
4245
+ const line = lines[i];
4246
+ const lineNo = i + 1;
4247
+ if (surface === "repo-npmrc" || surface === "home-npmrc") {
4248
+ findings.push(...scanNpmrcLine(line, path, lineNo, surface, tracked));
4249
+ } else if (surface === "shell-profile") {
4250
+ findings.push(...scanShellProfileLine(line, path, lineNo, tracked));
4251
+ } else {
4252
+ findings.push(...scanLockfileLine(line, path, lineNo, tracked));
4253
+ }
4254
+ }
4255
+ return findings;
4256
+ }
4257
+ function scanNpmrcLine(lineText, path, line, surface, tracked) {
4258
+ const findings = [];
4259
+ const stripped = lineText.trim();
4260
+ if (stripped === "" || stripped.startsWith("#") || stripped.startsWith(";"))
4261
+ return findings;
4262
+ const auth = stripped.match(/(?:^|:)(_[A-Za-z]*(?:auth|password)[A-Za-z]*|password)\s*=\s*(.+)$/i);
4263
+ if (auth) {
4264
+ const value = stripQuotes(stripInlineComment(auth[2].trim()));
4265
+ if (value && !isSafeReference(value)) {
4266
+ findings.push({
4267
+ path,
4268
+ line,
4269
+ rule: "npmrc-literal-auth",
4270
+ surface,
4271
+ severity: "error",
4272
+ tracked,
4273
+ detail: tracked ? "tracked npm auth entry uses a literal value" : "npm auth entry uses a literal value"
4274
+ });
4275
+ }
4276
+ }
4277
+ findings.push(...scanCredentialedUrl(stripped, path, line, surface, tracked));
4278
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, surface, tracked));
4279
+ return findings;
4280
+ }
4281
+ function scanBunConfigFile(text, path, tracked) {
4282
+ const findings = [];
4283
+ const lines = text.split(/\r?\n/);
4284
+ let inReleaseAgeExcludes = false;
4285
+ let hasMinimumReleaseAge = false;
4286
+ for (let i = 0;i < lines.length; i++) {
4287
+ const lineText = lines[i];
4288
+ const line = i + 1;
4289
+ const stripped = lineText.trim();
4290
+ if (stripped === "" || stripped.startsWith("#"))
4291
+ continue;
4292
+ const releaseAge = stripped.match(/^minimumReleaseAge\s*=\s*(?:"([^"]+)"|'([^']+)'|([0-9]+))\s*(?:#.*)?$/i);
4293
+ if (releaseAge) {
4294
+ hasMinimumReleaseAge = true;
4295
+ const rawValue = releaseAge[1] ?? releaseAge[2] ?? releaseAge[3] ?? "";
4296
+ const value = Number(rawValue);
4297
+ if (!Number.isFinite(value) || value <= 0) {
4298
+ findings.push({
4299
+ path,
4300
+ line,
4301
+ rule: "bun-release-age-disabled",
4302
+ surface: "bun-config",
4303
+ severity: "error",
4304
+ tracked,
4305
+ detail: "Bun release-age quarantine is disabled"
4306
+ });
4307
+ }
4308
+ }
4309
+ const startsReleaseAgeExcludes = /minimumReleaseAgeExcludes/i.test(stripped);
4310
+ const scanExcludes = startsReleaseAgeExcludes || inReleaseAgeExcludes;
4311
+ if (scanExcludes) {
4312
+ const quoted = [...stripped.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
4313
+ for (const item of quoted) {
4314
+ if (!isExactHasnaPackageName(item)) {
4315
+ findings.push({
4316
+ path,
4317
+ line,
4318
+ rule: "bun-release-age-broad-exclude",
4319
+ surface: "bun-config",
4320
+ severity: "error",
4321
+ tracked,
4322
+ detail: "Bun release-age exclude must be an exact @hasna package name"
4323
+ });
4324
+ }
4325
+ }
4326
+ }
4327
+ inReleaseAgeExcludes = startsReleaseAgeExcludes ? stripped.includes("[") && !stripped.includes("]") : inReleaseAgeExcludes && !stripped.includes("]");
4328
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "bun-config", tracked));
4329
+ }
4330
+ if (!hasMinimumReleaseAge) {
4331
+ findings.push({
4332
+ path,
4333
+ line: 1,
4334
+ rule: "bun-release-age-missing",
4335
+ surface: "bun-config",
4336
+ severity: "error",
4337
+ tracked,
4338
+ detail: "Bun release-age quarantine must be configured with a positive minimumReleaseAge"
4339
+ });
4340
+ }
4341
+ return findings;
4342
+ }
4343
+ function scanShellProfileLine(lineText, path, line, tracked) {
4344
+ const findings = [];
4345
+ const stripped = lineText.trim();
4346
+ if (stripped === "" || stripped.startsWith("#"))
4347
+ return findings;
4348
+ const assignment = stripped.match(/^(?:export\s+)?(NPM(?:_CONFIG)?_[A-Z0-9_]*TOKEN|NODE_AUTH_TOKEN|NPM_TOKEN)\s*=\s*(.+)$/);
4349
+ if (assignment) {
4350
+ const value = stripQuotes(stripInlineComment(assignment[2].trim()));
4351
+ if (value && !isSafeReference(value)) {
4352
+ findings.push({
4353
+ path,
4354
+ line,
4355
+ rule: "shell-literal-package-token",
4356
+ surface: "shell-profile",
4357
+ severity: "error",
4358
+ tracked,
4359
+ detail: "shell profile package-manager token uses a literal value"
4360
+ });
4361
+ }
4362
+ }
4363
+ findings.push(...scanKnownTokenPatterns(stripped, path, line, "shell-profile", tracked));
4364
+ return findings;
4365
+ }
4366
+ function scanLockfileLine(lineText, path, line, tracked) {
4367
+ const findings = scanKnownTokenPatterns(lineText, path, line, "lockfile", tracked);
4368
+ if (/(?:^|:)_authToken\s*=\s*/i.test(lineText) && !/\$\{[A-Z0-9_]+\}|\{\{[A-Z0-9_]+\}\}/.test(lineText)) {
4369
+ findings.push({
4370
+ path,
4371
+ line,
4372
+ rule: "lockfile-auth-token",
4373
+ surface: "lockfile",
4374
+ severity: "error",
4375
+ tracked,
4376
+ detail: "lockfile contains package-manager auth token material"
4377
+ });
4378
+ }
4379
+ return findings;
4380
+ }
4381
+ function scanKnownTokenPatterns(lineText, path, line, surface, tracked) {
4382
+ const findings = [];
4383
+ for (const pattern of TOKEN_VALUE_PATTERNS) {
4384
+ if (pattern.re.test(lineText)) {
4385
+ findings.push({
4386
+ path,
4387
+ line,
4388
+ rule: pattern.rule,
4389
+ surface,
4390
+ severity: "error",
4391
+ tracked,
4392
+ detail: pattern.detail
4393
+ });
4394
+ }
4395
+ }
4396
+ return findings;
4397
+ }
4398
+ function scanCredentialedUrl(lineText, path, line, surface, tracked) {
4399
+ const findings = [];
4400
+ for (const match of lineText.matchAll(/\bhttps?:\/\/([^/\s#;]+)@/gi)) {
4401
+ const userInfo = match[1];
4402
+ const credentialPart = userInfo.includes(":") ? userInfo.split(":").slice(1).join(":") : userInfo;
4403
+ if (credentialPart && !isSafeReference(credentialPart)) {
4404
+ findings.push({
4405
+ path,
4406
+ line,
4407
+ rule: "package-manager-url-credentials",
4408
+ surface,
4409
+ severity: "error",
4410
+ tracked,
4411
+ detail: "package-manager URL embeds literal credentials"
4412
+ });
4413
+ }
4414
+ }
4415
+ return findings;
4416
+ }
4417
+ function trackedFiles(root) {
4418
+ try {
4419
+ const output = execFileSync("git", ["-C", root, "ls-files", "-z"], {
4420
+ encoding: "utf-8",
4421
+ stdio: ["ignore", "pipe", "ignore"]
4422
+ });
4423
+ return new Set(output.split("\x00").filter(Boolean).map(toPosix));
4424
+ } catch {
4425
+ return new Set;
4426
+ }
4427
+ }
4428
+ function isTrackedFile(file) {
4429
+ try {
4430
+ const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
4431
+ encoding: "utf-8",
4432
+ stdio: ["ignore", "pipe", "ignore"]
4433
+ }).trim();
4434
+ const rel = toPosix(relative4(repoRoot, file));
4435
+ execFileSync("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
4436
+ stdio: ["ignore", "ignore", "ignore"]
4437
+ });
4438
+ return true;
4439
+ } catch {
4440
+ return false;
4441
+ }
4442
+ }
4443
+ function isExactHasnaPackageName(item) {
4444
+ return /^@hasna\/[a-z0-9][a-z0-9._-]*$/.test(item);
4445
+ }
4446
+ function isSafeReference(value) {
4447
+ const trimmed = stripQuotes(value.trim());
4448
+ 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);
4449
+ }
4450
+ function stripQuotes(value) {
4451
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
4452
+ return value.slice(1, -1);
4453
+ }
4454
+ return value;
4455
+ }
4456
+ function stripInlineComment(value) {
4457
+ return value.replace(/\s[#;].*$/, "").trim();
4458
+ }
4459
+ function displayPath(file, root) {
4460
+ const home = homedir6();
4461
+ if (root === home && (file === home || file.startsWith(home + "/")))
4462
+ return "~/" + toPosix(relative4(home, file));
4463
+ if (isAbsolute3(root) && file.startsWith(root + "/"))
4464
+ return toPosix(relative4(root, file));
4465
+ if (file === home || file.startsWith(home + "/"))
4466
+ return "~/" + toPosix(relative4(home, file));
4467
+ return file;
4468
+ }
4469
+ function toPosix(path) {
4470
+ return path.split("\\").join("/");
4471
+ }
4472
+ var SKIP_DIRS, LOCKFILE_NAMES, HOME_FILES, TOKEN_VALUE_PATTERNS;
4473
+ var init_package_manager_guard = __esm(() => {
4474
+ SKIP_DIRS = new Set([
4475
+ ".git",
4476
+ "node_modules",
4477
+ "dist",
4478
+ "build",
4479
+ "coverage",
4480
+ ".next",
4481
+ ".turbo",
4482
+ ".cache"
4483
+ ]);
4484
+ LOCKFILE_NAMES = new Set([
4485
+ "bun.lock",
4486
+ "package-lock.json",
4487
+ "npm-shrinkwrap.json",
4488
+ "pnpm-lock.yaml",
4489
+ "yarn.lock"
4490
+ ]);
4491
+ HOME_FILES = [
4492
+ ".npmrc",
4493
+ ".bunfig.toml",
4494
+ "bunfig.toml",
4495
+ ".bashrc",
4496
+ ".bash_profile",
4497
+ ".zshrc",
4498
+ ".zprofile",
4499
+ ".profile"
4500
+ ];
4501
+ TOKEN_VALUE_PATTERNS = [
4502
+ { re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
4503
+ { re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
4504
+ { re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
4505
+ { re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
4506
+ { re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
4507
+ { re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
4508
+ ];
4509
+ });
4510
+
3584
4511
  // node_modules/@hasna/events/dist/commander.js
3585
4512
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
3586
4513
  import { existsSync } from "fs";
@@ -4238,198 +5165,65 @@ function registerEventCommands(program, options) {
4238
5165
  for (const event of rows)
4239
5166
  console.log(`${event.time} ${event.id} ${event.source} ${event.type} ${event.severity}`);
4240
5167
  });
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));
5168
+ 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) => {
5169
+ const result = await createClient(options).replay({
5170
+ eventId: actionOptions.id,
5171
+ source: actionOptions.source,
5172
+ type: actionOptions.type,
5173
+ dryRun: actionOptions.dryRun
5174
+ });
5175
+ print(result, wantsJson(actionOptions, command), `Replayed ${result.events.length} event(s), ${result.deliveries.length} delivery result(s)`);
5176
+ });
5177
+ return events;
4392
5178
  }
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;
5179
+ function registerEventsCommands(program, options) {
5180
+ registerWebhookCommands(program, options);
5181
+ registerEventCommands(program, options);
4396
5182
  }
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;
5183
+ function parseNumber(value) {
5184
+ const parsed = Number(value);
5185
+ if (!Number.isFinite(parsed))
5186
+ throw new Error(`Expected a number, got ${value}`);
5187
+ return parsed;
4406
5188
  }
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;
5189
+ function collectValues(value, previous) {
5190
+ previous.push(value);
5191
+ return previous;
4415
5192
  }
4416
5193
 
5194
+ // node_modules/.pnpm/commander@13.1.0/node_modules/commander/esm.mjs
5195
+ var import__ = __toESM(require_commander(), 1);
5196
+ var {
5197
+ program,
5198
+ createCommand,
5199
+ createArgument,
5200
+ createOption,
5201
+ CommanderError,
5202
+ InvalidArgumentError,
5203
+ InvalidOptionArgumentError,
5204
+ Command,
5205
+ Argument,
5206
+ Option,
5207
+ Help
5208
+ } = import__.default;
5209
+
4417
5210
  // src/cli/index.tsx
4418
- init_snapshots();
4419
- init_database();
4420
5211
  init_apply();
4421
5212
  init_sync();
4422
5213
  init_redact();
5214
+ import chalk from "chalk";
5215
+ import { existsSync as existsSync13, readFileSync as readFileSync9, writeSync } from "fs";
5216
+ import { homedir as homedir7 } from "os";
5217
+ import { basename as basename6, join as join11, resolve as resolve7 } from "path";
4423
5218
 
4424
5219
  // src/lib/export.ts
4425
- init_database();
4426
- init_configs();
4427
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
5220
+ init_config_store();
5221
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
4428
5222
  import { join as join6, resolve as resolve2 } from "path";
4429
5223
  import { tmpdir } from "os";
4430
5224
  async function exportConfigs(outputPath, opts = {}) {
4431
- const d = opts.db || getDatabase();
4432
- const configs = listConfigs(opts.filter, d);
5225
+ const store = opts.store ?? resolveConfigStore();
5226
+ const configs = await store.listConfigs(opts.filter);
4433
5227
  const absOutput = resolve2(outputPath);
4434
5228
  const tmpDir = join6(tmpdir(), `configs-export-${Date.now()}`);
4435
5229
  const contentsDir = join6(tmpDir, "contents");
@@ -4437,7 +5231,7 @@ async function exportConfigs(outputPath, opts = {}) {
4437
5231
  mkdirSync3(contentsDir, { recursive: true });
4438
5232
  const manifest = {
4439
5233
  version: "1.0.0",
4440
- exported_at: now2(),
5234
+ exported_at: new Date().toISOString(),
4441
5235
  configs: configs.map(({ content: _content, ...meta }) => meta)
4442
5236
  };
4443
5237
  writeFileSync2(join6(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
@@ -4457,19 +5251,18 @@ async function exportConfigs(outputPath, opts = {}) {
4457
5251
  return { path: absOutput, count: configs.length };
4458
5252
  } finally {
4459
5253
  if (existsSync7(tmpDir)) {
4460
- rmSync(tmpDir, { recursive: true, force: true });
5254
+ rmSync2(tmpDir, { recursive: true, force: true });
4461
5255
  }
4462
5256
  }
4463
5257
  }
4464
5258
 
4465
5259
  // 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";
5260
+ init_config_store();
5261
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync3 } from "fs";
4469
5262
  import { join as join7, resolve as resolve3 } from "path";
4470
5263
  import { tmpdir as tmpdir2 } from "os";
4471
5264
  async function importConfigs(bundlePath, opts = {}) {
4472
- const d = opts.db || getDatabase();
5265
+ const store = opts.store ?? resolveConfigStore();
4473
5266
  const conflict = opts.conflict ?? "skip";
4474
5267
  const absPath = resolve3(bundlePath);
4475
5268
  const tmpDir = join7(tmpdir2(), `configs-import-${Date.now()}`);
@@ -4496,17 +5289,17 @@ async function importConfigs(bundlePath, opts = {}) {
4496
5289
  const content = existsSync8(contentFile) ? readFileSync4(contentFile, "utf-8") : "";
4497
5290
  let existing = null;
4498
5291
  try {
4499
- existing = getConfig(meta.slug, d);
5292
+ existing = await store.getConfig(meta.slug);
4500
5293
  } catch {}
4501
5294
  if (existing) {
4502
5295
  if (conflict === "skip") {
4503
5296
  result.skipped++;
4504
5297
  } else if (conflict === "overwrite" || conflict === "version") {
4505
- updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs }, d);
5298
+ await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
4506
5299
  result.updated++;
4507
5300
  }
4508
5301
  } else {
4509
- createConfig({
5302
+ await store.createConfig({
4510
5303
  name: meta.name,
4511
5304
  kind: meta.kind,
4512
5305
  category: meta.category,
@@ -4518,7 +5311,7 @@ async function importConfigs(bundlePath, opts = {}) {
4518
5311
  description: meta.description ?? undefined,
4519
5312
  tags: meta.tags,
4520
5313
  is_template: meta.is_template
4521
- }, d);
5314
+ });
4522
5315
  result.created++;
4523
5316
  }
4524
5317
  } catch (err) {
@@ -4528,7 +5321,7 @@ async function importConfigs(bundlePath, opts = {}) {
4528
5321
  return result;
4529
5322
  } finally {
4530
5323
  if (existsSync8(tmpDir)) {
4531
- rmSync2(tmpDir, { recursive: true, force: true });
5324
+ rmSync3(tmpDir, { recursive: true, force: true });
4532
5325
  }
4533
5326
  }
4534
5327
  }
@@ -4538,14 +5331,14 @@ init_template();
4538
5331
  init_machine();
4539
5332
 
4540
5333
  // src/lib/session-apply.ts
4541
- import { createHash as createHash2, randomUUID as randomUUID5 } from "crypto";
5334
+ import { createHash as createHash2, randomUUID as randomUUID6 } from "crypto";
4542
5335
  import {
4543
5336
  existsSync as existsSync10,
4544
5337
  lstatSync,
4545
5338
  mkdirSync as mkdirSync5,
4546
5339
  readFileSync as readFileSync6,
4547
5340
  renameSync,
4548
- rmSync as rmSync3,
5341
+ rmSync as rmSync4,
4549
5342
  writeFileSync as writeFileSync3
4550
5343
  } from "fs";
4551
5344
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join9, parse as parse2, relative as relative3, resolve as resolve5 } from "path";
@@ -5478,7 +6271,7 @@ function applySessionRender(plan, options = {}) {
5478
6271
  continue;
5479
6272
  assertNoSymlinkSegments(targetHome, result.path);
5480
6273
  if (existsSync10(result.path))
5481
- rmSync3(result.path);
6274
+ rmSync4(result.path);
5482
6275
  }
5483
6276
  }
5484
6277
  return {
@@ -5712,7 +6505,7 @@ function writePlannedFile(path, content, targetHome) {
5712
6505
  const dir = dirname3(path);
5713
6506
  mkdirSync5(dir, { recursive: true });
5714
6507
  assertNoSymlinkSegments(targetHome, path);
5715
- const tmp = join9(dir, `.session-${randomUUID5()}.tmp`);
6508
+ const tmp = join9(dir, `.session-${randomUUID6()}.tmp`);
5716
6509
  writeFileSync3(tmp, content, "utf-8");
5717
6510
  renameSync(tmp, path);
5718
6511
  }
@@ -5730,7 +6523,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
5730
6523
  if (!previousManifest && existingFiles.length === 0)
5731
6524
  return null;
5732
6525
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
5733
- const snapshotPath = resolve5(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID5()}.json`);
6526
+ const snapshotPath = resolve5(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID6()}.json`);
5734
6527
  const snapshot = {
5735
6528
  schema: "hasna.configs.session-render-snapshot/v1",
5736
6529
  createdAt: new Date().toISOString(),
@@ -5788,10 +6581,10 @@ function sha2562(content) {
5788
6581
  }
5789
6582
 
5790
6583
  // src/lib/platform-profiles.ts
5791
- init_configs();
6584
+ init_config_store();
5792
6585
 
5793
6586
  // src/lib/project-dashboard-standard.ts
5794
- init_configs();
6587
+ init_config_store();
5795
6588
  var PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
5796
6589
  var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
5797
6590
  PROJECT_DASHBOARD_DIR: ".hasna/project",
@@ -5869,7 +6662,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
5869
6662
  ids, passport numbers, credentials, and contract clauses unless an explicit
5870
6663
  approved storage policy exists.
5871
6664
  `;
5872
- function ensureProjectDashboardStandardConfig(db) {
6665
+ async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
5873
6666
  const input = {
5874
6667
  name: "Agent Managed Project Dashboard Standard",
5875
6668
  category: "workspace",
@@ -5881,17 +6674,21 @@ function ensureProjectDashboardStandardConfig(db) {
5881
6674
  tags: ["projects-dashboard", "agent-projects", "json-render"]
5882
6675
  };
5883
6676
  try {
5884
- const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG, db);
6677
+ const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
5885
6678
  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);
6679
+ return await store.updateConfig(existing.id, input);
5887
6680
  }
5888
6681
  return existing;
5889
6682
  } catch {
5890
- return createConfig(input, db);
6683
+ return await store.createConfig(input);
5891
6684
  }
5892
6685
  }
5893
6686
 
5894
6687
  // src/lib/platform-profiles.ts
6688
+ function profileHasSelectors2(profile) {
6689
+ const selectors = profile.selectors ?? {};
6690
+ return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
6691
+ }
5895
6692
  var PLATFORM_PROFILE_PRESETS = [
5896
6693
  {
5897
6694
  name: "linux-arm64",
@@ -5918,25 +6715,25 @@ var PLATFORM_PROFILE_PRESETS = [
5918
6715
  }
5919
6716
  }
5920
6717
  ];
5921
- function ensurePlatformProfiles(db) {
5922
- const configs = listConfigs(undefined, db);
6718
+ async function ensurePlatformProfiles(store = resolveConfigStore()) {
6719
+ const configs = await store.listConfigs();
5923
6720
  const ensured = [];
5924
6721
  for (const preset of PLATFORM_PROFILE_PRESETS) {
5925
6722
  let profile;
5926
6723
  try {
5927
- profile = getProfile(preset.name, db);
5928
- if (!profileHasSelectors(profile) || Object.keys(profile.variables).length === 0) {
5929
- profile = updateProfile(profile.id, {
6724
+ profile = await store.getProfile(preset.name);
6725
+ if (!profileHasSelectors2(profile) || Object.keys(profile.variables).length === 0) {
6726
+ profile = await store.updateProfile(profile.id, {
5930
6727
  description: profile.description ?? preset.description,
5931
- selectors: profileHasSelectors(profile) ? profile.selectors : preset.selectors,
6728
+ selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
5932
6729
  variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
5933
- }, db);
6730
+ });
5934
6731
  }
5935
6732
  } catch {
5936
- profile = createProfile(preset, db);
6733
+ profile = await store.createProfile(preset);
5937
6734
  }
5938
6735
  for (const config of configs) {
5939
- addConfigToProfile(profile.id, config.id, db);
6736
+ await store.addConfigToProfile(profile.id, config.id);
5940
6737
  }
5941
6738
  ensured.push(profile);
5942
6739
  }
@@ -5944,20 +6741,10 @@ function ensurePlatformProfiles(db) {
5944
6741
  }
5945
6742
 
5946
6743
  // 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
6744
+ init_config_store();
5959
6745
  init_apply();
5960
6746
  init_redact();
6747
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
5961
6748
  var PACKAGE_NAME = "@hasna/instructions";
5962
6749
  var PACKAGE_VERSION = "0.3.0";
5963
6750
  function activeDatabaseEnv() {
@@ -5981,21 +6768,13 @@ function countBy(items, getValue) {
5981
6768
  }
5982
6769
  return counts;
5983
6770
  }
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()) {
6771
+ async function getConfigsStatus(store = resolveConfigStore()) {
5993
6772
  let databaseReachable = true;
5994
6773
  let configs = [];
5995
6774
  let categoryStats = { total: 0 };
5996
6775
  try {
5997
- configs = listConfigs(undefined, db);
5998
- categoryStats = getConfigStats(db);
6776
+ configs = await store.listConfigs();
6777
+ categoryStats = await store.getConfigStats();
5999
6778
  } catch {
6000
6779
  databaseReachable = false;
6001
6780
  }
@@ -6020,10 +6799,25 @@ function getConfigsStatus(db = getDatabase()) {
6020
6799
  driftedTargets += 1;
6021
6800
  }
6022
6801
  }
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;
6802
+ let profiles = 0;
6803
+ let machines = 0;
6804
+ let profileLinks = 0;
6805
+ let snapshots = 0;
6806
+ if (databaseReachable) {
6807
+ try {
6808
+ const profileList = await store.listProfiles();
6809
+ profiles = profileList.length;
6810
+ machines = (await store.listMachines()).length;
6811
+ for (const profile of profileList) {
6812
+ profileLinks += (await store.getProfileConfigs(profile.id)).length;
6813
+ }
6814
+ for (const config of configs) {
6815
+ snapshots += (await store.listSnapshots(config.id)).length;
6816
+ }
6817
+ } catch {
6818
+ databaseReachable = false;
6819
+ }
6820
+ }
6027
6821
  const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
6028
6822
  const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
6029
6823
  return {
@@ -6077,425 +6871,8 @@ function getConfigsStatus(db = getDatabase()) {
6077
6871
  };
6078
6872
  }
6079
6873
 
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
- }
6874
+ // src/cli/index.tsx
6875
+ init_config_store();
6499
6876
 
6500
6877
  // src/lib/compact-output.ts
6501
6878
  var DEFAULT_LIST_LIMIT = 20;
@@ -6548,6 +6925,32 @@ function truncateMiddle(value, max = 80) {
6548
6925
  // src/cli/index.tsx
6549
6926
  import { createRequire } from "module";
6550
6927
  var pkg = createRequire(import.meta.url)("../../package.json");
6928
+ var EAGAIN_SLEEP = new Int32Array(new SharedArrayBuffer(4));
6929
+ function writeStdout(text) {
6930
+ const buf = Buffer.from(text, "utf8");
6931
+ let offset = 0;
6932
+ while (offset < buf.length) {
6933
+ try {
6934
+ offset += writeSync(1, buf, offset);
6935
+ } catch (e) {
6936
+ const code = e.code;
6937
+ if (code === "EAGAIN") {
6938
+ Atomics.wait(EAGAIN_SLEEP, 0, 0, 1);
6939
+ continue;
6940
+ }
6941
+ if (code === "EPIPE")
6942
+ return;
6943
+ throw e;
6944
+ }
6945
+ }
6946
+ }
6947
+ function printLine(text = "") {
6948
+ writeStdout(`${text}
6949
+ `);
6950
+ }
6951
+ function printJson(value) {
6952
+ printLine(JSON.stringify(value, null, 2));
6953
+ }
6551
6954
  function fmtConfig(c, format) {
6552
6955
  if (format === "json")
6553
6956
  return JSON.stringify(c, null, 2);
@@ -6574,9 +6977,9 @@ function pageFooter(command, page, detailsHint) {
6574
6977
  function printConfigRows(configs) {
6575
6978
  console.log(`${pad("slug", 32)} ${pad("type", 15)} ${pad("fmt", 8)} ${pad("path", 44)} out v`);
6576
6979
  for (const c of configs) {
6577
- const type = `${c.category}/${c.agent}`;
6980
+ const type2 = `${c.category}/${c.agent}`;
6578
6981
  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}`);
6982
+ 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
6983
  }
6581
6984
  }
6582
6985
  function splitCsv(value) {
@@ -6611,11 +7014,11 @@ function parseSessionSource(value, order, replaceIds) {
6611
7014
  if (!path)
6612
7015
  throw new Error(`Invalid --source "${value}" (expected path or id=path)`);
6613
7016
  const absPath = resolveSessionPath(path);
6614
- if (!existsSync12(absPath))
7017
+ if (!existsSync13(absPath))
6615
7018
  throw new Error(`Instruction source file not found: ${absPath}`);
6616
- const content = readFileSync8(absPath, "utf-8");
7019
+ const content = readFileSync9(absPath, "utf-8");
6617
7020
  const source = sourceFromFilePath(absPath, content, order);
6618
- const resolvedId = id || source.id || basename5(absPath);
7021
+ const resolvedId = id || source.id || basename6(absPath);
6619
7022
  return {
6620
7023
  ...source,
6621
7024
  id: resolvedId,
@@ -6640,18 +7043,18 @@ function parseLayeredReference(value) {
6640
7043
  throw new Error("Instruction reference cannot be empty.");
6641
7044
  return { id: trimmed };
6642
7045
  }
6643
- function collectSessionSources(opts, tool) {
7046
+ async function collectSessionSources(opts, tool, store) {
6644
7047
  const replaceIds = new Set(opts.replaceSource ?? []);
6645
7048
  const sources = (opts.source ?? []).map((value, index) => parseSessionSource(value, index, replaceIds));
6646
7049
  for (const value of opts.config ?? []) {
6647
7050
  const { layer, id } = parseLayeredReference(value);
6648
- sources.push(sourceFromConfig(getConfig(id), sources.length, layer));
7051
+ sources.push(sourceFromConfig(await store.getConfig(id), sources.length, layer));
6649
7052
  }
6650
7053
  for (const value of opts.identityExport ?? []) {
6651
7054
  const path = resolveSessionPath(value);
6652
- if (!existsSync12(path))
7055
+ if (!existsSync13(path))
6653
7056
  throw new Error(`Identity instruction export not found: ${path}`);
6654
- const parsed = JSON.parse(readFileSync8(path, "utf-8"));
7057
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
6655
7058
  sources.push(...sourcesFromIdentityExport(parsed, { path, tool, orderOffset: sources.length }));
6656
7059
  }
6657
7060
  return sources.map((source) => replaceIds.has(source.id) ? { ...source, merge: "replace" } : source);
@@ -6683,12 +7086,12 @@ function parseVarArgs(values) {
6683
7086
  function parseProfileSelectors(opts) {
6684
7087
  const selectors = {};
6685
7088
  const os = splitCsv(opts.os);
6686
- const arch = splitCsv(opts.arch);
7089
+ const arch2 = splitCsv(opts.arch);
6687
7090
  const hostnames = splitCsv(opts.hostname);
6688
7091
  if (os)
6689
7092
  selectors.os = os;
6690
- if (arch)
6691
- selectors.arch = arch;
7093
+ if (arch2)
7094
+ selectors.arch = arch2;
6692
7095
  if (hostnames)
6693
7096
  selectors.hostnames = hostnames;
6694
7097
  return Object.keys(selectors).length > 0 ? selectors : undefined;
@@ -6706,14 +7109,14 @@ function formatProfileSelectorSummary(profile) {
6706
7109
  function formatProfileVariables(profile) {
6707
7110
  return Object.entries(profile.variables).map(([key, value]) => `${key}=${value}`).join(", ");
6708
7111
  }
6709
- function getMachineProfileContext(opts) {
7112
+ async function getMachineProfileContext(opts, store) {
6710
7113
  const machine = detectMachineContext({ hostname: opts.hostname, os: opts.os, arch: opts.arch });
6711
- const profile = resolveProfileForMachine(machine);
7114
+ const profile = await store.resolveProfileForMachine(machine);
6712
7115
  return { machine, profile, vars: resolveProfileVariables(profile, machine) };
6713
7116
  }
6714
7117
  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
7118
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6716
- const configs = listConfigs({
7119
+ const configs = await resolveConfigStore().listConfigs({
6717
7120
  category: opts.category,
6718
7121
  agent: opts.agent,
6719
7122
  kind: opts.kind,
@@ -6725,7 +7128,7 @@ program.command("list").alias("ls").description("List stored configs").option("-
6725
7128
  return;
6726
7129
  }
6727
7130
  if (fmt === "json") {
6728
- console.log(JSON.stringify(configs, null, 2));
7131
+ printJson(configs);
6729
7132
  return;
6730
7133
  }
6731
7134
  const page = paginate(configs, { limit: opts.limit, cursor: opts.cursor });
@@ -6741,37 +7144,37 @@ program.command("list").alias("ls").description("List stored configs").option("-
6741
7144
  });
6742
7145
  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
7146
  try {
6744
- const c = getConfig(id);
7147
+ const c = await resolveConfigStore().getConfig(id);
6745
7148
  if (opts.format === "json") {
6746
- console.log(JSON.stringify(c, null, 2));
7149
+ printJson(c);
6747
7150
  return;
6748
7151
  }
6749
7152
  if (opts.format === "content") {
6750
- console.log(c.content);
7153
+ printLine(c.content);
6751
7154
  return;
6752
7155
  }
6753
7156
  console.log(fmtConfig(c, "table"));
6754
7157
  console.log();
6755
7158
  console.log(chalk.bold("Content:"));
6756
7159
  console.log(chalk.dim("\u2500".repeat(60)));
6757
- console.log(c.content);
7160
+ printLine(c.content);
6758
7161
  } catch (e) {
6759
7162
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
6760
7163
  process.exit(1);
6761
7164
  }
6762
7165
  });
6763
7166
  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)) {
7167
+ const abs = resolve7(filePath);
7168
+ if (!existsSync13(abs)) {
6766
7169
  console.error(chalk.red(`File not found: ${abs}`));
6767
7170
  process.exit(1);
6768
7171
  }
6769
- const rawContent = readFileSync8(abs, "utf-8");
7172
+ const rawContent = readFileSync9(abs, "utf-8");
6770
7173
  const fmt = detectFormat(abs);
6771
7174
  const { content, redacted, isTemplate: isTemplate2 } = redactContent(rawContent, fmt);
6772
- const targetPath = abs.startsWith(homedir6()) ? abs.replace(homedir6(), "~") : abs;
7175
+ const targetPath = abs.startsWith(homedir7()) ? abs.replace(homedir7(), "~") : abs;
6773
7176
  const name = opts.name || filePath.split("/").pop();
6774
- const config = createConfig({
7177
+ const config = await resolveConfigStore().createConfig({
6775
7178
  name,
6776
7179
  kind: opts.kind ?? "file",
6777
7180
  category: opts.category ?? detectCategory(abs),
@@ -6789,10 +7192,26 @@ program.command("add <path>").description("Ingest a file into the config DB").op
6789
7192
  console.log(chalk.dim(" Config stored as a template. Use `configs template vars` to see placeholders."));
6790
7193
  }
6791
7194
  });
7195
+ 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) => {
7196
+ try {
7197
+ const store = resolveConfigStore();
7198
+ const config = await store.getConfig(id);
7199
+ await store.deleteConfig(config.id);
7200
+ if (opts.json) {
7201
+ printJson({ deleted: true, id: config.id, slug: config.slug });
7202
+ return;
7203
+ }
7204
+ console.log(chalk.green("\u2713") + ` Deleted: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
7205
+ } catch (e) {
7206
+ console.error(chalk.red(e instanceof Error ? e.message : String(e)));
7207
+ process.exit(1);
7208
+ }
7209
+ });
6792
7210
  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
7211
  try {
6794
- const config = getConfig(id);
6795
- const result = await applyConfig(config, { dryRun: opts.dryRun });
7212
+ const store = resolveConfigStore();
7213
+ const config = await store.getConfig(id);
7214
+ const result = await applyConfig(config, { dryRun: opts.dryRun, store });
6796
7215
  const status = opts.dryRun ? chalk.yellow("[dry-run]") : result.changed ? chalk.green("\u2713") : chalk.dim("=");
6797
7216
  const change = result.changed ? "changed" : "unchanged";
6798
7217
  console.log(`${status} ${result.path} ${chalk.dim(`(${change})`)}`);
@@ -6808,17 +7227,18 @@ program.command("apply <id>").description("Apply a config to its target_path and
6808
7227
  });
6809
7228
  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
7229
  try {
7230
+ const store = resolveConfigStore();
6811
7231
  if (id) {
6812
- const config = getConfig(id);
6813
- console.log(diffConfig(config));
7232
+ const config = await store.getConfig(id);
7233
+ console.log(await diffConfig(config, { store }));
6814
7234
  return;
6815
7235
  }
6816
- const configs = listConfigs({ kind: "file" });
7236
+ const configs = await store.listConfigs({ kind: "file" });
6817
7237
  let drifted = 0;
6818
7238
  for (const c of configs) {
6819
7239
  if (!c.target_path)
6820
7240
  continue;
6821
- const diff = diffConfig(c);
7241
+ const diff = await diffConfig(c, { store });
6822
7242
  if (diff.includes("no diff") || diff.includes("not found"))
6823
7243
  continue;
6824
7244
  drifted++;
@@ -6833,6 +7253,7 @@ program.command("diff [id]").description("Show diff between stored config and di
6833
7253
  }
6834
7254
  });
6835
7255
  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) => {
7256
+ const store = resolveConfigStore();
6836
7257
  if (opts.list) {
6837
7258
  const targets = KNOWN_CONFIGS.filter((k) => {
6838
7259
  if (opts.agent && k.agent !== opts.agent)
@@ -6853,18 +7274,18 @@ program.command("sync").description("Sync known AI coding configs from disk into
6853
7274
  if (opts.project) {
6854
7275
  const dir = typeof opts.project === "string" ? opts.project : process.cwd();
6855
7276
  if (opts.all) {
6856
- const { readdirSync: readdirSync3, statSync: st } = await import("fs");
7277
+ const { readdirSync: readdirSync4, statSync: st } = await import("fs");
6857
7278
  const absDir = expandPath(dir);
6858
- const entries = readdirSync3(absDir, { withFileTypes: true });
7279
+ const entries = readdirSync4(absDir, { withFileTypes: true });
6859
7280
  let totalAdded = 0, totalUpdated = 0, totalUnchanged = 0, projects = 0;
6860
7281
  for (const entry of entries) {
6861
7282
  if (!entry.isDirectory())
6862
7283
  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"));
7284
+ const projDir = join11(absDir, entry.name);
7285
+ const hasClaude = existsSync13(join11(projDir, "CLAUDE.md")) || existsSync13(join11(projDir, ".mcp.json")) || existsSync13(join11(projDir, ".claude"));
6865
7286
  if (!hasClaude)
6866
7287
  continue;
6867
- const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun });
7288
+ const result2 = await syncProject({ projectDir: projDir, dryRun: opts.dryRun, store });
6868
7289
  if (result2.added + result2.updated > 0) {
6869
7290
  console.log(` ${chalk.green("\u2713")} ${entry.name}: +${result2.added} updated:${result2.updated}`);
6870
7291
  }
@@ -6876,15 +7297,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
6876
7297
  console.log(chalk.green("\u2713") + ` Synced ${projects} projects: +${totalAdded} updated:${totalUpdated} unchanged:${totalUnchanged}`);
6877
7298
  return;
6878
7299
  }
6879
- const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun });
7300
+ const result = await syncProject({ projectDir: dir, dryRun: opts.dryRun, store });
6880
7301
  console.log(chalk.green("\u2713") + ` Project sync: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
6881
7302
  return;
6882
7303
  }
6883
7304
  if (opts.toDisk) {
6884
- const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
7305
+ const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category, store });
6885
7306
  console.log(chalk.green("\u2713") + ` Written to disk: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
6886
7307
  } else {
6887
- const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category });
7308
+ const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, category: opts.category, store });
6888
7309
  console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
6889
7310
  if (result.skipped.length > 0) {
6890
7311
  console.log(chalk.dim(" skipped (not found): " + result.skipped.join(", ")));
@@ -6893,13 +7314,15 @@ program.command("sync").description("Sync known AI coding configs from disk into
6893
7314
  });
6894
7315
  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
7316
  const result = await exportConfigs(opts.output, {
6896
- filter: opts.category ? { category: opts.category } : undefined
7317
+ filter: opts.category ? { category: opts.category } : undefined,
7318
+ store: resolveConfigStore()
6897
7319
  });
6898
7320
  console.log(chalk.green("\u2713") + ` Exported ${result.count} configs to ${result.path}`);
6899
7321
  });
6900
7322
  program.command("import <file>").description("Import configs from a tar.gz bundle").option("--overwrite", "overwrite existing configs").action(async (file, opts) => {
6901
7323
  const result = await importConfigs(file, {
6902
- conflict: opts.overwrite ? "overwrite" : "skip"
7324
+ conflict: opts.overwrite ? "overwrite" : "skip",
7325
+ store: resolveConfigStore()
6903
7326
  });
6904
7327
  console.log(chalk.green("\u2713") + ` Import complete: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
6905
7328
  if (result.errors.length > 0) {
@@ -6909,10 +7332,11 @@ program.command("import <file>").description("Import configs from a tar.gz bundl
6909
7332
  }
6910
7333
  });
6911
7334
  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();
7335
+ const store = resolveConfigStore();
7336
+ const dbPath = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["CONFIGS_DB_PATH"] || join11(homedir7(), ".hasna", "configs", "configs.db");
7337
+ const stats = await store.getConfigStats();
6914
7338
  console.log(chalk.bold("@hasna/configs") + chalk.dim(" v" + pkg.version));
6915
- console.log(chalk.cyan("DB:") + " " + dbPath);
7339
+ console.log(chalk.cyan(isCloudMode() ? "API:" : "DB:") + " " + dbPath);
6916
7340
  console.log(chalk.cyan("Total configs:") + " " + (stats["total"] || 0));
6917
7341
  console.log();
6918
7342
  console.log(chalk.bold("By category:"));
@@ -6922,7 +7346,7 @@ program.command("whoami").description("Show setup summary").action(async () => {
6922
7346
  if (count > 0)
6923
7347
  console.log(` ${chalk.cyan(cat.padEnd(16))} ${count}`);
6924
7348
  }
6925
- const profiles = listProfiles();
7349
+ const profiles = await store.listProfiles();
6926
7350
  if (profiles.length > 0) {
6927
7351
  console.log();
6928
7352
  console.log(chalk.bold("Profiles:") + chalk.dim(` (${profiles.length})`));
@@ -6933,13 +7357,14 @@ program.command("whoami").description("Show setup summary").action(async () => {
6933
7357
  var profileCmd = program.command("profile").description("Manage config profiles (named bundles)");
6934
7358
  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
7359
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
6936
- const profiles = listProfiles();
7360
+ const store = resolveConfigStore();
7361
+ const profiles = await store.listProfiles();
6937
7362
  if (profiles.length === 0) {
6938
7363
  console.log(chalk.dim("No profiles."));
6939
7364
  return;
6940
7365
  }
6941
7366
  if (fmt === "json") {
6942
- console.log(JSON.stringify(profiles, null, 2));
7367
+ printJson(profiles);
6943
7368
  return;
6944
7369
  }
6945
7370
  const page = paginate(profiles, { limit: opts.limit, cursor: opts.cursor });
@@ -6948,10 +7373,10 @@ profileCmd.command("list").description("List all profiles").option("--brief", "c
6948
7373
  for (const p of page.items) {
6949
7374
  if (fmt === "compact") {
6950
7375
  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}`);
7376
+ console.log(`${pad(p.slug, 28)} ${pad(String((await store.getProfileConfigs(p.id)).length), 8)} ${pad(selectorSummary2 || "-", 36)} ${Object.keys(p.variables).length}`);
6952
7377
  continue;
6953
7378
  }
6954
- const configs = getProfileConfigs(p.id);
7379
+ const configs = await store.getProfileConfigs(p.id);
6955
7380
  console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configs.length} config(s)`);
6956
7381
  if (p.description)
6957
7382
  console.log(` ${chalk.dim(p.description)}`);
@@ -6965,7 +7390,7 @@ profileCmd.command("list").description("List all profiles").option("--brief", "c
6965
7390
  pageFooter("configs profile list", page, "Use --verbose for expanded rows, --json for full records, or `configs profile show <slug>` for details.");
6966
7391
  });
6967
7392
  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({
7393
+ const p = await resolveConfigStore().createProfile({
6969
7394
  name,
6970
7395
  description: opts.description,
6971
7396
  selectors: parseProfileSelectors(opts),
@@ -6975,8 +7400,9 @@ profileCmd.command("create <name>").description("Create a new profile").option("
6975
7400
  });
6976
7401
  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
7402
  try {
6978
- const p = getProfile(id);
6979
- const configs = getProfileConfigs(id);
7403
+ const store = resolveConfigStore();
7404
+ const p = await store.getProfile(id);
7405
+ const configs = await store.getProfileConfigs(id);
6980
7406
  console.log(chalk.bold(p.name) + chalk.dim(` (${p.slug})`));
6981
7407
  if (p.description)
6982
7408
  console.log(chalk.dim(p.description));
@@ -7000,8 +7426,9 @@ profileCmd.command("show <id>").description("Show profile and its configs").opti
7000
7426
  });
7001
7427
  profileCmd.command("add <profile> <config>").description("Add a config to a profile").action(async (profile, config) => {
7002
7428
  try {
7003
- const c = getConfig(config);
7004
- addConfigToProfile(profile, c.id);
7429
+ const store = resolveConfigStore();
7430
+ const c = await store.getConfig(config);
7431
+ await store.addConfigToProfile(profile, c.id);
7005
7432
  console.log(chalk.green("\u2713") + ` Added ${c.slug} to profile ${profile}`);
7006
7433
  } catch (e) {
7007
7434
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7010,8 +7437,9 @@ profileCmd.command("add <profile> <config>").description("Add a config to a prof
7010
7437
  });
7011
7438
  profileCmd.command("remove <profile> <config>").description("Remove a config from a profile").action(async (profile, config) => {
7012
7439
  try {
7013
- const c = getConfig(config);
7014
- removeConfigFromProfile(profile, c.id);
7440
+ const store = resolveConfigStore();
7441
+ const c = await store.getConfig(config);
7442
+ await store.removeConfigFromProfile(profile, c.id);
7015
7443
  console.log(chalk.green("\u2713") + ` Removed ${c.slug} from profile ${profile}`);
7016
7444
  } catch (e) {
7017
7445
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7020,15 +7448,16 @@ profileCmd.command("remove <profile> <config>").description("Remove a config fro
7020
7448
  });
7021
7449
  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
7450
  try {
7023
- const { machine, profile } = getMachineProfileContext(opts);
7024
- const selected = opts.auto ? profile : id ? getProfile(id) : null;
7451
+ const store = resolveConfigStore();
7452
+ const { machine, profile } = await getMachineProfileContext(opts, store);
7453
+ const selected = opts.auto ? profile : id ? await store.getProfile(id) : null;
7025
7454
  if (!selected) {
7026
7455
  console.error(chalk.red(opts.auto ? "No matching machine-aware profile found." : "Provide a profile id or use --auto."));
7027
7456
  process.exit(1);
7028
7457
  }
7029
- const configs = getProfileConfigs(selected.id);
7458
+ const configs = await store.getProfileConfigs(selected.id);
7030
7459
  const vars = resolveProfileVariables(selected, machine);
7031
- const results = await applyConfigs(configs, { dryRun: opts.dryRun, vars });
7460
+ const results = await applyConfigs(configs, { dryRun: opts.dryRun, vars, store });
7032
7461
  let changed = 0;
7033
7462
  for (const r of results) {
7034
7463
  const status = opts.dryRun ? chalk.yellow("[dry-run]") : r.changed ? chalk.green("\u2713") : chalk.dim("=");
@@ -7044,7 +7473,8 @@ ${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${
7044
7473
  }
7045
7474
  });
7046
7475
  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);
7476
+ const store = resolveConfigStore();
7477
+ const { machine, profile, vars } = await getMachineProfileContext(opts, store);
7048
7478
  if (!profile) {
7049
7479
  console.log(chalk.yellow(`No matching profile for ${machine.hostname} ${machine.os_family}/${machine.arch}`));
7050
7480
  process.exit(1);
@@ -7061,8 +7491,9 @@ profileCmd.command("resolve").description("Resolve the matching machine-aware pr
7061
7491
  });
7062
7492
  profileCmd.command("delete <id>").description("Delete a profile").action(async (id) => {
7063
7493
  try {
7064
- const p = getProfile(id);
7065
- deleteProfile(id);
7494
+ const store = resolveConfigStore();
7495
+ const p = await store.getProfile(id);
7496
+ await store.deleteProfile(p.id);
7066
7497
  console.log(chalk.green("\u2713") + ` Deleted profile: ${p.name}`);
7067
7498
  } catch (e) {
7068
7499
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7077,7 +7508,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
7077
7508
  console.error(chalk.red(`Unsupported tool: ${opts.tool}`));
7078
7509
  process.exit(1);
7079
7510
  }
7080
- const sources = collectSessionSources(opts, tool);
7511
+ const sources = await collectSessionSources(opts, tool, resolveConfigStore());
7081
7512
  const plan = planSessionRender({
7082
7513
  tool,
7083
7514
  profile: opts.profile,
@@ -7088,7 +7519,7 @@ sessionCmd.command("plan").description("Produce a dry-run render plan for profil
7088
7519
  sources
7089
7520
  });
7090
7521
  if (opts.json) {
7091
- console.log(JSON.stringify(planJsonForOutput(plan), null, 2));
7522
+ printJson(planJsonForOutput(plan));
7092
7523
  return;
7093
7524
  }
7094
7525
  console.log(chalk.bold(`${plan.tool} session render plan`) + chalk.dim(` (${plan.adapter.mode})`));
@@ -7121,7 +7552,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7121
7552
  console.error(chalk.red(`Unsupported tool: ${opts.tool}`));
7122
7553
  process.exit(1);
7123
7554
  }
7124
- const sources = collectSessionSources(opts, tool);
7555
+ const sources = await collectSessionSources(opts, tool, resolveConfigStore());
7125
7556
  const plan = planSessionRender({
7126
7557
  tool,
7127
7558
  profile: opts.profile,
@@ -7133,7 +7564,7 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7133
7564
  });
7134
7565
  const result = applySessionRender(plan, { dryRun: opts.dryRun, force: opts.force });
7135
7566
  if (opts.json) {
7136
- console.log(JSON.stringify(result, null, 2));
7567
+ printJson(result);
7137
7568
  if (result.conflicts.length > 0)
7138
7569
  process.exitCode = 1;
7139
7570
  return;
@@ -7168,8 +7599,9 @@ sessionCmd.command("apply").description("Write a session render plan to its mana
7168
7599
  var snapshotCmd = program.command("snapshot").description("Manage config version history");
7169
7600
  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
7601
  try {
7171
- const c = getConfig(configId);
7172
- const snaps = listSnapshots(c.id);
7602
+ const store = resolveConfigStore();
7603
+ const c = await store.getConfig(configId);
7604
+ const snaps = await store.listSnapshots(c.id);
7173
7605
  if (snaps.length === 0) {
7174
7606
  console.log(chalk.dim("No snapshots."));
7175
7607
  return;
@@ -7185,21 +7617,22 @@ snapshotCmd.command("list <config>").description("List snapshots for a config").
7185
7617
  }
7186
7618
  });
7187
7619
  snapshotCmd.command("show <id>").description("Show a snapshot's content").action(async (id) => {
7188
- const snap = getSnapshot(id);
7620
+ const snap = await resolveConfigStore().getSnapshot(id);
7189
7621
  if (!snap) {
7190
7622
  console.error(chalk.red("Snapshot not found: " + id));
7191
7623
  process.exit(1);
7192
7624
  }
7193
- console.log(snap.content);
7625
+ printLine(snap.content);
7194
7626
  });
7195
7627
  snapshotCmd.command("restore <config> <snapshot-id>").description("Restore a config to a snapshot version").action(async (configId, snapId) => {
7196
7628
  try {
7197
- const snap = getSnapshot(snapId);
7629
+ const store = resolveConfigStore();
7630
+ const snap = await store.getSnapshot(snapId);
7198
7631
  if (!snap) {
7199
7632
  console.error(chalk.red("Snapshot not found: " + snapId));
7200
7633
  process.exit(1);
7201
7634
  }
7202
- updateConfig(configId, { content: snap.content });
7635
+ await store.updateConfig(configId, { content: snap.content });
7203
7636
  console.log(chalk.green("\u2713") + ` Restored ${configId} to snapshot v${snap.version}`);
7204
7637
  } catch (e) {
7205
7638
  console.error(chalk.red(e instanceof Error ? e.message : String(e)));
@@ -7209,7 +7642,7 @@ snapshotCmd.command("restore <config> <snapshot-id>").description("Restore a con
7209
7642
  var templateCmd = program.command("template").description("Work with template configs");
7210
7643
  templateCmd.command("vars <id>").description("Show template variables").action(async (id) => {
7211
7644
  try {
7212
- const c = getConfig(id);
7645
+ const c = await resolveConfigStore().getConfig(id);
7213
7646
  const vars = extractTemplateVars(c.content);
7214
7647
  if (vars.length === 0) {
7215
7648
  console.log(chalk.dim("No template variables found."));
@@ -7226,7 +7659,7 @@ templateCmd.command("vars <id>").description("Show template variables").action(a
7226
7659
  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
7660
  try {
7228
7661
  const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
7229
- const c = getConfig(id);
7662
+ const c = await resolveConfigStore().getConfig(id);
7230
7663
  const vars = {};
7231
7664
  if (opts.var) {
7232
7665
  for (const kv of opts.var) {
@@ -7257,9 +7690,9 @@ templateCmd.command("render <id>").description("Render a template config with va
7257
7690
  console.log(rendered);
7258
7691
  } else {
7259
7692
  const { writeFileSync: writeFileSync4, mkdirSync: mkdirSync6 } = await import("fs");
7260
- const { dirname: dirname4 } = await import("path");
7693
+ const { dirname: dirname5 } = await import("path");
7261
7694
  const path = expandPath(c.target_path);
7262
- mkdirSync6(dirname4(path), { recursive: true });
7695
+ mkdirSync6(dirname5(path), { recursive: true });
7263
7696
  writeFileSync4(path, rendered, "utf-8");
7264
7697
  console.log(chalk.green("\u2713") + ` Rendered and applied to ${path}`);
7265
7698
  }
@@ -7272,11 +7705,12 @@ templateCmd.command("render <id>").description("Render a template config with va
7272
7705
  }
7273
7706
  });
7274
7707
  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) => {
7708
+ const store = resolveConfigStore();
7275
7709
  let configs;
7276
7710
  if (id) {
7277
- configs = [getConfig(id)];
7711
+ configs = [await store.getConfig(id)];
7278
7712
  } else if (opts.all) {
7279
- configs = listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
7713
+ configs = await store.listConfigs(opts.category ? { kind: "file", category: opts.category } : { kind: "file" });
7280
7714
  } else {
7281
7715
  const { KNOWN_CONFIGS: KNOWN_CONFIGS2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
7282
7716
  const slugs = [
@@ -7285,10 +7719,10 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7285
7719
  const fetched = [];
7286
7720
  for (const slug2 of slugs) {
7287
7721
  try {
7288
- fetched.push(getConfig(slug2));
7722
+ fetched.push(await store.getConfig(slug2));
7289
7723
  } catch {}
7290
7724
  }
7291
- const rules = listConfigs({ category: "rules", agent: "claude" });
7725
+ const rules = await store.listConfigs({ category: "rules", agent: "claude" });
7292
7726
  for (const r of rules)
7293
7727
  if (!fetched.find((c) => c.id === r.id))
7294
7728
  fetched.push(r);
@@ -7318,7 +7752,7 @@ program.command("scan [id]").description("Scan configs for secrets. Defaults to
7318
7752
  }
7319
7753
  if (opts.fix) {
7320
7754
  const { content, isTemplate: isTemplate2 } = redactContent(c.content, fmt);
7321
- updateConfig(c.id, { content, is_template: isTemplate2 });
7755
+ await store.updateConfig(c.id, { content, is_template: isTemplate2 });
7322
7756
  if (visible.length > 0)
7323
7757
  console.log(chalk.green(" \u2713 Redacted."));
7324
7758
  }
@@ -7337,6 +7771,32 @@ Run with --fix to redact in-place.`));
7337
7771
  Redacted all ${total} finding(s); printed ${printed}. Re-run without --fix and a higher --limit for full details.`));
7338
7772
  }
7339
7773
  });
7774
+ 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) => {
7775
+ const { scanPackageManagerSecrets: scanPackageManagerSecrets2 } = await Promise.resolve().then(() => (init_package_manager_guard(), exports_package_manager_guard));
7776
+ const roots = paths && paths.length > 0 ? paths : [process.cwd()];
7777
+ const result = scanPackageManagerSecrets2({ roots, includeHome: !!opts.home });
7778
+ const maxPrinted = parseLimit(opts.limit, DEFAULT_LIST_LIMIT);
7779
+ const visible = result.findings.slice(0, maxPrinted);
7780
+ const omitted = Math.max(0, result.findings.length - visible.length);
7781
+ if (opts.json) {
7782
+ printJson(result);
7783
+ } else if (result.findings.length === 0) {
7784
+ console.log(chalk.green("\u2713") + ` Package-manager scan clean (${result.scannedFiles} file(s)).`);
7785
+ } else {
7786
+ console.log(chalk.red(`\u2717 ${result.findings.length} package-manager finding(s) detected.`));
7787
+ for (const finding of visible) {
7788
+ const tracked = finding.tracked ? "tracked" : "untracked";
7789
+ const color = finding.severity === "error" ? chalk.red : chalk.yellow;
7790
+ console.log(color(` ${finding.path}:${finding.line} ${finding.rule}`) + chalk.dim(` [${finding.surface}, ${tracked}] ${finding.detail}`));
7791
+ }
7792
+ if (omitted > 0)
7793
+ console.log(chalk.dim(` Omitted ${omitted} finding(s). Re-run with --limit ${result.findings.length} or --json.`));
7794
+ console.log(chalk.dim(" Secret values are never printed by this command."));
7795
+ }
7796
+ if (opts.failOnFindings && result.findings.length > 0) {
7797
+ process.exitCode = 1;
7798
+ }
7799
+ });
7340
7800
  var mcpCmd = program.command("mcp").description("Install/remove MCP server for AI agents");
7341
7801
  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
7802
  const targets = opts.all ? ["claude", "codex", "gemini"] : [
@@ -7350,7 +7810,7 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
7350
7810
  }
7351
7811
  for (const target of targets) {
7352
7812
  try {
7353
- const { vars } = getMachineProfileContext({});
7813
+ const { vars } = await getMachineProfileContext({}, resolveConfigStore());
7354
7814
  const mcpBinary = `${vars["BUN_BIN_DIR"]}/configs-mcp`;
7355
7815
  if (target === "claude") {
7356
7816
  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 +7820,14 @@ mcpCmd.command("install").alias("add").description("Install configs MCP server i
7360
7820
  } else if (target === "codex") {
7361
7821
  const { appendFileSync, existsSync: ex } = await import("fs");
7362
7822
  const { join: j } = await import("path");
7363
- const configPath = j(homedir6(), ".codex", "config.toml");
7823
+ const configPath = j(homedir7(), ".codex", "config.toml");
7364
7824
  const block = `
7365
7825
  [mcp_servers.configs]
7366
7826
  command = "${mcpBinary}"
7367
7827
  args = []
7368
7828
  `;
7369
7829
  if (ex(configPath)) {
7370
- const content = readFileSync8(configPath, "utf-8");
7830
+ const content = readFileSync9(configPath, "utf-8");
7371
7831
  if (content.includes("[mcp_servers.configs]")) {
7372
7832
  console.log(chalk.dim("= Already installed in Codex"));
7373
7833
  continue;
@@ -7378,7 +7838,7 @@ args = []
7378
7838
  } else if (target === "gemini") {
7379
7839
  const { readFileSync: rf, writeFileSync: wf, existsSync: ex } = await import("fs");
7380
7840
  const { join: j } = await import("path");
7381
- const configPath = j(homedir6(), ".gemini", "settings.json");
7841
+ const configPath = j(homedir7(), ".gemini", "settings.json");
7382
7842
  let settings = {};
7383
7843
  if (ex(configPath)) {
7384
7844
  try {
@@ -7405,16 +7865,14 @@ mcpCmd.command("uninstall").alias("remove").description("Remove configs MCP serv
7405
7865
  }
7406
7866
  });
7407
7867
  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();
7868
+ const store = resolveConfigStore();
7869
+ if (opts.force) {
7870
+ await store.reset();
7871
+ console.log(chalk.dim("Reset local store."));
7414
7872
  }
7415
7873
  console.log(chalk.bold(`@hasna/configs \u2014 initializing
7416
7874
  `));
7417
- const result = await syncKnown({});
7875
+ const result = await syncKnown({ store });
7418
7876
  console.log(chalk.green("\u2713") + ` Synced: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
7419
7877
  if (result.skipped.length > 0) {
7420
7878
  console.log(chalk.dim(" skipped: " + result.skipped.join(", ")));
@@ -7432,37 +7890,38 @@ Keys: ANTHROPIC_API_KEY, OPENAI_API_KEY, EXA_API_KEY, NPM_TOKEN, GITHUB_TOKEN`,
7432
7890
  ];
7433
7891
  for (const ref of refs) {
7434
7892
  try {
7435
- getConfig(ref.slug);
7893
+ await store.getConfig(ref.slug);
7436
7894
  } catch {
7437
- createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
7895
+ await store.createConfig({ name: ref.name, category: ref.category, agent: "global", format: "markdown", content: ref.content, kind: "reference", description: ref.desc });
7438
7896
  }
7439
7897
  }
7440
- ensureProjectDashboardStandardConfig();
7898
+ await ensureProjectDashboardStandardConfig(store);
7441
7899
  try {
7442
- getProfile("my-setup");
7900
+ await store.getProfile("my-setup");
7443
7901
  } catch {
7444
- const p = createProfile({ name: "my-setup", description: "Default profile with all known configs" });
7445
- const allConfigs = listConfigs();
7902
+ const p = await store.createProfile({ name: "my-setup", description: "Default profile with all known configs" });
7903
+ const allConfigs = await store.listConfigs();
7446
7904
  for (const c of allConfigs)
7447
- addConfigToProfile(p.id, c.id);
7905
+ await store.addConfigToProfile(p.id, c.id);
7448
7906
  console.log(chalk.green("\u2713") + ` Created profile "my-setup" with ${allConfigs.length} configs`);
7449
7907
  }
7450
- const machineProfiles = ensurePlatformProfiles();
7908
+ const machineProfiles = await ensurePlatformProfiles(store);
7451
7909
  console.log(chalk.green("\u2713") + ` Ensured ${machineProfiles.length} machine-aware profile(s)`);
7452
- const stats = getConfigStats();
7910
+ const stats = await store.getConfigStats();
7453
7911
  console.log(chalk.bold(`
7454
7912
  DB stats:`));
7455
7913
  for (const [key, count] of Object.entries(stats)) {
7456
7914
  if (count > 0)
7457
7915
  console.log(` ${key.padEnd(18)} ${count}`);
7458
7916
  }
7917
+ const location = isCloudMode() ? `${process.env["HASNA_INSTRUCTIONS_API_URL"]}/v1 (self_hosted)` : process.env["CONFIGS_DB_PATH"] || join11(homedir7(), ".hasna", "configs", "configs.db");
7459
7918
  console.log(chalk.dim(`
7460
- DB: ${dbPath}`));
7919
+ ${isCloudMode() ? "API" : "DB"}: ${location}`));
7461
7920
  });
7462
7921
  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();
7922
+ const status = await getConfigsStatus(resolveConfigStore());
7464
7923
  if (opts.json) {
7465
- console.log(JSON.stringify(status, null, 2));
7924
+ printJson(status);
7466
7925
  return;
7467
7926
  }
7468
7927
  console.log(chalk.bold("@hasna/configs") + chalk.dim(` v${pkg.version}`));
@@ -7476,17 +7935,17 @@ program.command("status").description("Health check: total configs, drift from d
7476
7935
  });
7477
7936
  program.command("backup").description("Export configs to a timestamped backup file").action(async () => {
7478
7937
  const { mkdirSync: mk } = await import("fs");
7479
- const backupDir = join10(homedir6(), ".hasna", "configs", "backups");
7938
+ const backupDir = join11(homedir7(), ".hasna", "configs", "backups");
7480
7939
  mk(backupDir, { recursive: true });
7481
7940
  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);
7941
+ const outPath = join11(backupDir, `configs-${ts}.tar.gz`);
7942
+ const result = await exportConfigs(outPath, { store: resolveConfigStore() });
7484
7943
  const { statSync: st } = await import("fs");
7485
7944
  const size = st(outPath).size;
7486
7945
  console.log(chalk.green("\u2713") + ` Backup: ${result.count} configs \u2192 ${outPath} (${(size / 1024).toFixed(1)}KB)`);
7487
7946
  });
7488
7947
  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" });
7948
+ const result = await importConfigs(file, { conflict: opts.overwrite ? "overwrite" : "skip", store: resolveConfigStore() });
7490
7949
  console.log(chalk.green("\u2713") + ` Restored: +${result.created} updated:${result.updated} skipped:${result.skipped}`);
7491
7950
  if (result.errors.length > 0) {
7492
7951
  for (const e of result.errors)
@@ -7494,6 +7953,7 @@ program.command("restore <file>").description("Restore configs from a backup fil
7494
7953
  }
7495
7954
  });
7496
7955
  program.command("doctor").description("Validate configs: syntax, permissions, missing files, secrets").action(async () => {
7956
+ const store = resolveConfigStore();
7497
7957
  let issues = 0;
7498
7958
  const pass = (msg) => console.log(chalk.green(" \u2713 ") + msg);
7499
7959
  const fail = (msg) => {
@@ -7506,12 +7966,12 @@ program.command("doctor").description("Validate configs: syntax, permissions, mi
7506
7966
  console.log(chalk.cyan("Known files on disk:"));
7507
7967
  for (const k of KNOWN_CONFIGS) {
7508
7968
  if (k.rulesDir) {
7509
- existsSync12(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
7969
+ existsSync13(expandPath(k.rulesDir)) ? pass(`${k.rulesDir}/ exists`) : k.optional ? skip(`${k.rulesDir}/ (optional)`) : fail(`${k.rulesDir}/ not found`);
7510
7970
  } else {
7511
- existsSync12(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
7971
+ existsSync13(expandPath(k.path)) ? pass(k.path) : k.optional ? skip(`${k.path} (optional)`) : fail(`${k.path} not found`);
7512
7972
  }
7513
7973
  }
7514
- const allConfigs = listConfigs();
7974
+ const allConfigs = await store.listConfigs();
7515
7975
  console.log(chalk.cyan(`
7516
7976
  Stored configs (${allConfigs.length}):`));
7517
7977
  let validCount = 0;
@@ -7581,8 +8041,9 @@ complete -F _configs_completions configs`);
7581
8041
  });
7582
8042
  program.command("compare <a> <b>").description("Diff two stored configs against each other").action(async (a, b) => {
7583
8043
  try {
7584
- const configA = getConfig(a);
7585
- const configB = getConfig(b);
8044
+ const store = resolveConfigStore();
8045
+ const configA = await store.getConfig(a);
8046
+ const configB = await store.getConfig(b);
7586
8047
  console.log(chalk.bold(`${configA.slug}`) + chalk.dim(` (${configA.category}/${configA.agent})`));
7587
8048
  console.log(chalk.bold(`${configB.slug}`) + chalk.dim(` (${configB.category}/${configB.agent})`));
7588
8049
  console.log();
@@ -7621,6 +8082,7 @@ ${diffs} difference(s)`));
7621
8082
  }
7622
8083
  });
7623
8084
  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) => {
8085
+ const store = resolveConfigStore();
7624
8086
  const interval = Number(opts.interval);
7625
8087
  const { statSync: st } = await import("fs");
7626
8088
  const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
@@ -7631,16 +8093,16 @@ program.command("watch").description("Watch known config files for changes and a
7631
8093
  for (const k of KNOWN_CONFIGS) {
7632
8094
  if (k.rulesDir) {
7633
8095
  const absDir = expandPath2(k.rulesDir);
7634
- if (!existsSync12(absDir))
8096
+ if (!existsSync13(absDir))
7635
8097
  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);
8098
+ const { readdirSync: readdirSync4 } = await import("fs");
8099
+ for (const f of readdirSync4(absDir).filter((f2) => f2.endsWith(".md"))) {
8100
+ const abs = join11(absDir, f);
7639
8101
  mtimes.set(abs, st(abs).mtimeMs);
7640
8102
  }
7641
8103
  } else {
7642
8104
  const abs = expandPath2(k.path);
7643
- if (existsSync12(abs))
8105
+ if (existsSync13(abs))
7644
8106
  mtimes.set(abs, st(abs).mtimeMs);
7645
8107
  }
7646
8108
  }
@@ -7648,7 +8110,7 @@ program.command("watch").description("Watch known config files for changes and a
7648
8110
  const tick = async () => {
7649
8111
  let changed = 0;
7650
8112
  for (const [abs, oldMtime] of mtimes) {
7651
- if (!existsSync12(abs))
8113
+ if (!existsSync13(abs))
7652
8114
  continue;
7653
8115
  const newMtime = st(abs).mtimeMs;
7654
8116
  if (newMtime !== oldMtime) {
@@ -7660,10 +8122,10 @@ program.command("watch").description("Watch known config files for changes and a
7660
8122
  for (const k of KNOWN_CONFIGS) {
7661
8123
  if (k.rulesDir) {
7662
8124
  const absDir = expandPath2(k.rulesDir);
7663
- if (!existsSync12(absDir))
8125
+ if (!existsSync13(absDir))
7664
8126
  continue;
7665
8127
  for (const f of rd(absDir).filter((f2) => f2.endsWith(".md"))) {
7666
- const abs = join10(absDir, f);
8128
+ const abs = join11(absDir, f);
7667
8129
  if (!mtimes.has(abs)) {
7668
8130
  mtimes.set(abs, st(abs).mtimeMs);
7669
8131
  changed++;
@@ -7671,14 +8133,14 @@ program.command("watch").description("Watch known config files for changes and a
7671
8133
  }
7672
8134
  } else {
7673
8135
  const abs = expandPath2(k.path);
7674
- if (existsSync12(abs) && !mtimes.has(abs)) {
8136
+ if (existsSync13(abs) && !mtimes.has(abs)) {
7675
8137
  mtimes.set(abs, st(abs).mtimeMs);
7676
8138
  changed++;
7677
8139
  }
7678
8140
  }
7679
8141
  }
7680
8142
  if (changed > 0) {
7681
- const result = await syncKnown({});
8143
+ const result = await syncKnown({ store });
7682
8144
  const ts = new Date().toLocaleTimeString();
7683
8145
  console.log(`${chalk.dim(ts)} ${chalk.green("\u2713")} ${changed} file(s) changed/new \u2192 synced +${result.added} updated:${result.updated}`);
7684
8146
  }
@@ -7687,22 +8149,23 @@ program.command("watch").description("Watch known config files for changes and a
7687
8149
  await new Promise(() => {});
7688
8150
  });
7689
8151
  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();
8152
+ const store = resolveConfigStore();
8153
+ const stats = await store.getConfigStats();
8154
+ const allConfigs = await store.listConfigs();
7692
8155
  const fileConfigs = allConfigs.filter((c) => c.kind === "file");
7693
8156
  const refConfigs = allConfigs.filter((c) => c.kind === "reference");
7694
8157
  const templates = allConfigs.filter((c) => c.is_template);
7695
- const profiles = listProfiles();
8158
+ const profiles = await store.listProfiles();
7696
8159
  let drifted = 0, missing = 0;
7697
8160
  for (const c of fileConfigs) {
7698
8161
  if (!c.target_path)
7699
8162
  continue;
7700
8163
  const abs = expandPath(c.target_path);
7701
- if (!existsSync12(abs)) {
8164
+ if (!existsSync13(abs)) {
7702
8165
  missing++;
7703
8166
  continue;
7704
8167
  }
7705
- const disk = readFileSync8(abs, "utf-8");
8168
+ const disk = readFileSync9(abs, "utf-8");
7706
8169
  const { content: redactedDisk } = redactContent(disk, c.format);
7707
8170
  if (redactedDisk !== c.content)
7708
8171
  drifted++;
@@ -7734,7 +8197,8 @@ program.command("report").description("Summary of stored configs, drift, and eco
7734
8197
  }
7735
8198
  });
7736
8199
  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" });
8200
+ const store = resolveConfigStore();
8201
+ const configs = await store.listConfigs({ kind: "file" });
7738
8202
  let removed = 0;
7739
8203
  let printed = 0;
7740
8204
  const maxPrinted = parseLimit(opts.limit, DEFAULT_LIST_LIMIT);
@@ -7742,7 +8206,7 @@ program.command("clean").description("Remove configs from DB whose target files
7742
8206
  if (!c.target_path)
7743
8207
  continue;
7744
8208
  const abs = expandPath(c.target_path);
7745
- if (!existsSync12(abs)) {
8209
+ if (!existsSync13(abs)) {
7746
8210
  if (printed < maxPrinted) {
7747
8211
  if (opts.dryRun) {
7748
8212
  console.log(chalk.yellow(" would remove:") + ` ${c.slug} ${chalk.dim(`(${truncateMiddle(c.target_path, 88)})`)}`);
@@ -7752,7 +8216,7 @@ program.command("clean").description("Remove configs from DB whose target files
7752
8216
  printed++;
7753
8217
  }
7754
8218
  if (!opts.dryRun)
7755
- deleteConfig(c.id);
8219
+ await store.deleteConfig(c.id);
7756
8220
  removed++;
7757
8221
  }
7758
8222
  }
@@ -7767,6 +8231,7 @@ ${removed} orphaned config(s) ${opts.dryRun ? "found" : "removed"}${omitted > 0
7767
8231
  }
7768
8232
  });
7769
8233
  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) => {
8234
+ const store = resolveConfigStore();
7770
8235
  const packages = [
7771
8236
  { name: "@hasna/todos", bin: "todos", mcp: "todos-mcp" },
7772
8237
  { name: "@hasna/mementos", bin: "mementos", mcp: "mementos-mcp" },
@@ -7823,7 +8288,7 @@ Registering MCP servers in Claude Code:`));
7823
8288
  console.log(chalk.cyan(`
7824
8289
  Initializing configs:`));
7825
8290
  if (!opts.dryRun) {
7826
- const result = await syncKnown({});
8291
+ const result = await syncKnown({ store });
7827
8292
  console.log(chalk.green(" \u2713 ") + `Synced ${result.added + result.updated + result.unchanged} known configs`);
7828
8293
  } else {
7829
8294
  console.log(chalk.dim(" would run: configs init"));
@@ -7832,11 +8297,11 @@ Initializing configs:`));
7832
8297
  \u2713 Bootstrap complete.`) + chalk.dim(" Restart Claude Code for MCP servers to activate."));
7833
8298
  });
7834
8299
  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 });
8300
+ const result = await syncKnown({ dryRun: opts.dryRun, agent: opts.agent, store: resolveConfigStore() });
7836
8301
  console.log(chalk.green("\u2713") + ` Pulled: +${result.added} updated:${result.updated} unchanged:${result.unchanged}`);
7837
8302
  });
7838
8303
  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 });
8304
+ const result = await syncToDisk({ dryRun: opts.dryRun, agent: opts.agent, store: resolveConfigStore() });
7840
8305
  console.log(chalk.green("\u2713") + ` Pushed: updated:${result.updated} unchanged:${result.unchanged} skipped:${result.skipped.length}`);
7841
8306
  });
7842
8307
  program.command("update").description("Check for updates and install latest version").option("--check", "only check, don't install").action(async (opts) => {
@@ -7860,11 +8325,14 @@ program.command("update").description("Check for updates and install latest vers
7860
8325
  }
7861
8326
  });
7862
8327
  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]);
8328
+ await resolveConfigStore().sendFeedback({
8329
+ message,
8330
+ email: opts.email || null,
8331
+ category: opts.category || "general",
8332
+ version: pkg.version
8333
+ });
7865
8334
  console.log(chalk.green("\u2713") + " Feedback saved. Thank you!");
7866
8335
  });
7867
8336
  program.version(pkg.version).name("instructions");
7868
- registerStorageCommands(program);
7869
8337
  registerEventsCommands(program, { source: "configs" });
7870
8338
  program.parse(process.argv);