@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/mcp/index.js
CHANGED
|
@@ -18,17 +18,38 @@ var __export = (target, all) => {
|
|
|
18
18
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
19
|
var __require = import.meta.require;
|
|
20
20
|
|
|
21
|
-
// src/
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
// src/types/index.ts
|
|
22
|
+
var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
|
|
23
|
+
var init_types = __esm(() => {
|
|
24
|
+
ConfigNotFoundError = class ConfigNotFoundError extends Error {
|
|
25
|
+
constructor(id) {
|
|
26
|
+
super(`Config not found: ${id}`);
|
|
27
|
+
this.name = "ConfigNotFoundError";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
ProfileNotFoundError = class ProfileNotFoundError extends Error {
|
|
31
|
+
constructor(id) {
|
|
32
|
+
super(`Profile not found: ${id}`);
|
|
33
|
+
this.name = "ProfileNotFoundError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
ConfigApplyError = class ConfigApplyError extends Error {
|
|
37
|
+
constructor(message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "ConfigApplyError";
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
TemplateRenderError = class TemplateRenderError extends Error {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "TemplateRenderError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
29
48
|
});
|
|
49
|
+
|
|
50
|
+
// src/db/database.ts
|
|
30
51
|
import { Database } from "bun:sqlite";
|
|
31
|
-
import { cpSync, existsSync, mkdirSync, statSync } from "fs";
|
|
52
|
+
import { cpSync, existsSync, mkdirSync, rmSync, statSync } from "fs";
|
|
32
53
|
import { join } from "path";
|
|
33
54
|
import { randomUUID } from "crypto";
|
|
34
55
|
function getDbPath() {
|
|
@@ -56,6 +77,9 @@ function slugify(name) {
|
|
|
56
77
|
function getDatabase(path) {
|
|
57
78
|
if (_db)
|
|
58
79
|
return _db;
|
|
80
|
+
if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
|
|
81
|
+
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.");
|
|
82
|
+
}
|
|
59
83
|
const dbPath = path || getDbPath();
|
|
60
84
|
const db = new Database(dbPath);
|
|
61
85
|
db.run("PRAGMA journal_mode = WAL");
|
|
@@ -73,6 +97,16 @@ function resetDatabase() {
|
|
|
73
97
|
}
|
|
74
98
|
_db = null;
|
|
75
99
|
}
|
|
100
|
+
function resetLocalDatabase() {
|
|
101
|
+
resetDatabase();
|
|
102
|
+
const dbPath = getDbPath();
|
|
103
|
+
if (dbPath === ":memory:")
|
|
104
|
+
return;
|
|
105
|
+
for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
106
|
+
if (existsSync(p))
|
|
107
|
+
rmSync(p);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
76
110
|
function applyMigrations(db) {
|
|
77
111
|
let currentVersion = 0;
|
|
78
112
|
try {
|
|
@@ -98,6 +132,22 @@ function ensureFeedbackTable(db) {
|
|
|
98
132
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
99
133
|
)
|
|
100
134
|
`);
|
|
135
|
+
const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
|
|
136
|
+
const required = [
|
|
137
|
+
["email", "TEXT"],
|
|
138
|
+
["category", "TEXT DEFAULT 'general'"],
|
|
139
|
+
["version", "TEXT"],
|
|
140
|
+
["machine_id", "TEXT"],
|
|
141
|
+
["created_at", "TEXT"]
|
|
142
|
+
];
|
|
143
|
+
for (const [name, def] of required) {
|
|
144
|
+
if (!existing.has(name))
|
|
145
|
+
db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function insertFeedback(input, db) {
|
|
149
|
+
const d = db || getDatabase();
|
|
150
|
+
d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
|
|
101
151
|
}
|
|
102
152
|
function migrateDotfile() {
|
|
103
153
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
@@ -189,46 +239,7 @@ var init_database = __esm(() => {
|
|
|
189
239
|
];
|
|
190
240
|
});
|
|
191
241
|
|
|
192
|
-
// src/types/index.ts
|
|
193
|
-
var ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, TemplateRenderError;
|
|
194
|
-
var init_types = __esm(() => {
|
|
195
|
-
ConfigNotFoundError = class ConfigNotFoundError extends Error {
|
|
196
|
-
constructor(id) {
|
|
197
|
-
super(`Config not found: ${id}`);
|
|
198
|
-
this.name = "ConfigNotFoundError";
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
ProfileNotFoundError = class ProfileNotFoundError extends Error {
|
|
202
|
-
constructor(id) {
|
|
203
|
-
super(`Profile not found: ${id}`);
|
|
204
|
-
this.name = "ProfileNotFoundError";
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
ConfigApplyError = class ConfigApplyError extends Error {
|
|
208
|
-
constructor(message) {
|
|
209
|
-
super(message);
|
|
210
|
-
this.name = "ConfigApplyError";
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
TemplateRenderError = class TemplateRenderError extends Error {
|
|
214
|
-
constructor(message) {
|
|
215
|
-
super(message);
|
|
216
|
-
this.name = "TemplateRenderError";
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
});
|
|
220
|
-
|
|
221
242
|
// src/db/configs.ts
|
|
222
|
-
var exports_configs = {};
|
|
223
|
-
__export(exports_configs, {
|
|
224
|
-
updateConfig: () => updateConfig,
|
|
225
|
-
listConfigs: () => listConfigs,
|
|
226
|
-
getConfigStats: () => getConfigStats,
|
|
227
|
-
getConfigById: () => getConfigById,
|
|
228
|
-
getConfig: () => getConfig,
|
|
229
|
-
deleteConfig: () => deleteConfig,
|
|
230
|
-
createConfig: () => createConfig
|
|
231
|
-
});
|
|
232
243
|
function rowToConfig(row) {
|
|
233
244
|
let outputs = [];
|
|
234
245
|
try {
|
|
@@ -413,26 +424,6 @@ var init_configs = __esm(() => {
|
|
|
413
424
|
init_database();
|
|
414
425
|
});
|
|
415
426
|
|
|
416
|
-
// src/db/snapshots.ts
|
|
417
|
-
function createSnapshot(configId, content, version, db) {
|
|
418
|
-
const d = db || getDatabase();
|
|
419
|
-
const id = uuid();
|
|
420
|
-
const ts = now();
|
|
421
|
-
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
422
|
-
return { id, config_id: configId, content, version, created_at: ts };
|
|
423
|
-
}
|
|
424
|
-
function listSnapshots(configId, db) {
|
|
425
|
-
const d = db || getDatabase();
|
|
426
|
-
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
427
|
-
}
|
|
428
|
-
function getSnapshotByVersion(configId, version, db) {
|
|
429
|
-
const d = db || getDatabase();
|
|
430
|
-
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
431
|
-
}
|
|
432
|
-
var init_snapshots = __esm(() => {
|
|
433
|
-
init_database();
|
|
434
|
-
});
|
|
435
|
-
|
|
436
427
|
// src/lib/template.ts
|
|
437
428
|
var exports_template = {};
|
|
438
429
|
__export(exports_template, {
|
|
@@ -588,6 +579,527 @@ var init_machine = __esm(() => {
|
|
|
588
579
|
init_template();
|
|
589
580
|
});
|
|
590
581
|
|
|
582
|
+
// src/db/profiles.ts
|
|
583
|
+
function rowToProfile(row) {
|
|
584
|
+
return {
|
|
585
|
+
...row,
|
|
586
|
+
selectors: JSON.parse(row.selectors || "{}"),
|
|
587
|
+
variables: JSON.parse(row.variables || "{}")
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function uniqueProfileSlug(name, db, excludeId) {
|
|
591
|
+
const base = slugify(name);
|
|
592
|
+
let slug = base;
|
|
593
|
+
let i = 1;
|
|
594
|
+
while (true) {
|
|
595
|
+
const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
|
|
596
|
+
if (!existing || existing.id === excludeId)
|
|
597
|
+
return slug;
|
|
598
|
+
slug = `${base}-${i++}`;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function createProfile(input, db) {
|
|
602
|
+
const d = db || getDatabase();
|
|
603
|
+
const id = uuid();
|
|
604
|
+
const ts = now();
|
|
605
|
+
const slug = uniqueProfileSlug(input.name, d);
|
|
606
|
+
d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
607
|
+
id,
|
|
608
|
+
input.name,
|
|
609
|
+
slug,
|
|
610
|
+
input.description ?? null,
|
|
611
|
+
JSON.stringify(input.selectors ?? {}),
|
|
612
|
+
JSON.stringify(input.variables ?? {}),
|
|
613
|
+
ts,
|
|
614
|
+
ts
|
|
615
|
+
]);
|
|
616
|
+
return getProfile(id, d);
|
|
617
|
+
}
|
|
618
|
+
function getProfile(idOrSlug, db) {
|
|
619
|
+
const d = db || getDatabase();
|
|
620
|
+
const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
621
|
+
if (!row)
|
|
622
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
623
|
+
return rowToProfile(row);
|
|
624
|
+
}
|
|
625
|
+
function listProfiles(db) {
|
|
626
|
+
const d = db || getDatabase();
|
|
627
|
+
return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
|
|
628
|
+
}
|
|
629
|
+
function updateProfile(idOrSlug, input, db) {
|
|
630
|
+
const d = db || getDatabase();
|
|
631
|
+
const existing = getProfile(idOrSlug, d);
|
|
632
|
+
const ts = now();
|
|
633
|
+
const updates = ["updated_at = ?"];
|
|
634
|
+
const params = [ts];
|
|
635
|
+
if (input.name !== undefined) {
|
|
636
|
+
updates.push("name = ?", "slug = ?");
|
|
637
|
+
params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
|
|
638
|
+
}
|
|
639
|
+
if (input.description !== undefined) {
|
|
640
|
+
updates.push("description = ?");
|
|
641
|
+
params.push(input.description);
|
|
642
|
+
}
|
|
643
|
+
if (input.selectors !== undefined) {
|
|
644
|
+
updates.push("selectors = ?");
|
|
645
|
+
params.push(JSON.stringify(input.selectors));
|
|
646
|
+
}
|
|
647
|
+
if (input.variables !== undefined) {
|
|
648
|
+
updates.push("variables = ?");
|
|
649
|
+
params.push(JSON.stringify(input.variables));
|
|
650
|
+
}
|
|
651
|
+
params.push(existing.id);
|
|
652
|
+
d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
653
|
+
return getProfile(existing.id, d);
|
|
654
|
+
}
|
|
655
|
+
function deleteProfile(idOrSlug, db) {
|
|
656
|
+
const d = db || getDatabase();
|
|
657
|
+
const existing = getProfile(idOrSlug, d);
|
|
658
|
+
d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
|
|
659
|
+
}
|
|
660
|
+
function addConfigToProfile(profileIdOrSlug, configId, db) {
|
|
661
|
+
const d = db || getDatabase();
|
|
662
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
663
|
+
const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
|
|
664
|
+
const order = (maxRow?.max_order ?? -1) + 1;
|
|
665
|
+
d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
|
|
666
|
+
}
|
|
667
|
+
function removeConfigFromProfile(profileIdOrSlug, configId, db) {
|
|
668
|
+
const d = db || getDatabase();
|
|
669
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
670
|
+
d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
|
|
671
|
+
}
|
|
672
|
+
function getProfileConfigs(profileIdOrSlug, db) {
|
|
673
|
+
const d = db || getDatabase();
|
|
674
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
675
|
+
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
|
|
676
|
+
if (rows.length === 0)
|
|
677
|
+
return [];
|
|
678
|
+
const ids = rows.map((r) => r.config_id);
|
|
679
|
+
return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
|
|
680
|
+
}
|
|
681
|
+
function profileHasSelectors(profile) {
|
|
682
|
+
const selectors = profile.selectors ?? {};
|
|
683
|
+
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
684
|
+
}
|
|
685
|
+
function profileMatchesMachine(profile, machine) {
|
|
686
|
+
const selectors = profile.selectors ?? {};
|
|
687
|
+
const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
|
|
688
|
+
const value = candidate.trim().toLowerCase();
|
|
689
|
+
return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
|
|
690
|
+
});
|
|
691
|
+
const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
|
|
692
|
+
const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
|
|
693
|
+
return osMatches && archMatches && hostnameMatches;
|
|
694
|
+
}
|
|
695
|
+
function resolveProfileForMachine(machine = detectMachineContext(), db) {
|
|
696
|
+
const profiles = listProfiles(db).filter(profileHasSelectors);
|
|
697
|
+
const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
|
|
698
|
+
const selectors = profile.selectors;
|
|
699
|
+
const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
|
|
700
|
+
return { profile, score };
|
|
701
|
+
}).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
|
|
702
|
+
return matches[0]?.profile ?? null;
|
|
703
|
+
}
|
|
704
|
+
var init_profiles = __esm(() => {
|
|
705
|
+
init_types();
|
|
706
|
+
init_database();
|
|
707
|
+
init_configs();
|
|
708
|
+
init_machine();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// src/db/snapshots.ts
|
|
712
|
+
function createSnapshot(configId, content, version, db) {
|
|
713
|
+
const d = db || getDatabase();
|
|
714
|
+
const id = uuid();
|
|
715
|
+
const ts = now();
|
|
716
|
+
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
717
|
+
return { id, config_id: configId, content, version, created_at: ts };
|
|
718
|
+
}
|
|
719
|
+
function listSnapshots(configId, db) {
|
|
720
|
+
const d = db || getDatabase();
|
|
721
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
722
|
+
}
|
|
723
|
+
function getSnapshot(id, db) {
|
|
724
|
+
const d = db || getDatabase();
|
|
725
|
+
return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
|
|
726
|
+
}
|
|
727
|
+
function getSnapshotByVersion(configId, version, db) {
|
|
728
|
+
const d = db || getDatabase();
|
|
729
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
730
|
+
}
|
|
731
|
+
function pruneSnapshots(configId, keep = 10, db) {
|
|
732
|
+
const d = db || getDatabase();
|
|
733
|
+
const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
|
|
734
|
+
SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
|
|
735
|
+
)`, [configId, configId, keep]);
|
|
736
|
+
return result.changes;
|
|
737
|
+
}
|
|
738
|
+
var init_snapshots = __esm(() => {
|
|
739
|
+
init_database();
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// src/db/machines.ts
|
|
743
|
+
import { arch, hostname, type } from "os";
|
|
744
|
+
function currentHostname2() {
|
|
745
|
+
return hostname();
|
|
746
|
+
}
|
|
747
|
+
function currentOs() {
|
|
748
|
+
return type();
|
|
749
|
+
}
|
|
750
|
+
function currentArch2() {
|
|
751
|
+
return arch();
|
|
752
|
+
}
|
|
753
|
+
function registerMachine(hostnameStr, os, archStr, db) {
|
|
754
|
+
const d = db || getDatabase();
|
|
755
|
+
const h = hostnameStr ?? currentHostname2();
|
|
756
|
+
const o = os ?? currentOs();
|
|
757
|
+
const a = archStr ?? currentArch2();
|
|
758
|
+
const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
|
|
759
|
+
if (existing) {
|
|
760
|
+
if (existing.os !== o || existing.arch !== a) {
|
|
761
|
+
d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
|
|
762
|
+
return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
|
|
763
|
+
}
|
|
764
|
+
return existing;
|
|
765
|
+
}
|
|
766
|
+
const id = uuid();
|
|
767
|
+
const ts = now();
|
|
768
|
+
d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
|
|
769
|
+
return d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
770
|
+
}
|
|
771
|
+
function updateMachineApplied(hostnameStr, db) {
|
|
772
|
+
const d = db || getDatabase();
|
|
773
|
+
const h = hostnameStr ?? currentHostname2();
|
|
774
|
+
d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now(), h]);
|
|
775
|
+
}
|
|
776
|
+
function listMachines(db) {
|
|
777
|
+
const d = db || getDatabase();
|
|
778
|
+
return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
|
|
779
|
+
}
|
|
780
|
+
var init_machines = __esm(() => {
|
|
781
|
+
init_database();
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
// src/data/config-store.ts
|
|
785
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
786
|
+
function resolveCloudConfig(env = process.env) {
|
|
787
|
+
const apiUrl = env[API_URL_ENV]?.trim();
|
|
788
|
+
const apiKey = env[API_KEY_ENV]?.trim();
|
|
789
|
+
if (!apiUrl && !apiKey)
|
|
790
|
+
return null;
|
|
791
|
+
if (!apiUrl || !apiKey) {
|
|
792
|
+
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.`);
|
|
793
|
+
}
|
|
794
|
+
return { apiUrl, apiKey };
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
class LocalConfigStore {
|
|
798
|
+
db;
|
|
799
|
+
mode = "local";
|
|
800
|
+
constructor(db) {
|
|
801
|
+
this.db = db;
|
|
802
|
+
}
|
|
803
|
+
async listConfigs(filter) {
|
|
804
|
+
return listConfigs(filter, this.db);
|
|
805
|
+
}
|
|
806
|
+
async getConfig(idOrSlug) {
|
|
807
|
+
return getConfig(idOrSlug, this.db);
|
|
808
|
+
}
|
|
809
|
+
async getConfigById(id) {
|
|
810
|
+
return getConfigById(id, this.db);
|
|
811
|
+
}
|
|
812
|
+
async createConfig(input) {
|
|
813
|
+
return createConfig(input, this.db);
|
|
814
|
+
}
|
|
815
|
+
async updateConfig(idOrSlug, input) {
|
|
816
|
+
return updateConfig(idOrSlug, input, this.db);
|
|
817
|
+
}
|
|
818
|
+
async deleteConfig(idOrSlug) {
|
|
819
|
+
deleteConfig(idOrSlug, this.db);
|
|
820
|
+
}
|
|
821
|
+
async getConfigStats() {
|
|
822
|
+
return getConfigStats(this.db);
|
|
823
|
+
}
|
|
824
|
+
async listSnapshots(configId) {
|
|
825
|
+
return listSnapshots(configId, this.db);
|
|
826
|
+
}
|
|
827
|
+
async getSnapshot(id) {
|
|
828
|
+
return getSnapshot(id, this.db);
|
|
829
|
+
}
|
|
830
|
+
async getSnapshotByVersion(configId, version) {
|
|
831
|
+
return getSnapshotByVersion(configId, version, this.db);
|
|
832
|
+
}
|
|
833
|
+
async createSnapshot(configId, content, version) {
|
|
834
|
+
return createSnapshot(configId, content, version, this.db);
|
|
835
|
+
}
|
|
836
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
837
|
+
return pruneSnapshots(configId, keep, this.db);
|
|
838
|
+
}
|
|
839
|
+
async listProfiles() {
|
|
840
|
+
return listProfiles(this.db);
|
|
841
|
+
}
|
|
842
|
+
async getProfile(idOrSlug) {
|
|
843
|
+
return getProfile(idOrSlug, this.db);
|
|
844
|
+
}
|
|
845
|
+
async getProfileConfigs(idOrSlug) {
|
|
846
|
+
return getProfileConfigs(idOrSlug, this.db);
|
|
847
|
+
}
|
|
848
|
+
async createProfile(input) {
|
|
849
|
+
return createProfile(input, this.db);
|
|
850
|
+
}
|
|
851
|
+
async updateProfile(idOrSlug, input) {
|
|
852
|
+
return updateProfile(idOrSlug, input, this.db);
|
|
853
|
+
}
|
|
854
|
+
async deleteProfile(idOrSlug) {
|
|
855
|
+
deleteProfile(idOrSlug, this.db);
|
|
856
|
+
}
|
|
857
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
858
|
+
addConfigToProfile(profileIdOrSlug, configId, this.db);
|
|
859
|
+
}
|
|
860
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
861
|
+
removeConfigFromProfile(profileIdOrSlug, configId, this.db);
|
|
862
|
+
}
|
|
863
|
+
async resolveProfileForMachine(machine) {
|
|
864
|
+
return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
|
|
865
|
+
}
|
|
866
|
+
async registerMachine(hostname2, os, arch2) {
|
|
867
|
+
return registerMachine(hostname2, os, arch2, this.db);
|
|
868
|
+
}
|
|
869
|
+
async updateMachineApplied(hostname2) {
|
|
870
|
+
updateMachineApplied(hostname2, this.db);
|
|
871
|
+
}
|
|
872
|
+
async listMachines() {
|
|
873
|
+
return listMachines(this.db);
|
|
874
|
+
}
|
|
875
|
+
async sendFeedback(input) {
|
|
876
|
+
insertFeedback(input, this.db);
|
|
877
|
+
}
|
|
878
|
+
async reset() {
|
|
879
|
+
resetLocalDatabase();
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
class CloudConfigStore {
|
|
884
|
+
mode = "api";
|
|
885
|
+
base;
|
|
886
|
+
apiKey;
|
|
887
|
+
timeoutMs;
|
|
888
|
+
constructor(config) {
|
|
889
|
+
this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
|
|
890
|
+
this.apiKey = config.apiKey;
|
|
891
|
+
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
892
|
+
}
|
|
893
|
+
async request(method, path, body, opts = {}) {
|
|
894
|
+
const controller = new AbortController;
|
|
895
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
896
|
+
const headers = {
|
|
897
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
898
|
+
Accept: "application/json"
|
|
899
|
+
};
|
|
900
|
+
if (body !== undefined)
|
|
901
|
+
headers["Content-Type"] = "application/json";
|
|
902
|
+
if (opts.idempotent)
|
|
903
|
+
headers["Idempotency-Key"] = randomUUID2();
|
|
904
|
+
try {
|
|
905
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
906
|
+
method,
|
|
907
|
+
headers,
|
|
908
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
909
|
+
signal: controller.signal
|
|
910
|
+
});
|
|
911
|
+
if (res.status === 404 && opts.allow404)
|
|
912
|
+
return { status: 404, data: null };
|
|
913
|
+
const text = await res.text();
|
|
914
|
+
let parsed = null;
|
|
915
|
+
if (text) {
|
|
916
|
+
try {
|
|
917
|
+
parsed = JSON.parse(text);
|
|
918
|
+
} catch {
|
|
919
|
+
parsed = text;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (!res.ok) {
|
|
923
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
924
|
+
throw new CloudHttpError(res.status, message, parsed);
|
|
925
|
+
}
|
|
926
|
+
return { status: res.status, data: parsed };
|
|
927
|
+
} finally {
|
|
928
|
+
clearTimeout(timer);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async listConfigs(filter = {}) {
|
|
932
|
+
const params = new URLSearchParams;
|
|
933
|
+
if (filter.category)
|
|
934
|
+
params.set("category", filter.category);
|
|
935
|
+
if (filter.agent)
|
|
936
|
+
params.set("agent", filter.agent);
|
|
937
|
+
if (filter.kind)
|
|
938
|
+
params.set("kind", filter.kind);
|
|
939
|
+
if (filter.search)
|
|
940
|
+
params.set("search", filter.search);
|
|
941
|
+
const qs = params.toString();
|
|
942
|
+
const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
|
|
943
|
+
let configs = data?.configs ?? [];
|
|
944
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
945
|
+
configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
|
|
946
|
+
}
|
|
947
|
+
if (filter.is_template !== undefined) {
|
|
948
|
+
configs = configs.filter((c) => c.is_template === filter.is_template);
|
|
949
|
+
}
|
|
950
|
+
return configs;
|
|
951
|
+
}
|
|
952
|
+
async getConfig(idOrSlug) {
|
|
953
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
954
|
+
if (status === 404 || !data?.config)
|
|
955
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
956
|
+
return data.config;
|
|
957
|
+
}
|
|
958
|
+
async getConfigById(id) {
|
|
959
|
+
return this.getConfig(id);
|
|
960
|
+
}
|
|
961
|
+
async createConfig(input) {
|
|
962
|
+
const { data } = await this.request("POST", "/configs", input, {
|
|
963
|
+
idempotent: true
|
|
964
|
+
});
|
|
965
|
+
return data.config;
|
|
966
|
+
}
|
|
967
|
+
async updateConfig(idOrSlug, input) {
|
|
968
|
+
const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
|
|
969
|
+
return data.config;
|
|
970
|
+
}
|
|
971
|
+
async deleteConfig(idOrSlug) {
|
|
972
|
+
const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
973
|
+
if (status === 404)
|
|
974
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
975
|
+
}
|
|
976
|
+
async getConfigStats() {
|
|
977
|
+
const { data } = await this.request("GET", "/stats");
|
|
978
|
+
return data ?? { total: 0 };
|
|
979
|
+
}
|
|
980
|
+
async listSnapshots(configId) {
|
|
981
|
+
const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
|
|
982
|
+
return data?.snapshots ?? [];
|
|
983
|
+
}
|
|
984
|
+
async getSnapshot(id) {
|
|
985
|
+
const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
986
|
+
if (status === 404 || !data?.snapshot)
|
|
987
|
+
return null;
|
|
988
|
+
return data.snapshot;
|
|
989
|
+
}
|
|
990
|
+
async getSnapshotByVersion(configId, version) {
|
|
991
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
|
|
992
|
+
if (status === 404 || !data?.snapshot)
|
|
993
|
+
return null;
|
|
994
|
+
return data.snapshot;
|
|
995
|
+
}
|
|
996
|
+
async createSnapshot(configId, content, version) {
|
|
997
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
|
|
998
|
+
return data.snapshot;
|
|
999
|
+
}
|
|
1000
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
1001
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
|
|
1002
|
+
return data?.pruned ?? 0;
|
|
1003
|
+
}
|
|
1004
|
+
async listProfiles() {
|
|
1005
|
+
const { data } = await this.request("GET", "/profiles");
|
|
1006
|
+
return data?.profiles ?? [];
|
|
1007
|
+
}
|
|
1008
|
+
async getProfile(idOrSlug) {
|
|
1009
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1010
|
+
if (status === 404 || !data?.profile)
|
|
1011
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1012
|
+
const { configs: _configs, ...profile } = data.profile;
|
|
1013
|
+
return profile;
|
|
1014
|
+
}
|
|
1015
|
+
async getProfileConfigs(idOrSlug) {
|
|
1016
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1017
|
+
if (status === 404 || !data?.profile)
|
|
1018
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1019
|
+
return data.profile.configs ?? [];
|
|
1020
|
+
}
|
|
1021
|
+
async createProfile(input) {
|
|
1022
|
+
const { data } = await this.request("POST", "/profiles", input, {
|
|
1023
|
+
idempotent: true
|
|
1024
|
+
});
|
|
1025
|
+
return data.profile;
|
|
1026
|
+
}
|
|
1027
|
+
async updateProfile(idOrSlug, input) {
|
|
1028
|
+
const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
|
|
1029
|
+
return data.profile;
|
|
1030
|
+
}
|
|
1031
|
+
async deleteProfile(idOrSlug) {
|
|
1032
|
+
const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1033
|
+
if (status === 404)
|
|
1034
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1035
|
+
}
|
|
1036
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
1037
|
+
await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
|
|
1038
|
+
}
|
|
1039
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
1040
|
+
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
1041
|
+
}
|
|
1042
|
+
async resolveProfileForMachine(machine) {
|
|
1043
|
+
const params = new URLSearchParams;
|
|
1044
|
+
if (machine?.hostname)
|
|
1045
|
+
params.set("hostname", machine.hostname);
|
|
1046
|
+
if (machine?.os)
|
|
1047
|
+
params.set("os", machine.os);
|
|
1048
|
+
if (machine?.arch)
|
|
1049
|
+
params.set("arch", machine.arch);
|
|
1050
|
+
const qs = params.toString();
|
|
1051
|
+
const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
|
|
1052
|
+
if (status === 404 || !data?.profile)
|
|
1053
|
+
return null;
|
|
1054
|
+
return data.profile;
|
|
1055
|
+
}
|
|
1056
|
+
async registerMachine(hostname2, os, arch2) {
|
|
1057
|
+
const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
|
|
1058
|
+
return data.machine;
|
|
1059
|
+
}
|
|
1060
|
+
async updateMachineApplied(hostname2) {
|
|
1061
|
+
await this.request("POST", "/machines/applied", { hostname: hostname2 });
|
|
1062
|
+
}
|
|
1063
|
+
async listMachines() {
|
|
1064
|
+
const { data } = await this.request("GET", "/machines");
|
|
1065
|
+
return data?.machines ?? [];
|
|
1066
|
+
}
|
|
1067
|
+
async sendFeedback(input) {
|
|
1068
|
+
await this.request("POST", "/feedback", {
|
|
1069
|
+
message: input.message,
|
|
1070
|
+
email: input.email ?? undefined,
|
|
1071
|
+
category: input.category ?? undefined,
|
|
1072
|
+
version: input.version ?? undefined
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
async reset() {
|
|
1076
|
+
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.");
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function resolveConfigStore(env = process.env) {
|
|
1080
|
+
const cloud = resolveCloudConfig(env);
|
|
1081
|
+
return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
|
|
1082
|
+
}
|
|
1083
|
+
var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
|
|
1084
|
+
var init_config_store = __esm(() => {
|
|
1085
|
+
init_configs();
|
|
1086
|
+
init_profiles();
|
|
1087
|
+
init_snapshots();
|
|
1088
|
+
init_machines();
|
|
1089
|
+
init_database();
|
|
1090
|
+
init_types();
|
|
1091
|
+
CloudHttpError = class CloudHttpError extends Error {
|
|
1092
|
+
status;
|
|
1093
|
+
body;
|
|
1094
|
+
constructor(status, message, body) {
|
|
1095
|
+
super(message);
|
|
1096
|
+
this.status = status;
|
|
1097
|
+
this.body = body;
|
|
1098
|
+
this.name = "CloudHttpError";
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
});
|
|
1102
|
+
|
|
591
1103
|
// src/lib/transforms.ts
|
|
592
1104
|
import { basename, extname } from "path";
|
|
593
1105
|
function ensureTrailingNewline(content) {
|
|
@@ -755,8 +1267,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
755
1267
|
mkdirSync2(dir, { recursive: true });
|
|
756
1268
|
}
|
|
757
1269
|
if (previousContent !== null && changed) {
|
|
758
|
-
const
|
|
759
|
-
createSnapshot(config.id, previousContent, config.version
|
|
1270
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1271
|
+
await store.createSnapshot(config.id, previousContent, config.version);
|
|
760
1272
|
}
|
|
761
1273
|
writeFileSync(path, renderedContent, "utf-8");
|
|
762
1274
|
}
|
|
@@ -782,8 +1294,8 @@ async function applyConfig(config, opts = {}) {
|
|
|
782
1294
|
if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
|
|
783
1295
|
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
784
1296
|
}
|
|
785
|
-
const
|
|
786
|
-
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(
|
|
1297
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1298
|
+
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
787
1299
|
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
788
1300
|
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
789
1301
|
}
|
|
@@ -804,7 +1316,7 @@ async function applyConfig(config, opts = {}) {
|
|
|
804
1316
|
};
|
|
805
1317
|
}
|
|
806
1318
|
if (!opts.dryRun) {
|
|
807
|
-
updateConfig(config.id, { synced_at:
|
|
1319
|
+
await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
|
|
808
1320
|
}
|
|
809
1321
|
return result;
|
|
810
1322
|
}
|
|
@@ -826,9 +1338,7 @@ async function applyConfigs(configs, opts = {}) {
|
|
|
826
1338
|
}
|
|
827
1339
|
var init_apply = __esm(() => {
|
|
828
1340
|
init_types();
|
|
829
|
-
|
|
830
|
-
init_configs();
|
|
831
|
-
init_snapshots();
|
|
1341
|
+
init_config_store();
|
|
832
1342
|
init_machine();
|
|
833
1343
|
init_transforms();
|
|
834
1344
|
});
|
|
@@ -922,9 +1432,9 @@ function redactIni(content) {
|
|
|
922
1432
|
for (let i = 0;i < lines.length; i++) {
|
|
923
1433
|
const line = lines[i];
|
|
924
1434
|
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
925
|
-
if (authM && !authM[2].
|
|
926
|
-
redacted.push({ varName: "
|
|
927
|
-
out.push(`${authM[1]}{
|
|
1435
|
+
if (authM && !isReferenceValue(authM[2].trim())) {
|
|
1436
|
+
redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
1437
|
+
out.push(`${authM[1]}\${NPM_TOKEN}`);
|
|
928
1438
|
continue;
|
|
929
1439
|
}
|
|
930
1440
|
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
@@ -964,6 +1474,8 @@ function redactGeneric(content) {
|
|
|
964
1474
|
function shouldRedactKeyValue(key, value) {
|
|
965
1475
|
if (!value || value.startsWith("{{"))
|
|
966
1476
|
return false;
|
|
1477
|
+
if (isReferenceValue(value.trim()))
|
|
1478
|
+
return false;
|
|
967
1479
|
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
968
1480
|
return false;
|
|
969
1481
|
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
@@ -985,6 +1497,9 @@ function reasonFor(key, value) {
|
|
|
985
1497
|
}
|
|
986
1498
|
return "secret value pattern";
|
|
987
1499
|
}
|
|
1500
|
+
function isReferenceValue(value) {
|
|
1501
|
+
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);
|
|
1502
|
+
}
|
|
988
1503
|
function redactContent(content, format) {
|
|
989
1504
|
switch (format) {
|
|
990
1505
|
case "shell":
|
|
@@ -1088,11 +1603,11 @@ function isKnownGeneratedTargetPath(targetPath) {
|
|
|
1088
1603
|
return hasClaudeRuleSourceForCursorTarget(targetPath);
|
|
1089
1604
|
}
|
|
1090
1605
|
async function syncProject(opts) {
|
|
1091
|
-
const
|
|
1606
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1092
1607
|
const absDir = expandPath(opts.projectDir);
|
|
1093
1608
|
const projectName = absDir.split("/").pop() || "project";
|
|
1094
1609
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1095
|
-
const allConfigs = listConfigs(
|
|
1610
|
+
const allConfigs = await store.listConfigs();
|
|
1096
1611
|
const machine = detectMachineContext();
|
|
1097
1612
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
1098
1613
|
const abs = join3(absDir, pf.file);
|
|
@@ -1114,11 +1629,11 @@ async function syncProject(opts) {
|
|
|
1114
1629
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1115
1630
|
if (!existing) {
|
|
1116
1631
|
if (!opts.dryRun)
|
|
1117
|
-
createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }
|
|
1632
|
+
await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
|
|
1118
1633
|
result.added++;
|
|
1119
1634
|
} else if (existing.content !== content) {
|
|
1120
1635
|
if (!opts.dryRun)
|
|
1121
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
1636
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
1122
1637
|
result.updated++;
|
|
1123
1638
|
} else {
|
|
1124
1639
|
result.unchanged++;
|
|
@@ -1143,11 +1658,11 @@ async function syncProject(opts) {
|
|
|
1143
1658
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1144
1659
|
if (!existing) {
|
|
1145
1660
|
if (!opts.dryRun)
|
|
1146
|
-
createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }
|
|
1661
|
+
await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
|
|
1147
1662
|
result.added++;
|
|
1148
1663
|
} else if (existing.content !== content) {
|
|
1149
1664
|
if (!opts.dryRun)
|
|
1150
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
1665
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
1151
1666
|
result.updated++;
|
|
1152
1667
|
} else {
|
|
1153
1668
|
result.unchanged++;
|
|
@@ -1157,7 +1672,7 @@ async function syncProject(opts) {
|
|
|
1157
1672
|
return result;
|
|
1158
1673
|
}
|
|
1159
1674
|
async function syncKnown(opts = {}) {
|
|
1160
|
-
const
|
|
1675
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1161
1676
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1162
1677
|
const home = getConfigHome();
|
|
1163
1678
|
const machine = detectMachineContext();
|
|
@@ -1166,7 +1681,7 @@ async function syncKnown(opts = {}) {
|
|
|
1166
1681
|
targets = targets.filter((k) => k.agent === opts.agent);
|
|
1167
1682
|
if (opts.category)
|
|
1168
1683
|
targets = targets.filter((k) => k.category === opts.category);
|
|
1169
|
-
const allConfigs = listConfigs(
|
|
1684
|
+
const allConfigs = await store.listConfigs();
|
|
1170
1685
|
const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
|
|
1171
1686
|
for (const known of targets) {
|
|
1172
1687
|
if (known.rulesDir) {
|
|
@@ -1195,15 +1710,15 @@ async function syncKnown(opts = {}) {
|
|
|
1195
1710
|
const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
|
|
1196
1711
|
if (!existing) {
|
|
1197
1712
|
if (!opts.dryRun)
|
|
1198
|
-
createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }
|
|
1713
|
+
await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
|
|
1199
1714
|
result.added++;
|
|
1200
1715
|
} else if (existing.content !== content) {
|
|
1201
1716
|
if (!opts.dryRun)
|
|
1202
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs }
|
|
1717
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
|
|
1203
1718
|
result.updated++;
|
|
1204
1719
|
} else if (!outputsEqual(existing.outputs, outputs)) {
|
|
1205
1720
|
if (!opts.dryRun)
|
|
1206
|
-
updateConfig(existing.id, { outputs }
|
|
1721
|
+
await store.updateConfig(existing.id, { outputs });
|
|
1207
1722
|
result.updated++;
|
|
1208
1723
|
} else {
|
|
1209
1724
|
result.unchanged++;
|
|
@@ -1235,7 +1750,7 @@ async function syncKnown(opts = {}) {
|
|
|
1235
1750
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
|
|
1236
1751
|
if (!existing) {
|
|
1237
1752
|
if (!opts.dryRun) {
|
|
1238
|
-
createConfig({
|
|
1753
|
+
await store.createConfig({
|
|
1239
1754
|
name: known.name,
|
|
1240
1755
|
category: known.category,
|
|
1241
1756
|
agent: known.agent,
|
|
@@ -1246,16 +1761,16 @@ async function syncKnown(opts = {}) {
|
|
|
1246
1761
|
description: known.description,
|
|
1247
1762
|
is_template: isTemplate2,
|
|
1248
1763
|
outputs: known.outputs
|
|
1249
|
-
}
|
|
1764
|
+
});
|
|
1250
1765
|
}
|
|
1251
1766
|
result.added++;
|
|
1252
1767
|
} else if (existing.content !== content) {
|
|
1253
1768
|
if (!opts.dryRun)
|
|
1254
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }
|
|
1769
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
|
|
1255
1770
|
result.updated++;
|
|
1256
1771
|
} else if (!outputsEqual(existing.outputs, known.outputs)) {
|
|
1257
1772
|
if (!opts.dryRun)
|
|
1258
|
-
updateConfig(existing.id, { outputs: known.outputs }
|
|
1773
|
+
await store.updateConfig(existing.id, { outputs: known.outputs });
|
|
1259
1774
|
result.updated++;
|
|
1260
1775
|
} else {
|
|
1261
1776
|
result.unchanged++;
|
|
@@ -1267,9 +1782,9 @@ async function syncKnown(opts = {}) {
|
|
|
1267
1782
|
return result;
|
|
1268
1783
|
}
|
|
1269
1784
|
async function syncToDisk(opts = {}) {
|
|
1270
|
-
const
|
|
1785
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1271
1786
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1272
|
-
const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }
|
|
1787
|
+
const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
|
|
1273
1788
|
const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
|
|
1274
1789
|
let configs = allFileConfigs.filter((config) => {
|
|
1275
1790
|
return !isGeneratedOutputTarget2(config, outputOwners);
|
|
@@ -1282,7 +1797,7 @@ async function syncToDisk(opts = {}) {
|
|
|
1282
1797
|
if (!config.target_path && config.outputs.length === 0)
|
|
1283
1798
|
continue;
|
|
1284
1799
|
try {
|
|
1285
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
1800
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
|
|
1286
1801
|
r.changed ? result.updated++ : result.unchanged++;
|
|
1287
1802
|
} catch {
|
|
1288
1803
|
result.skipped.push(config.target_path ?? config.id);
|
|
@@ -1319,12 +1834,12 @@ function buildDiff(expectedContent, targetPath) {
|
|
|
1319
1834
|
return lines.join(`
|
|
1320
1835
|
`);
|
|
1321
1836
|
}
|
|
1322
|
-
function diffConfig(config, opts = {}) {
|
|
1837
|
+
async function diffConfig(config, opts = {}) {
|
|
1323
1838
|
if (!config.target_path && config.outputs.length === 0)
|
|
1324
1839
|
return "(reference \u2014 no target path)";
|
|
1325
1840
|
const diffs = [];
|
|
1326
|
-
const
|
|
1327
|
-
const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(
|
|
1841
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1842
|
+
const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
1328
1843
|
if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
|
|
1329
1844
|
return "(generated output \u2014 managed by fan-out)";
|
|
1330
1845
|
}
|
|
@@ -1403,8 +1918,7 @@ function detectFormat(filePath) {
|
|
|
1403
1918
|
}
|
|
1404
1919
|
var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
|
|
1405
1920
|
var init_sync = __esm(() => {
|
|
1406
|
-
|
|
1407
|
-
init_configs();
|
|
1921
|
+
init_config_store();
|
|
1408
1922
|
init_apply();
|
|
1409
1923
|
init_redact();
|
|
1410
1924
|
init_machine();
|
|
@@ -1467,14 +1981,14 @@ function shouldSkip(p) {
|
|
|
1467
1981
|
return SKIP.some((s) => p.includes(s));
|
|
1468
1982
|
}
|
|
1469
1983
|
async function syncFromDir(dir, opts = {}) {
|
|
1470
|
-
const
|
|
1984
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1471
1985
|
const absDir = expandPath(dir);
|
|
1472
1986
|
if (!existsSync5(absDir))
|
|
1473
1987
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
1474
1988
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
|
|
1475
1989
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1476
1990
|
const home = homedir3();
|
|
1477
|
-
const allConfigs = listConfigs(
|
|
1991
|
+
const allConfigs = await store.listConfigs();
|
|
1478
1992
|
for (const file of files) {
|
|
1479
1993
|
if (shouldSkip(file)) {
|
|
1480
1994
|
result.skipped.push(file);
|
|
@@ -1490,11 +2004,11 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
1490
2004
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
1491
2005
|
if (!existing) {
|
|
1492
2006
|
if (!opts.dryRun)
|
|
1493
|
-
createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }
|
|
2007
|
+
await store.createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
|
|
1494
2008
|
result.added++;
|
|
1495
2009
|
} else if (existing.content !== content) {
|
|
1496
2010
|
if (!opts.dryRun)
|
|
1497
|
-
updateConfig(existing.id, { content }
|
|
2011
|
+
await store.updateConfig(existing.id, { content });
|
|
1498
2012
|
result.updated++;
|
|
1499
2013
|
} else {
|
|
1500
2014
|
result.unchanged++;
|
|
@@ -1506,17 +2020,17 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
1506
2020
|
return result;
|
|
1507
2021
|
}
|
|
1508
2022
|
async function syncToDir(dir, opts = {}) {
|
|
1509
|
-
const
|
|
2023
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1510
2024
|
const home = homedir3();
|
|
1511
2025
|
const absDir = expandPath(dir);
|
|
1512
2026
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
1513
|
-
const configs = listConfigs(
|
|
2027
|
+
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
1514
2028
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1515
2029
|
for (const config of configs) {
|
|
1516
2030
|
if (config.kind === "reference")
|
|
1517
2031
|
continue;
|
|
1518
2032
|
try {
|
|
1519
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
2033
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store });
|
|
1520
2034
|
r.changed ? result.updated++ : result.unchanged++;
|
|
1521
2035
|
} catch {
|
|
1522
2036
|
result.skipped.push(config.target_path || config.id);
|
|
@@ -1538,8 +2052,7 @@ function walkDir(dir, files = []) {
|
|
|
1538
2052
|
}
|
|
1539
2053
|
var SKIP;
|
|
1540
2054
|
var init_sync_dir = __esm(() => {
|
|
1541
|
-
|
|
1542
|
-
init_configs();
|
|
2055
|
+
init_config_store();
|
|
1543
2056
|
init_apply();
|
|
1544
2057
|
init_sync();
|
|
1545
2058
|
SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
@@ -1549,7 +2062,7 @@ var init_sync_dir = __esm(() => {
|
|
|
1549
2062
|
var require_package = __commonJS((exports, module) => {
|
|
1550
2063
|
module.exports = {
|
|
1551
2064
|
name: "@hasna/instructions",
|
|
1552
|
-
version: "0.
|
|
2065
|
+
version: "0.4.1",
|
|
1553
2066
|
description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK + Dashboard.",
|
|
1554
2067
|
type: "module",
|
|
1555
2068
|
main: "dist/index.js",
|
|
@@ -1564,10 +2077,6 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1564
2077
|
".": {
|
|
1565
2078
|
types: "./dist/index.d.ts",
|
|
1566
2079
|
import: "./dist/index.js"
|
|
1567
|
-
},
|
|
1568
|
-
"./storage": {
|
|
1569
|
-
types: "./dist/storage.d.ts",
|
|
1570
|
-
import: "./dist/storage.js"
|
|
1571
2080
|
}
|
|
1572
2081
|
},
|
|
1573
2082
|
files: [
|
|
@@ -1578,14 +2087,15 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1578
2087
|
],
|
|
1579
2088
|
scripts: {
|
|
1580
2089
|
clean: "rm -rf dist",
|
|
1581
|
-
build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts
|
|
1582
|
-
"build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts
|
|
2090
|
+
build: "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg && tsc --emitDeclarationOnly --outDir dist",
|
|
2091
|
+
"build:server": "bun run clean && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external pg --external @modelcontextprotocol/sdk && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external pg --external @modelcontextprotocol/sdk && bun build src/server/index.ts --outdir dist/server --target bun --external pg && bun build src/index.ts --outdir dist --target bun --external pg",
|
|
1583
2092
|
"build:dashboard": "cd dashboard && bun run build",
|
|
1584
2093
|
migrate: "bun run src/server/index.ts migrate",
|
|
1585
2094
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
1586
2095
|
"kit:check": "bunx @hasna/contracts vendor-kit --check",
|
|
1587
2096
|
typecheck: "tsc --noEmit",
|
|
1588
2097
|
test: "bun test",
|
|
2098
|
+
"check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings --home .",
|
|
1589
2099
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
1590
2100
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
1591
2101
|
"dev:serve": "bun run src/server/index.ts",
|
|
@@ -1649,420 +2159,12 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1649
2159
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1650
2160
|
|
|
1651
2161
|
// src/mcp/server.ts
|
|
1652
|
-
|
|
1653
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1654
|
-
|
|
1655
|
-
// src/db/storage-sync.ts
|
|
1656
|
-
init_database();
|
|
1657
|
-
|
|
1658
|
-
// src/db/pg-migrations.ts
|
|
1659
|
-
var PG_MIGRATIONS = [
|
|
1660
|
-
`CREATE TABLE IF NOT EXISTS configs (
|
|
1661
|
-
id TEXT PRIMARY KEY,
|
|
1662
|
-
name TEXT NOT NULL,
|
|
1663
|
-
slug TEXT NOT NULL UNIQUE,
|
|
1664
|
-
kind TEXT NOT NULL DEFAULT 'file',
|
|
1665
|
-
category TEXT NOT NULL,
|
|
1666
|
-
agent TEXT NOT NULL DEFAULT 'global',
|
|
1667
|
-
target_path TEXT,
|
|
1668
|
-
outputs TEXT NOT NULL DEFAULT '[]',
|
|
1669
|
-
format TEXT NOT NULL DEFAULT 'text',
|
|
1670
|
-
content TEXT NOT NULL DEFAULT '',
|
|
1671
|
-
description TEXT,
|
|
1672
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
1673
|
-
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
1674
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
1675
|
-
created_at TEXT NOT NULL,
|
|
1676
|
-
updated_at TEXT NOT NULL,
|
|
1677
|
-
synced_at TEXT
|
|
1678
|
-
)`,
|
|
1679
|
-
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
1680
|
-
id TEXT PRIMARY KEY,
|
|
1681
|
-
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1682
|
-
content TEXT NOT NULL,
|
|
1683
|
-
version INTEGER NOT NULL,
|
|
1684
|
-
created_at TEXT NOT NULL
|
|
1685
|
-
)`,
|
|
1686
|
-
`CREATE TABLE IF NOT EXISTS profiles (
|
|
1687
|
-
id TEXT PRIMARY KEY,
|
|
1688
|
-
name TEXT NOT NULL,
|
|
1689
|
-
slug TEXT NOT NULL UNIQUE,
|
|
1690
|
-
description TEXT,
|
|
1691
|
-
selectors TEXT NOT NULL DEFAULT '{}',
|
|
1692
|
-
variables TEXT NOT NULL DEFAULT '{}',
|
|
1693
|
-
created_at TEXT NOT NULL,
|
|
1694
|
-
updated_at TEXT NOT NULL
|
|
1695
|
-
)`,
|
|
1696
|
-
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
1697
|
-
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
1698
|
-
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1699
|
-
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
1700
|
-
PRIMARY KEY (profile_id, config_id)
|
|
1701
|
-
)`,
|
|
1702
|
-
`CREATE TABLE IF NOT EXISTS machines (
|
|
1703
|
-
id TEXT PRIMARY KEY,
|
|
1704
|
-
hostname TEXT NOT NULL UNIQUE,
|
|
1705
|
-
os TEXT,
|
|
1706
|
-
arch TEXT,
|
|
1707
|
-
last_applied_at TEXT,
|
|
1708
|
-
created_at TEXT NOT NULL
|
|
1709
|
-
)`,
|
|
1710
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1711
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1712
|
-
message TEXT NOT NULL,
|
|
1713
|
-
email TEXT,
|
|
1714
|
-
category TEXT DEFAULT 'general',
|
|
1715
|
-
version TEXT,
|
|
1716
|
-
machine_id TEXT,
|
|
1717
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1718
|
-
)`,
|
|
1719
|
-
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
1720
|
-
];
|
|
1721
|
-
|
|
1722
|
-
// src/db/remote-storage.ts
|
|
1723
|
-
import pg from "pg";
|
|
1724
|
-
var DISABLED_SSL_MODE = "disable";
|
|
1725
|
-
function translatePlaceholders(sql) {
|
|
1726
|
-
let index = 0;
|
|
1727
|
-
return sql.replace(/\?/g, () => `$${++index}`);
|
|
1728
|
-
}
|
|
1729
|
-
function normalizeParams(params) {
|
|
1730
|
-
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
1731
|
-
return flat.map((value) => value === undefined ? null : value);
|
|
1732
|
-
}
|
|
1733
|
-
function normalizeHost(hostname) {
|
|
1734
|
-
const stripped = hostname.replace(/^\[/, "").replace(/\]$/, "");
|
|
1735
|
-
try {
|
|
1736
|
-
return decodeURIComponent(stripped).toLowerCase();
|
|
1737
|
-
} catch {
|
|
1738
|
-
return stripped.toLowerCase();
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
function isLocalPostgresHost(hostname) {
|
|
1742
|
-
const host = normalizeHost(hostname);
|
|
1743
|
-
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "" || host.startsWith("/");
|
|
1744
|
-
}
|
|
1745
|
-
function effectivePgHost(url) {
|
|
1746
|
-
const hosts = url.searchParams.getAll("host");
|
|
1747
|
-
const finalHost = hosts.length > 0 ? hosts[hosts.length - 1] : null;
|
|
1748
|
-
return finalHost?.trim() ? finalHost : url.hostname;
|
|
1749
|
-
}
|
|
1750
|
-
function buildPgPoolConfig(connectionString) {
|
|
1751
|
-
let url;
|
|
1752
|
-
try {
|
|
1753
|
-
url = new URL(connectionString);
|
|
1754
|
-
} catch {
|
|
1755
|
-
throw new Error("Invalid PostgreSQL connection string");
|
|
1756
|
-
}
|
|
1757
|
-
const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
|
|
1758
|
-
const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
|
|
1759
|
-
const isLocal = isLocalPostgresHost(effectivePgHost(url));
|
|
1760
|
-
const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
|
|
1761
|
-
if (!isLocal && hasDisabledSsl) {
|
|
1762
|
-
throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
|
|
1763
|
-
}
|
|
1764
|
-
const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
|
|
1765
|
-
url.searchParams.delete("sslmode");
|
|
1766
|
-
url.searchParams.delete("ssl");
|
|
1767
|
-
return {
|
|
1768
|
-
connectionString: url.toString(),
|
|
1769
|
-
ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
|
|
1770
|
-
};
|
|
1771
|
-
}
|
|
1772
|
-
|
|
1773
|
-
class PgAdapterAsync {
|
|
1774
|
-
pool;
|
|
1775
|
-
constructor(connectionString) {
|
|
1776
|
-
this.pool = new pg.Pool(buildPgPoolConfig(connectionString));
|
|
1777
|
-
}
|
|
1778
|
-
async run(sql, ...params) {
|
|
1779
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1780
|
-
return { changes: result.rowCount ?? 0 };
|
|
1781
|
-
}
|
|
1782
|
-
async all(sql, ...params) {
|
|
1783
|
-
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1784
|
-
return result.rows;
|
|
1785
|
-
}
|
|
1786
|
-
async close() {
|
|
1787
|
-
await this.pool.end();
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// src/db/storage-sync.ts
|
|
1792
|
-
var STORAGE_TABLES = ["configs", "config_snapshots", "profiles", "profile_configs", "machines", "feedback"];
|
|
1793
|
-
var PRIMARY_KEYS = {
|
|
1794
|
-
configs: ["id"],
|
|
1795
|
-
config_snapshots: ["id"],
|
|
1796
|
-
profiles: ["id"],
|
|
1797
|
-
profile_configs: ["profile_id", "config_id"],
|
|
1798
|
-
machines: ["id"],
|
|
1799
|
-
feedback: ["id"]
|
|
1800
|
-
};
|
|
1801
|
-
var CONFIGS_STORAGE_ENV = "HASNA_CONFIGS_DATABASE_URL";
|
|
1802
|
-
var CONFIGS_STORAGE_FALLBACK_ENV = "CONFIGS_DATABASE_URL";
|
|
1803
|
-
var CONFIGS_STORAGE_MODE_ENV = "HASNA_CONFIGS_STORAGE_MODE";
|
|
1804
|
-
var CONFIGS_STORAGE_MODE_FALLBACK_ENV = "CONFIGS_STORAGE_MODE";
|
|
1805
|
-
var STORAGE_DATABASE_ENV = [CONFIGS_STORAGE_ENV, CONFIGS_STORAGE_FALLBACK_ENV];
|
|
1806
|
-
var STORAGE_MODE_ENV = [CONFIGS_STORAGE_MODE_ENV, CONFIGS_STORAGE_MODE_FALLBACK_ENV];
|
|
1807
|
-
function firstEnv(names) {
|
|
1808
|
-
for (const name of names) {
|
|
1809
|
-
const value = process.env[name];
|
|
1810
|
-
if (value)
|
|
1811
|
-
return value;
|
|
1812
|
-
}
|
|
1813
|
-
return null;
|
|
1814
|
-
}
|
|
1815
|
-
function normalizeStorageMode(value) {
|
|
1816
|
-
const normalized = value?.trim().toLowerCase();
|
|
1817
|
-
if (normalized === "local" || normalized === "hybrid" || normalized === "remote")
|
|
1818
|
-
return normalized;
|
|
1819
|
-
return;
|
|
1820
|
-
}
|
|
1821
|
-
function getStorageDatabaseUrl() {
|
|
1822
|
-
return firstEnv(STORAGE_DATABASE_ENV);
|
|
1823
|
-
}
|
|
1824
|
-
function getStorageMode() {
|
|
1825
|
-
const mode = normalizeStorageMode(firstEnv(STORAGE_MODE_ENV));
|
|
1826
|
-
if (mode)
|
|
1827
|
-
return mode;
|
|
1828
|
-
return getStorageDatabaseUrl() ? "hybrid" : "local";
|
|
1829
|
-
}
|
|
1830
|
-
async function getStoragePg() {
|
|
1831
|
-
const url = getStorageDatabaseUrl();
|
|
1832
|
-
if (!url)
|
|
1833
|
-
throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
|
|
1834
|
-
return new PgAdapterAsync(url);
|
|
1835
|
-
}
|
|
1836
|
-
async function runStorageMigrations(remote) {
|
|
1837
|
-
await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
|
|
1838
|
-
for (const sql of PG_MIGRATIONS)
|
|
1839
|
-
await remote.run(sql);
|
|
1840
|
-
}
|
|
1841
|
-
async function storagePush(options) {
|
|
1842
|
-
const remote = await getStoragePg();
|
|
1843
|
-
const db = getDatabase();
|
|
1844
|
-
try {
|
|
1845
|
-
await runStorageMigrations(remote);
|
|
1846
|
-
const results = [];
|
|
1847
|
-
for (const table of resolveTables(options?.tables))
|
|
1848
|
-
results.push(await pushTable(db, remote, table));
|
|
1849
|
-
recordSyncMeta(db, "push", results);
|
|
1850
|
-
return results;
|
|
1851
|
-
} finally {
|
|
1852
|
-
await remote.close();
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
async function storagePull(options) {
|
|
1856
|
-
const remote = await getStoragePg();
|
|
1857
|
-
const db = getDatabase();
|
|
1858
|
-
try {
|
|
1859
|
-
await runStorageMigrations(remote);
|
|
1860
|
-
const results = [];
|
|
1861
|
-
for (const table of resolveTables(options?.tables))
|
|
1862
|
-
results.push(await pullTable(remote, db, table));
|
|
1863
|
-
recordSyncMeta(db, "pull", results);
|
|
1864
|
-
return results;
|
|
1865
|
-
} finally {
|
|
1866
|
-
await remote.close();
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
async function storageSync(options) {
|
|
1870
|
-
const pull = await storagePull(options);
|
|
1871
|
-
const push = await storagePush(options);
|
|
1872
|
-
return { pull, push };
|
|
1873
|
-
}
|
|
1874
|
-
function getStorageSyncMetaAll() {
|
|
1875
|
-
const db = getDatabase();
|
|
1876
|
-
ensureSyncMetaTable(db);
|
|
1877
|
-
return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
|
|
1878
|
-
}
|
|
1879
|
-
function getStorageStatus() {
|
|
1880
|
-
return {
|
|
1881
|
-
configured: Boolean(getStorageDatabaseUrl()),
|
|
1882
|
-
mode: getStorageMode(),
|
|
1883
|
-
env: STORAGE_DATABASE_ENV,
|
|
1884
|
-
service: "configs",
|
|
1885
|
-
tables: STORAGE_TABLES,
|
|
1886
|
-
sync: getStorageSyncMetaAll()
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
function resolveTables(tables) {
|
|
1890
|
-
if (!tables || tables.length === 0)
|
|
1891
|
-
return [...STORAGE_TABLES];
|
|
1892
|
-
const allowed = new Set(STORAGE_TABLES);
|
|
1893
|
-
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
1894
|
-
const invalid = requested.filter((table) => !allowed.has(table));
|
|
1895
|
-
if (invalid.length > 0)
|
|
1896
|
-
throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
|
|
1897
|
-
return requested;
|
|
1898
|
-
}
|
|
1899
|
-
async function pushTable(db, remote, table) {
|
|
1900
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
1901
|
-
try {
|
|
1902
|
-
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
|
|
1903
|
-
result.rowsRead = rows.length;
|
|
1904
|
-
if (rows.length === 0)
|
|
1905
|
-
return result;
|
|
1906
|
-
const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
|
|
1907
|
-
result.rowsWritten = await upsertPg(remote, table, columns, rows);
|
|
1908
|
-
} catch (error) {
|
|
1909
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1910
|
-
}
|
|
1911
|
-
return result;
|
|
1912
|
-
}
|
|
1913
|
-
async function pullTable(remote, db, table) {
|
|
1914
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
1915
|
-
try {
|
|
1916
|
-
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
|
|
1917
|
-
result.rowsRead = rows.length;
|
|
1918
|
-
if (rows.length === 0)
|
|
1919
|
-
return result;
|
|
1920
|
-
const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
|
|
1921
|
-
result.rowsWritten = upsertSqlite(db, table, columns, rows);
|
|
1922
|
-
} catch (error) {
|
|
1923
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1924
|
-
}
|
|
1925
|
-
return result;
|
|
1926
|
-
}
|
|
1927
|
-
async function filterRemoteColumns(remote, table, columns) {
|
|
1928
|
-
const rows = await remote.all("SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = ?", table);
|
|
1929
|
-
if (rows.length === 0)
|
|
1930
|
-
return columns;
|
|
1931
|
-
const allowed = new Set(rows.map((row) => row.column_name));
|
|
1932
|
-
return columns.filter((column) => allowed.has(column));
|
|
1933
|
-
}
|
|
1934
|
-
function filterLocalColumns(db, table, columns) {
|
|
1935
|
-
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
1936
|
-
const allowed = new Set(rows.map((row) => row.name));
|
|
1937
|
-
return columns.filter((column) => allowed.has(column));
|
|
1938
|
-
}
|
|
1939
|
-
async function upsertPg(remote, table, columns, rows) {
|
|
1940
|
-
if (columns.length === 0)
|
|
1941
|
-
return 0;
|
|
1942
|
-
const primaryKeys = PRIMARY_KEYS[table];
|
|
1943
|
-
const columnList = columns.map(quoteIdent).join(", ");
|
|
1944
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
1945
|
-
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
1946
|
-
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
1947
|
-
const fallbackKey = primaryKeys[0];
|
|
1948
|
-
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
1949
|
-
for (const row of rows) {
|
|
1950
|
-
await remote.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`, ...columns.map((column) => row[column] ?? null));
|
|
1951
|
-
}
|
|
1952
|
-
return rows.length;
|
|
1953
|
-
}
|
|
1954
|
-
function upsertSqlite(db, table, columns, rows) {
|
|
1955
|
-
if (columns.length === 0)
|
|
1956
|
-
return 0;
|
|
1957
|
-
const primaryKeys = PRIMARY_KEYS[table];
|
|
1958
|
-
const columnList = columns.map(quoteIdent).join(", ");
|
|
1959
|
-
const placeholders = columns.map(() => "?").join(", ");
|
|
1960
|
-
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
1961
|
-
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
1962
|
-
const fallbackKey = primaryKeys[0];
|
|
1963
|
-
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = excluded.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = excluded.${quoteIdent(fallbackKey)}`;
|
|
1964
|
-
const statement = db.prepare(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES (${placeholders}) ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}`);
|
|
1965
|
-
db.transaction((batch) => {
|
|
1966
|
-
for (const row of batch)
|
|
1967
|
-
statement.run(...columns.map((column) => coerceForSqlite(row[column])));
|
|
1968
|
-
})(rows);
|
|
1969
|
-
return rows.length;
|
|
1970
|
-
}
|
|
1971
|
-
function recordSyncMeta(db, direction, results) {
|
|
1972
|
-
ensureSyncMetaTable(db);
|
|
1973
|
-
const now2 = new Date().toISOString();
|
|
1974
|
-
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");
|
|
1975
|
-
for (const result of results) {
|
|
1976
|
-
if (result.errors.length > 0)
|
|
1977
|
-
continue;
|
|
1978
|
-
statement.run(result.table, now2, direction);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
function ensureSyncMetaTable(db) {
|
|
1982
|
-
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))");
|
|
1983
|
-
}
|
|
1984
|
-
function quoteIdent(identifier) {
|
|
1985
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1986
|
-
}
|
|
1987
|
-
function coerceForSqlite(value) {
|
|
1988
|
-
if (value === undefined || value === null)
|
|
1989
|
-
return null;
|
|
1990
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
1991
|
-
return value;
|
|
1992
|
-
if (value instanceof Date)
|
|
1993
|
-
return value.toISOString();
|
|
1994
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
1995
|
-
return value;
|
|
1996
|
-
if (typeof value === "object")
|
|
1997
|
-
return JSON.stringify(value);
|
|
1998
|
-
return String(value);
|
|
1999
|
-
}
|
|
2000
|
-
|
|
2001
|
-
// src/mcp/server.ts
|
|
2002
|
-
init_configs();
|
|
2162
|
+
init_config_store();
|
|
2003
2163
|
init_apply();
|
|
2004
2164
|
init_sync_dir();
|
|
2005
|
-
|
|
2006
|
-
// src/db/profiles.ts
|
|
2007
|
-
init_types();
|
|
2008
|
-
init_database();
|
|
2009
|
-
init_configs();
|
|
2010
|
-
init_machine();
|
|
2011
|
-
function rowToProfile(row) {
|
|
2012
|
-
return {
|
|
2013
|
-
...row,
|
|
2014
|
-
selectors: JSON.parse(row.selectors || "{}"),
|
|
2015
|
-
variables: JSON.parse(row.variables || "{}")
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
|
-
function getProfile(idOrSlug, db) {
|
|
2019
|
-
const d = db || getDatabase();
|
|
2020
|
-
const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
2021
|
-
if (!row)
|
|
2022
|
-
throw new ProfileNotFoundError(idOrSlug);
|
|
2023
|
-
return rowToProfile(row);
|
|
2024
|
-
}
|
|
2025
|
-
function listProfiles(db) {
|
|
2026
|
-
const d = db || getDatabase();
|
|
2027
|
-
return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
|
|
2028
|
-
}
|
|
2029
|
-
function getProfileConfigs(profileIdOrSlug, db) {
|
|
2030
|
-
const d = db || getDatabase();
|
|
2031
|
-
const profile = getProfile(profileIdOrSlug, d);
|
|
2032
|
-
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
|
|
2033
|
-
if (rows.length === 0)
|
|
2034
|
-
return [];
|
|
2035
|
-
const ids = rows.map((r) => r.config_id);
|
|
2036
|
-
return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
|
|
2037
|
-
}
|
|
2038
|
-
function profileHasSelectors(profile) {
|
|
2039
|
-
const selectors = profile.selectors ?? {};
|
|
2040
|
-
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
2041
|
-
}
|
|
2042
|
-
function profileMatchesMachine(profile, machine) {
|
|
2043
|
-
const selectors = profile.selectors ?? {};
|
|
2044
|
-
const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
|
|
2045
|
-
const value = candidate.trim().toLowerCase();
|
|
2046
|
-
return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
|
|
2047
|
-
});
|
|
2048
|
-
const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
|
|
2049
|
-
const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
|
|
2050
|
-
return osMatches && archMatches && hostnameMatches;
|
|
2051
|
-
}
|
|
2052
|
-
function resolveProfileForMachine(machine = detectMachineContext(), db) {
|
|
2053
|
-
const profiles = listProfiles(db).filter(profileHasSelectors);
|
|
2054
|
-
const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
|
|
2055
|
-
const selectors = profile.selectors;
|
|
2056
|
-
const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
|
|
2057
|
-
return { profile, score };
|
|
2058
|
-
}).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
|
|
2059
|
-
return matches[0]?.profile ?? null;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
// src/mcp/server.ts
|
|
2063
|
-
init_apply();
|
|
2064
|
-
init_snapshots();
|
|
2065
2165
|
init_machine();
|
|
2166
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2167
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2066
2168
|
|
|
2067
2169
|
// src/lib/compact-output.ts
|
|
2068
2170
|
var DEFAULT_LIST_LIMIT = 20;
|
|
@@ -2208,11 +2310,7 @@ var ALL_LEAN_TOOLS = [
|
|
|
2208
2310
|
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
2209
2311
|
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
|
|
2210
2312
|
{ name: "list_agents", description: "List all registered agents.", inputSchema: { type: "object", properties: {} } },
|
|
2211
|
-
{ name: "send_feedback", description: "Send feedback about this service", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
|
|
2212
|
-
{ name: "storage_status", description: "Show storage sync configuration and local sync history.", inputSchema: { type: "object", properties: {} } },
|
|
2213
|
-
{ name: "storage_push", description: "Push local configs data to storage PostgreSQL.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } },
|
|
2214
|
-
{ name: "storage_pull", description: "Pull configs data from storage PostgreSQL to local SQLite.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } },
|
|
2215
|
-
{ name: "storage_sync", description: "Bidirectional configs sync: pull then push.", inputSchema: { type: "object", properties: { tables: { type: "array", items: { type: "string" } } } } }
|
|
2313
|
+
{ name: "send_feedback", description: "Send feedback about this service", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
|
|
2216
2314
|
];
|
|
2217
2315
|
function ok(data) {
|
|
2218
2316
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
@@ -2227,10 +2325,11 @@ function buildServer() {
|
|
|
2227
2325
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
|
|
2228
2326
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2229
2327
|
const { name, arguments: args = {} } = req.params;
|
|
2328
|
+
const store = resolveConfigStore();
|
|
2230
2329
|
try {
|
|
2231
2330
|
switch (name) {
|
|
2232
2331
|
case "list_configs": {
|
|
2233
|
-
const configs = listConfigs({
|
|
2332
|
+
const configs = await store.listConfigs({
|
|
2234
2333
|
category: args["category"] || undefined,
|
|
2235
2334
|
agent: args["agent"] || undefined,
|
|
2236
2335
|
kind: args["kind"] || undefined,
|
|
@@ -2244,11 +2343,11 @@ function buildServer() {
|
|
|
2244
2343
|
}));
|
|
2245
2344
|
}
|
|
2246
2345
|
case "get_config": {
|
|
2247
|
-
const c = getConfig(args["id_or_slug"]);
|
|
2346
|
+
const c = await store.getConfig(args["id_or_slug"]);
|
|
2248
2347
|
return ok(c);
|
|
2249
2348
|
}
|
|
2250
2349
|
case "create_config": {
|
|
2251
|
-
const c = createConfig({
|
|
2350
|
+
const c = await store.createConfig({
|
|
2252
2351
|
name: args["name"],
|
|
2253
2352
|
content: args["content"],
|
|
2254
2353
|
category: args["category"],
|
|
@@ -2264,7 +2363,7 @@ function buildServer() {
|
|
|
2264
2363
|
return ok({ id: c.id, slug: c.slug, name: c.name });
|
|
2265
2364
|
}
|
|
2266
2365
|
case "update_config": {
|
|
2267
|
-
const c = updateConfig(args["id_or_slug"], {
|
|
2366
|
+
const c = await store.updateConfig(args["id_or_slug"], {
|
|
2268
2367
|
content: args["content"],
|
|
2269
2368
|
name: args["name"],
|
|
2270
2369
|
tags: args["tags"],
|
|
@@ -2277,23 +2376,22 @@ function buildServer() {
|
|
|
2277
2376
|
return ok({ id: c.id, slug: c.slug, version: c.version });
|
|
2278
2377
|
}
|
|
2279
2378
|
case "delete_config": {
|
|
2280
|
-
|
|
2281
|
-
deleteConfig2(args["id_or_slug"]);
|
|
2379
|
+
await store.deleteConfig(args["id_or_slug"]);
|
|
2282
2380
|
return ok({ deleted: true });
|
|
2283
2381
|
}
|
|
2284
2382
|
case "apply_config": {
|
|
2285
|
-
const config = getConfig(args["id_or_slug"]);
|
|
2286
|
-
const result = await applyConfig(config, { dryRun: args["dry_run"] });
|
|
2383
|
+
const config = await store.getConfig(args["id_or_slug"]);
|
|
2384
|
+
const result = await applyConfig(config, { dryRun: args["dry_run"], store });
|
|
2287
2385
|
return ok(args["verbose"] ? result : summarizeApplyResult(result));
|
|
2288
2386
|
}
|
|
2289
2387
|
case "sync_directory": {
|
|
2290
2388
|
const dir = args["dir"];
|
|
2291
2389
|
const direction = args["direction"] || "from_disk";
|
|
2292
|
-
const result = direction === "to_disk" ? await syncToDir(dir) : await syncFromDir(dir);
|
|
2390
|
+
const result = direction === "to_disk" ? await syncToDir(dir, { store }) : await syncFromDir(dir, { store });
|
|
2293
2391
|
return ok(result);
|
|
2294
2392
|
}
|
|
2295
2393
|
case "list_profiles": {
|
|
2296
|
-
const profiles = listProfiles().map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
|
|
2394
|
+
const profiles = (await store.listProfiles()).map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
|
|
2297
2395
|
return ok(pagedPayload(profiles, {
|
|
2298
2396
|
limit: args["limit"],
|
|
2299
2397
|
cursor: args["cursor"],
|
|
@@ -2308,12 +2406,12 @@ function buildServer() {
|
|
|
2308
2406
|
});
|
|
2309
2407
|
if (!args["auto"] && !args["id_or_slug"])
|
|
2310
2408
|
return err("id_or_slug is required unless auto=true");
|
|
2311
|
-
const profile = args["auto"] ? resolveProfileForMachine(machine) : getProfile(args["id_or_slug"]);
|
|
2409
|
+
const profile = args["auto"] ? await store.resolveProfileForMachine(machine) : await store.getProfile(args["id_or_slug"]);
|
|
2312
2410
|
if (!profile)
|
|
2313
2411
|
return err("No matching machine-aware profile found");
|
|
2314
|
-
const configs = getProfileConfigs(profile.id);
|
|
2412
|
+
const configs = await store.getProfileConfigs(profile.id);
|
|
2315
2413
|
const vars = resolveProfileVariables(profile, machine);
|
|
2316
|
-
const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars });
|
|
2414
|
+
const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars, store });
|
|
2317
2415
|
return ok({
|
|
2318
2416
|
profile: summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }),
|
|
2319
2417
|
machine: {
|
|
@@ -2328,17 +2426,17 @@ function buildServer() {
|
|
|
2328
2426
|
});
|
|
2329
2427
|
}
|
|
2330
2428
|
case "get_snapshot": {
|
|
2331
|
-
const config = getConfig(args["config_id_or_slug"]);
|
|
2429
|
+
const config = await store.getConfig(args["config_id_or_slug"]);
|
|
2332
2430
|
if (args["version"]) {
|
|
2333
|
-
const snap = getSnapshotByVersion(config.id, args["version"]);
|
|
2431
|
+
const snap = await store.getSnapshotByVersion(config.id, args["version"]);
|
|
2334
2432
|
return snap ? ok(snap) : err("Snapshot not found");
|
|
2335
2433
|
}
|
|
2336
|
-
const snaps = listSnapshots(config.id);
|
|
2434
|
+
const snaps = await store.listSnapshots(config.id);
|
|
2337
2435
|
return ok(snaps[0] ?? null);
|
|
2338
2436
|
}
|
|
2339
2437
|
case "get_status": {
|
|
2340
|
-
const stats = getConfigStats();
|
|
2341
|
-
const allConfigs = listConfigs({ kind: "file" });
|
|
2438
|
+
const stats = await store.getConfigStats();
|
|
2439
|
+
const allConfigs = await store.listConfigs({ kind: "file" });
|
|
2342
2440
|
const { existsSync: ex, readFileSync: rf } = await import("fs");
|
|
2343
2441
|
const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
|
|
2344
2442
|
const { redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
|
|
@@ -2374,6 +2472,7 @@ function buildServer() {
|
|
|
2374
2472
|
case "sync_known": {
|
|
2375
2473
|
const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
2376
2474
|
const result = await syncKnown2({
|
|
2475
|
+
store,
|
|
2377
2476
|
agent: args["agent"] || undefined,
|
|
2378
2477
|
category: args["category"] || undefined
|
|
2379
2478
|
});
|
|
@@ -2382,12 +2481,12 @@ function buildServer() {
|
|
|
2382
2481
|
case "sync_project": {
|
|
2383
2482
|
const { syncProject: syncProject2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
2384
2483
|
const dir = args["project_dir"] || process.cwd();
|
|
2385
|
-
const result = await syncProject2({ projectDir: dir });
|
|
2484
|
+
const result = await syncProject2({ store, projectDir: dir });
|
|
2386
2485
|
return ok(result);
|
|
2387
2486
|
}
|
|
2388
2487
|
case "render_template": {
|
|
2389
2488
|
const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
|
|
2390
|
-
const config = getConfig(args["id_or_slug"]);
|
|
2489
|
+
const config = await store.getConfig(args["id_or_slug"]);
|
|
2391
2490
|
const vars = args["vars"] || {};
|
|
2392
2491
|
if (args["use_env"]) {
|
|
2393
2492
|
const { extractTemplateVars: extractTemplateVars2 } = await Promise.resolve().then(() => (init_template(), exports_template));
|
|
@@ -2402,7 +2501,7 @@ function buildServer() {
|
|
|
2402
2501
|
}
|
|
2403
2502
|
case "scan_secrets": {
|
|
2404
2503
|
const { scanSecrets: scanSecrets2, redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
|
|
2405
|
-
const configs = args["id_or_slug"] ? [getConfig(args["id_or_slug"])] : listConfigs({ kind: "file" });
|
|
2504
|
+
const configs = args["id_or_slug"] ? [await store.getConfig(args["id_or_slug"])] : await store.listConfigs({ kind: "file" });
|
|
2406
2505
|
const findings = [];
|
|
2407
2506
|
for (const c of configs) {
|
|
2408
2507
|
const fmt = c.format;
|
|
@@ -2411,7 +2510,7 @@ function buildServer() {
|
|
|
2411
2510
|
findings.push({ slug: c.slug, secrets: secrets.length, vars: secrets.map((s) => s.varName) });
|
|
2412
2511
|
if (args["fix"]) {
|
|
2413
2512
|
const { content, isTemplate: isTemplate2 } = redactContent2(c.content, fmt);
|
|
2414
|
-
updateConfig(c.id, { content, is_template: isTemplate2 });
|
|
2513
|
+
await store.updateConfig(c.id, { content, is_template: isTemplate2 });
|
|
2415
2514
|
}
|
|
2416
2515
|
}
|
|
2417
2516
|
}
|
|
@@ -2467,27 +2566,15 @@ function buildServer() {
|
|
|
2467
2566
|
return ok([..._cfgAgents.values()]);
|
|
2468
2567
|
}
|
|
2469
2568
|
case "send_feedback": {
|
|
2470
|
-
const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
2471
|
-
const db = getDatabase2();
|
|
2472
2569
|
const pkg = require_package();
|
|
2473
|
-
|
|
2570
|
+
await store.sendFeedback({
|
|
2571
|
+
message: args["message"],
|
|
2572
|
+
email: args["email"] || null,
|
|
2573
|
+
category: args["category"] || "general",
|
|
2574
|
+
version: pkg.version
|
|
2575
|
+
});
|
|
2474
2576
|
return ok({ message: "Feedback saved. Thank you!" });
|
|
2475
2577
|
}
|
|
2476
|
-
case "storage_status": {
|
|
2477
|
-
return ok(getStorageStatus());
|
|
2478
|
-
}
|
|
2479
|
-
case "storage_push": {
|
|
2480
|
-
const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
|
|
2481
|
-
return ok(await storagePush(tables ? { tables } : undefined));
|
|
2482
|
-
}
|
|
2483
|
-
case "storage_pull": {
|
|
2484
|
-
const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
|
|
2485
|
-
return ok(await storagePull(tables ? { tables } : undefined));
|
|
2486
|
-
}
|
|
2487
|
-
case "storage_sync": {
|
|
2488
|
-
const tables = Array.isArray(args["tables"]) ? args["tables"] : undefined;
|
|
2489
|
-
return ok(await storageSync(tables ? { tables } : undefined));
|
|
2490
|
-
}
|
|
2491
2578
|
default:
|
|
2492
2579
|
return err(`Unknown tool: ${name}`);
|
|
2493
2580
|
}
|