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