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