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