@hasna/instructions 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -12
- package/dashboard/README.md +73 -0
- package/dist/cli/index.js +1321 -1101
- package/dist/data/config-store.d.ts +134 -0
- package/dist/data/config-store.d.ts.map +1 -0
- package/dist/data/config-store.test.d.ts +2 -0
- package/dist/data/config-store.test.d.ts.map +1 -0
- package/dist/db/database.d.ts +15 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/generated/storage-kit/index.d.ts +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +896 -492
- package/dist/lib/apply.d.ts +2 -2
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/export.d.ts +2 -2
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +2 -2
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/package-manager-guard.d.ts +24 -0
- package/dist/lib/package-manager-guard.d.ts.map +1 -0
- package/dist/lib/package-manager-guard.test.d.ts +2 -0
- package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
- package/dist/lib/platform-profiles.d.ts +2 -2
- package/dist/lib/platform-profiles.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +2 -2
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts +3 -3
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/lib/sync.d.ts +6 -6
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +0 -13
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.js +650 -575
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1756 -17542
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/status.d.ts +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +21 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/package.json +5 -8
- package/dist/cli/storage.d.ts +0 -3
- package/dist/cli/storage.d.ts.map +0 -1
- package/dist/cli/storage.test.d.ts +0 -2
- package/dist/cli/storage.test.d.ts.map +0 -1
- package/dist/db/remote-storage.d.ts +0 -13
- package/dist/db/remote-storage.d.ts.map +0 -1
- package/dist/db/storage-sync.d.ts +0 -53
- package/dist/db/storage-sync.d.ts.map +0 -1
- package/dist/db/storage-sync.test.d.ts +0 -2
- package/dist/db/storage-sync.test.d.ts.map +0 -1
- package/dist/server/server.test.d.ts +0 -2
- package/dist/server/server.test.d.ts.map +0 -1
- package/dist/storage.d.ts +0 -5
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -537
package/dist/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 {
|
|
@@ -99,6 +133,10 @@ function ensureFeedbackTable(db) {
|
|
|
99
133
|
)
|
|
100
134
|
`);
|
|
101
135
|
}
|
|
136
|
+
function insertFeedback(input, db) {
|
|
137
|
+
const d = db || getDatabase();
|
|
138
|
+
d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
|
|
139
|
+
}
|
|
102
140
|
function migrateDotfile() {
|
|
103
141
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
104
142
|
const oldDirs = [join(home, ".open-configs"), join(home, ".configs")];
|
|
@@ -189,46 +227,7 @@ var init_database = __esm(() => {
|
|
|
189
227
|
];
|
|
190
228
|
});
|
|
191
229
|
|
|
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
230
|
// 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
231
|
function rowToConfig(row) {
|
|
233
232
|
let outputs = [];
|
|
234
233
|
try {
|
|
@@ -413,26 +412,6 @@ var init_configs = __esm(() => {
|
|
|
413
412
|
init_database();
|
|
414
413
|
});
|
|
415
414
|
|
|
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
415
|
// src/lib/template.ts
|
|
437
416
|
var exports_template = {};
|
|
438
417
|
__export(exports_template, {
|
|
@@ -588,6 +567,527 @@ var init_machine = __esm(() => {
|
|
|
588
567
|
init_template();
|
|
589
568
|
});
|
|
590
569
|
|
|
570
|
+
// src/db/profiles.ts
|
|
571
|
+
function rowToProfile(row) {
|
|
572
|
+
return {
|
|
573
|
+
...row,
|
|
574
|
+
selectors: JSON.parse(row.selectors || "{}"),
|
|
575
|
+
variables: JSON.parse(row.variables || "{}")
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function uniqueProfileSlug(name, db, excludeId) {
|
|
579
|
+
const base = slugify(name);
|
|
580
|
+
let slug = base;
|
|
581
|
+
let i = 1;
|
|
582
|
+
while (true) {
|
|
583
|
+
const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
|
|
584
|
+
if (!existing || existing.id === excludeId)
|
|
585
|
+
return slug;
|
|
586
|
+
slug = `${base}-${i++}`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function createProfile(input, db) {
|
|
590
|
+
const d = db || getDatabase();
|
|
591
|
+
const id = uuid();
|
|
592
|
+
const ts = now();
|
|
593
|
+
const slug = uniqueProfileSlug(input.name, d);
|
|
594
|
+
d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
595
|
+
id,
|
|
596
|
+
input.name,
|
|
597
|
+
slug,
|
|
598
|
+
input.description ?? null,
|
|
599
|
+
JSON.stringify(input.selectors ?? {}),
|
|
600
|
+
JSON.stringify(input.variables ?? {}),
|
|
601
|
+
ts,
|
|
602
|
+
ts
|
|
603
|
+
]);
|
|
604
|
+
return getProfile(id, d);
|
|
605
|
+
}
|
|
606
|
+
function getProfile(idOrSlug, db) {
|
|
607
|
+
const d = db || getDatabase();
|
|
608
|
+
const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
609
|
+
if (!row)
|
|
610
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
611
|
+
return rowToProfile(row);
|
|
612
|
+
}
|
|
613
|
+
function listProfiles(db) {
|
|
614
|
+
const d = db || getDatabase();
|
|
615
|
+
return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
|
|
616
|
+
}
|
|
617
|
+
function updateProfile(idOrSlug, input, db) {
|
|
618
|
+
const d = db || getDatabase();
|
|
619
|
+
const existing = getProfile(idOrSlug, d);
|
|
620
|
+
const ts = now();
|
|
621
|
+
const updates = ["updated_at = ?"];
|
|
622
|
+
const params = [ts];
|
|
623
|
+
if (input.name !== undefined) {
|
|
624
|
+
updates.push("name = ?", "slug = ?");
|
|
625
|
+
params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
|
|
626
|
+
}
|
|
627
|
+
if (input.description !== undefined) {
|
|
628
|
+
updates.push("description = ?");
|
|
629
|
+
params.push(input.description);
|
|
630
|
+
}
|
|
631
|
+
if (input.selectors !== undefined) {
|
|
632
|
+
updates.push("selectors = ?");
|
|
633
|
+
params.push(JSON.stringify(input.selectors));
|
|
634
|
+
}
|
|
635
|
+
if (input.variables !== undefined) {
|
|
636
|
+
updates.push("variables = ?");
|
|
637
|
+
params.push(JSON.stringify(input.variables));
|
|
638
|
+
}
|
|
639
|
+
params.push(existing.id);
|
|
640
|
+
d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
641
|
+
return getProfile(existing.id, d);
|
|
642
|
+
}
|
|
643
|
+
function deleteProfile(idOrSlug, db) {
|
|
644
|
+
const d = db || getDatabase();
|
|
645
|
+
const existing = getProfile(idOrSlug, d);
|
|
646
|
+
d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
|
|
647
|
+
}
|
|
648
|
+
function addConfigToProfile(profileIdOrSlug, configId, db) {
|
|
649
|
+
const d = db || getDatabase();
|
|
650
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
651
|
+
const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
|
|
652
|
+
const order = (maxRow?.max_order ?? -1) + 1;
|
|
653
|
+
d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order) VALUES (?, ?, ?)", [profile.id, configId, order]);
|
|
654
|
+
}
|
|
655
|
+
function removeConfigFromProfile(profileIdOrSlug, configId, db) {
|
|
656
|
+
const d = db || getDatabase();
|
|
657
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
658
|
+
d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
|
|
659
|
+
}
|
|
660
|
+
function getProfileConfigs(profileIdOrSlug, db) {
|
|
661
|
+
const d = db || getDatabase();
|
|
662
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
663
|
+
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
|
|
664
|
+
if (rows.length === 0)
|
|
665
|
+
return [];
|
|
666
|
+
const ids = rows.map((r) => r.config_id);
|
|
667
|
+
return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
|
|
668
|
+
}
|
|
669
|
+
function profileHasSelectors(profile) {
|
|
670
|
+
const selectors = profile.selectors ?? {};
|
|
671
|
+
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
672
|
+
}
|
|
673
|
+
function profileMatchesMachine(profile, machine) {
|
|
674
|
+
const selectors = profile.selectors ?? {};
|
|
675
|
+
const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
|
|
676
|
+
const value = candidate.trim().toLowerCase();
|
|
677
|
+
return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
|
|
678
|
+
});
|
|
679
|
+
const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
|
|
680
|
+
const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
|
|
681
|
+
return osMatches && archMatches && hostnameMatches;
|
|
682
|
+
}
|
|
683
|
+
function resolveProfileForMachine(machine = detectMachineContext(), db) {
|
|
684
|
+
const profiles = listProfiles(db).filter(profileHasSelectors);
|
|
685
|
+
const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
|
|
686
|
+
const selectors = profile.selectors;
|
|
687
|
+
const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
|
|
688
|
+
return { profile, score };
|
|
689
|
+
}).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
|
|
690
|
+
return matches[0]?.profile ?? null;
|
|
691
|
+
}
|
|
692
|
+
var init_profiles = __esm(() => {
|
|
693
|
+
init_types();
|
|
694
|
+
init_database();
|
|
695
|
+
init_configs();
|
|
696
|
+
init_machine();
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
// src/db/snapshots.ts
|
|
700
|
+
function createSnapshot(configId, content, version, db) {
|
|
701
|
+
const d = db || getDatabase();
|
|
702
|
+
const id = uuid();
|
|
703
|
+
const ts = now();
|
|
704
|
+
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
705
|
+
return { id, config_id: configId, content, version, created_at: ts };
|
|
706
|
+
}
|
|
707
|
+
function listSnapshots(configId, db) {
|
|
708
|
+
const d = db || getDatabase();
|
|
709
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
710
|
+
}
|
|
711
|
+
function getSnapshot(id, db) {
|
|
712
|
+
const d = db || getDatabase();
|
|
713
|
+
return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
|
|
714
|
+
}
|
|
715
|
+
function getSnapshotByVersion(configId, version, db) {
|
|
716
|
+
const d = db || getDatabase();
|
|
717
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
718
|
+
}
|
|
719
|
+
function pruneSnapshots(configId, keep = 10, db) {
|
|
720
|
+
const d = db || getDatabase();
|
|
721
|
+
const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
|
|
722
|
+
SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
|
|
723
|
+
)`, [configId, configId, keep]);
|
|
724
|
+
return result.changes;
|
|
725
|
+
}
|
|
726
|
+
var init_snapshots = __esm(() => {
|
|
727
|
+
init_database();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// src/db/machines.ts
|
|
731
|
+
import { arch, hostname, type } from "os";
|
|
732
|
+
function currentHostname2() {
|
|
733
|
+
return hostname();
|
|
734
|
+
}
|
|
735
|
+
function currentOs() {
|
|
736
|
+
return type();
|
|
737
|
+
}
|
|
738
|
+
function currentArch2() {
|
|
739
|
+
return arch();
|
|
740
|
+
}
|
|
741
|
+
function registerMachine(hostnameStr, os, archStr, db) {
|
|
742
|
+
const d = db || getDatabase();
|
|
743
|
+
const h = hostnameStr ?? currentHostname2();
|
|
744
|
+
const o = os ?? currentOs();
|
|
745
|
+
const a = archStr ?? currentArch2();
|
|
746
|
+
const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
|
|
747
|
+
if (existing) {
|
|
748
|
+
if (existing.os !== o || existing.arch !== a) {
|
|
749
|
+
d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
|
|
750
|
+
return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
|
|
751
|
+
}
|
|
752
|
+
return existing;
|
|
753
|
+
}
|
|
754
|
+
const id = uuid();
|
|
755
|
+
const ts = now();
|
|
756
|
+
d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
|
|
757
|
+
return d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
758
|
+
}
|
|
759
|
+
function updateMachineApplied(hostnameStr, db) {
|
|
760
|
+
const d = db || getDatabase();
|
|
761
|
+
const h = hostnameStr ?? currentHostname2();
|
|
762
|
+
d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now(), h]);
|
|
763
|
+
}
|
|
764
|
+
function listMachines(db) {
|
|
765
|
+
const d = db || getDatabase();
|
|
766
|
+
return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
|
|
767
|
+
}
|
|
768
|
+
var init_machines = __esm(() => {
|
|
769
|
+
init_database();
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// src/data/config-store.ts
|
|
773
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
774
|
+
function resolveCloudConfig(env = process.env) {
|
|
775
|
+
const apiUrl = env[API_URL_ENV]?.trim();
|
|
776
|
+
const apiKey = env[API_KEY_ENV]?.trim();
|
|
777
|
+
if (!apiUrl && !apiKey)
|
|
778
|
+
return null;
|
|
779
|
+
if (!apiUrl || !apiKey) {
|
|
780
|
+
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.`);
|
|
781
|
+
}
|
|
782
|
+
return { apiUrl, apiKey };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
class LocalConfigStore {
|
|
786
|
+
db;
|
|
787
|
+
mode = "local";
|
|
788
|
+
constructor(db) {
|
|
789
|
+
this.db = db;
|
|
790
|
+
}
|
|
791
|
+
async listConfigs(filter) {
|
|
792
|
+
return listConfigs(filter, this.db);
|
|
793
|
+
}
|
|
794
|
+
async getConfig(idOrSlug) {
|
|
795
|
+
return getConfig(idOrSlug, this.db);
|
|
796
|
+
}
|
|
797
|
+
async getConfigById(id) {
|
|
798
|
+
return getConfigById(id, this.db);
|
|
799
|
+
}
|
|
800
|
+
async createConfig(input) {
|
|
801
|
+
return createConfig(input, this.db);
|
|
802
|
+
}
|
|
803
|
+
async updateConfig(idOrSlug, input) {
|
|
804
|
+
return updateConfig(idOrSlug, input, this.db);
|
|
805
|
+
}
|
|
806
|
+
async deleteConfig(idOrSlug) {
|
|
807
|
+
deleteConfig(idOrSlug, this.db);
|
|
808
|
+
}
|
|
809
|
+
async getConfigStats() {
|
|
810
|
+
return getConfigStats(this.db);
|
|
811
|
+
}
|
|
812
|
+
async listSnapshots(configId) {
|
|
813
|
+
return listSnapshots(configId, this.db);
|
|
814
|
+
}
|
|
815
|
+
async getSnapshot(id) {
|
|
816
|
+
return getSnapshot(id, this.db);
|
|
817
|
+
}
|
|
818
|
+
async getSnapshotByVersion(configId, version) {
|
|
819
|
+
return getSnapshotByVersion(configId, version, this.db);
|
|
820
|
+
}
|
|
821
|
+
async createSnapshot(configId, content, version) {
|
|
822
|
+
return createSnapshot(configId, content, version, this.db);
|
|
823
|
+
}
|
|
824
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
825
|
+
return pruneSnapshots(configId, keep, this.db);
|
|
826
|
+
}
|
|
827
|
+
async listProfiles() {
|
|
828
|
+
return listProfiles(this.db);
|
|
829
|
+
}
|
|
830
|
+
async getProfile(idOrSlug) {
|
|
831
|
+
return getProfile(idOrSlug, this.db);
|
|
832
|
+
}
|
|
833
|
+
async getProfileConfigs(idOrSlug) {
|
|
834
|
+
return getProfileConfigs(idOrSlug, this.db);
|
|
835
|
+
}
|
|
836
|
+
async createProfile(input) {
|
|
837
|
+
return createProfile(input, this.db);
|
|
838
|
+
}
|
|
839
|
+
async updateProfile(idOrSlug, input) {
|
|
840
|
+
return updateProfile(idOrSlug, input, this.db);
|
|
841
|
+
}
|
|
842
|
+
async deleteProfile(idOrSlug) {
|
|
843
|
+
deleteProfile(idOrSlug, this.db);
|
|
844
|
+
}
|
|
845
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
846
|
+
addConfigToProfile(profileIdOrSlug, configId, this.db);
|
|
847
|
+
}
|
|
848
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
849
|
+
removeConfigFromProfile(profileIdOrSlug, configId, this.db);
|
|
850
|
+
}
|
|
851
|
+
async resolveProfileForMachine(machine) {
|
|
852
|
+
return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
|
|
853
|
+
}
|
|
854
|
+
async registerMachine(hostname2, os, arch2) {
|
|
855
|
+
return registerMachine(hostname2, os, arch2, this.db);
|
|
856
|
+
}
|
|
857
|
+
async updateMachineApplied(hostname2) {
|
|
858
|
+
updateMachineApplied(hostname2, this.db);
|
|
859
|
+
}
|
|
860
|
+
async listMachines() {
|
|
861
|
+
return listMachines(this.db);
|
|
862
|
+
}
|
|
863
|
+
async sendFeedback(input) {
|
|
864
|
+
insertFeedback(input, this.db);
|
|
865
|
+
}
|
|
866
|
+
async reset() {
|
|
867
|
+
resetLocalDatabase();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
class CloudConfigStore {
|
|
872
|
+
mode = "api";
|
|
873
|
+
base;
|
|
874
|
+
apiKey;
|
|
875
|
+
timeoutMs;
|
|
876
|
+
constructor(config) {
|
|
877
|
+
this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
|
|
878
|
+
this.apiKey = config.apiKey;
|
|
879
|
+
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
880
|
+
}
|
|
881
|
+
async request(method, path, body, opts = {}) {
|
|
882
|
+
const controller = new AbortController;
|
|
883
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
884
|
+
const headers = {
|
|
885
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
886
|
+
Accept: "application/json"
|
|
887
|
+
};
|
|
888
|
+
if (body !== undefined)
|
|
889
|
+
headers["Content-Type"] = "application/json";
|
|
890
|
+
if (opts.idempotent)
|
|
891
|
+
headers["Idempotency-Key"] = randomUUID2();
|
|
892
|
+
try {
|
|
893
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
894
|
+
method,
|
|
895
|
+
headers,
|
|
896
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
897
|
+
signal: controller.signal
|
|
898
|
+
});
|
|
899
|
+
if (res.status === 404 && opts.allow404)
|
|
900
|
+
return { status: 404, data: null };
|
|
901
|
+
const text = await res.text();
|
|
902
|
+
let parsed = null;
|
|
903
|
+
if (text) {
|
|
904
|
+
try {
|
|
905
|
+
parsed = JSON.parse(text);
|
|
906
|
+
} catch {
|
|
907
|
+
parsed = text;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (!res.ok) {
|
|
911
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
912
|
+
throw new CloudHttpError(res.status, message, parsed);
|
|
913
|
+
}
|
|
914
|
+
return { status: res.status, data: parsed };
|
|
915
|
+
} finally {
|
|
916
|
+
clearTimeout(timer);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
async listConfigs(filter = {}) {
|
|
920
|
+
const params = new URLSearchParams;
|
|
921
|
+
if (filter.category)
|
|
922
|
+
params.set("category", filter.category);
|
|
923
|
+
if (filter.agent)
|
|
924
|
+
params.set("agent", filter.agent);
|
|
925
|
+
if (filter.kind)
|
|
926
|
+
params.set("kind", filter.kind);
|
|
927
|
+
if (filter.search)
|
|
928
|
+
params.set("search", filter.search);
|
|
929
|
+
const qs = params.toString();
|
|
930
|
+
const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
|
|
931
|
+
let configs = data?.configs ?? [];
|
|
932
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
933
|
+
configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
|
|
934
|
+
}
|
|
935
|
+
if (filter.is_template !== undefined) {
|
|
936
|
+
configs = configs.filter((c) => c.is_template === filter.is_template);
|
|
937
|
+
}
|
|
938
|
+
return configs;
|
|
939
|
+
}
|
|
940
|
+
async getConfig(idOrSlug) {
|
|
941
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
942
|
+
if (status === 404 || !data?.config)
|
|
943
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
944
|
+
return data.config;
|
|
945
|
+
}
|
|
946
|
+
async getConfigById(id) {
|
|
947
|
+
return this.getConfig(id);
|
|
948
|
+
}
|
|
949
|
+
async createConfig(input) {
|
|
950
|
+
const { data } = await this.request("POST", "/configs", input, {
|
|
951
|
+
idempotent: true
|
|
952
|
+
});
|
|
953
|
+
return data.config;
|
|
954
|
+
}
|
|
955
|
+
async updateConfig(idOrSlug, input) {
|
|
956
|
+
const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
|
|
957
|
+
return data.config;
|
|
958
|
+
}
|
|
959
|
+
async deleteConfig(idOrSlug) {
|
|
960
|
+
const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
961
|
+
if (status === 404)
|
|
962
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
963
|
+
}
|
|
964
|
+
async getConfigStats() {
|
|
965
|
+
const { data } = await this.request("GET", "/stats");
|
|
966
|
+
return data ?? { total: 0 };
|
|
967
|
+
}
|
|
968
|
+
async listSnapshots(configId) {
|
|
969
|
+
const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
|
|
970
|
+
return data?.snapshots ?? [];
|
|
971
|
+
}
|
|
972
|
+
async getSnapshot(id) {
|
|
973
|
+
const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
974
|
+
if (status === 404 || !data?.snapshot)
|
|
975
|
+
return null;
|
|
976
|
+
return data.snapshot;
|
|
977
|
+
}
|
|
978
|
+
async getSnapshotByVersion(configId, version) {
|
|
979
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
|
|
980
|
+
if (status === 404 || !data?.snapshot)
|
|
981
|
+
return null;
|
|
982
|
+
return data.snapshot;
|
|
983
|
+
}
|
|
984
|
+
async createSnapshot(configId, content, version) {
|
|
985
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
|
|
986
|
+
return data.snapshot;
|
|
987
|
+
}
|
|
988
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
989
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
|
|
990
|
+
return data?.pruned ?? 0;
|
|
991
|
+
}
|
|
992
|
+
async listProfiles() {
|
|
993
|
+
const { data } = await this.request("GET", "/profiles");
|
|
994
|
+
return data?.profiles ?? [];
|
|
995
|
+
}
|
|
996
|
+
async getProfile(idOrSlug) {
|
|
997
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
998
|
+
if (status === 404 || !data?.profile)
|
|
999
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1000
|
+
const { configs: _configs, ...profile } = data.profile;
|
|
1001
|
+
return profile;
|
|
1002
|
+
}
|
|
1003
|
+
async getProfileConfigs(idOrSlug) {
|
|
1004
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1005
|
+
if (status === 404 || !data?.profile)
|
|
1006
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1007
|
+
return data.profile.configs ?? [];
|
|
1008
|
+
}
|
|
1009
|
+
async createProfile(input) {
|
|
1010
|
+
const { data } = await this.request("POST", "/profiles", input, {
|
|
1011
|
+
idempotent: true
|
|
1012
|
+
});
|
|
1013
|
+
return data.profile;
|
|
1014
|
+
}
|
|
1015
|
+
async updateProfile(idOrSlug, input) {
|
|
1016
|
+
const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
|
|
1017
|
+
return data.profile;
|
|
1018
|
+
}
|
|
1019
|
+
async deleteProfile(idOrSlug) {
|
|
1020
|
+
const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1021
|
+
if (status === 404)
|
|
1022
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1023
|
+
}
|
|
1024
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
1025
|
+
await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
|
|
1026
|
+
}
|
|
1027
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
1028
|
+
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
1029
|
+
}
|
|
1030
|
+
async resolveProfileForMachine(machine) {
|
|
1031
|
+
const params = new URLSearchParams;
|
|
1032
|
+
if (machine?.hostname)
|
|
1033
|
+
params.set("hostname", machine.hostname);
|
|
1034
|
+
if (machine?.os)
|
|
1035
|
+
params.set("os", machine.os);
|
|
1036
|
+
if (machine?.arch)
|
|
1037
|
+
params.set("arch", machine.arch);
|
|
1038
|
+
const qs = params.toString();
|
|
1039
|
+
const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
|
|
1040
|
+
if (status === 404 || !data?.profile)
|
|
1041
|
+
return null;
|
|
1042
|
+
return data.profile;
|
|
1043
|
+
}
|
|
1044
|
+
async registerMachine(hostname2, os, arch2) {
|
|
1045
|
+
const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
|
|
1046
|
+
return data.machine;
|
|
1047
|
+
}
|
|
1048
|
+
async updateMachineApplied(hostname2) {
|
|
1049
|
+
await this.request("POST", "/machines/applied", { hostname: hostname2 });
|
|
1050
|
+
}
|
|
1051
|
+
async listMachines() {
|
|
1052
|
+
const { data } = await this.request("GET", "/machines");
|
|
1053
|
+
return data?.machines ?? [];
|
|
1054
|
+
}
|
|
1055
|
+
async sendFeedback(input) {
|
|
1056
|
+
await this.request("POST", "/feedback", {
|
|
1057
|
+
message: input.message,
|
|
1058
|
+
email: input.email ?? undefined,
|
|
1059
|
+
category: input.category ?? undefined,
|
|
1060
|
+
version: input.version ?? undefined
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async reset() {
|
|
1064
|
+
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.");
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
function resolveConfigStore(env = process.env) {
|
|
1068
|
+
const cloud = resolveCloudConfig(env);
|
|
1069
|
+
return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
|
|
1070
|
+
}
|
|
1071
|
+
var CloudHttpError, API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL", API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
|
|
1072
|
+
var init_config_store = __esm(() => {
|
|
1073
|
+
init_configs();
|
|
1074
|
+
init_profiles();
|
|
1075
|
+
init_snapshots();
|
|
1076
|
+
init_machines();
|
|
1077
|
+
init_database();
|
|
1078
|
+
init_types();
|
|
1079
|
+
CloudHttpError = class CloudHttpError extends Error {
|
|
1080
|
+
status;
|
|
1081
|
+
body;
|
|
1082
|
+
constructor(status, message, body) {
|
|
1083
|
+
super(message);
|
|
1084
|
+
this.status = status;
|
|
1085
|
+
this.body = body;
|
|
1086
|
+
this.name = "CloudHttpError";
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
});
|
|
1090
|
+
|
|
591
1091
|
// src/lib/transforms.ts
|
|
592
1092
|
import { basename, extname } from "path";
|
|
593
1093
|
function ensureTrailingNewline(content) {
|
|
@@ -755,8 +1255,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
755
1255
|
mkdirSync2(dir, { recursive: true });
|
|
756
1256
|
}
|
|
757
1257
|
if (previousContent !== null && changed) {
|
|
758
|
-
const
|
|
759
|
-
createSnapshot(config.id, previousContent, config.version
|
|
1258
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1259
|
+
await store.createSnapshot(config.id, previousContent, config.version);
|
|
760
1260
|
}
|
|
761
1261
|
writeFileSync(path, renderedContent, "utf-8");
|
|
762
1262
|
}
|
|
@@ -782,8 +1282,8 @@ async function applyConfig(config, opts = {}) {
|
|
|
782
1282
|
if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
|
|
783
1283
|
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
784
1284
|
}
|
|
785
|
-
const
|
|
786
|
-
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(
|
|
1285
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1286
|
+
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
787
1287
|
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
788
1288
|
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
789
1289
|
}
|
|
@@ -804,7 +1304,7 @@ async function applyConfig(config, opts = {}) {
|
|
|
804
1304
|
};
|
|
805
1305
|
}
|
|
806
1306
|
if (!opts.dryRun) {
|
|
807
|
-
updateConfig(config.id, { synced_at:
|
|
1307
|
+
await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
|
|
808
1308
|
}
|
|
809
1309
|
return result;
|
|
810
1310
|
}
|
|
@@ -826,9 +1326,7 @@ async function applyConfigs(configs, opts = {}) {
|
|
|
826
1326
|
}
|
|
827
1327
|
var init_apply = __esm(() => {
|
|
828
1328
|
init_types();
|
|
829
|
-
|
|
830
|
-
init_configs();
|
|
831
|
-
init_snapshots();
|
|
1329
|
+
init_config_store();
|
|
832
1330
|
init_machine();
|
|
833
1331
|
init_transforms();
|
|
834
1332
|
});
|
|
@@ -922,9 +1420,9 @@ function redactIni(content) {
|
|
|
922
1420
|
for (let i = 0;i < lines.length; i++) {
|
|
923
1421
|
const line = lines[i];
|
|
924
1422
|
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
925
|
-
if (authM && !authM[2].
|
|
926
|
-
redacted.push({ varName: "
|
|
927
|
-
out.push(`${authM[1]}{
|
|
1423
|
+
if (authM && !isReferenceValue(authM[2].trim())) {
|
|
1424
|
+
redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
1425
|
+
out.push(`${authM[1]}\${NPM_TOKEN}`);
|
|
928
1426
|
continue;
|
|
929
1427
|
}
|
|
930
1428
|
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
@@ -964,6 +1462,8 @@ function redactGeneric(content) {
|
|
|
964
1462
|
function shouldRedactKeyValue(key, value) {
|
|
965
1463
|
if (!value || value.startsWith("{{"))
|
|
966
1464
|
return false;
|
|
1465
|
+
if (isReferenceValue(value.trim()))
|
|
1466
|
+
return false;
|
|
967
1467
|
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
968
1468
|
return false;
|
|
969
1469
|
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
@@ -985,6 +1485,9 @@ function reasonFor(key, value) {
|
|
|
985
1485
|
}
|
|
986
1486
|
return "secret value pattern";
|
|
987
1487
|
}
|
|
1488
|
+
function isReferenceValue(value) {
|
|
1489
|
+
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);
|
|
1490
|
+
}
|
|
988
1491
|
function redactContent(content, format) {
|
|
989
1492
|
switch (format) {
|
|
990
1493
|
case "shell":
|
|
@@ -1088,11 +1591,11 @@ function isKnownGeneratedTargetPath(targetPath) {
|
|
|
1088
1591
|
return hasClaudeRuleSourceForCursorTarget(targetPath);
|
|
1089
1592
|
}
|
|
1090
1593
|
async function syncProject(opts) {
|
|
1091
|
-
const
|
|
1594
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1092
1595
|
const absDir = expandPath(opts.projectDir);
|
|
1093
1596
|
const projectName = absDir.split("/").pop() || "project";
|
|
1094
1597
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1095
|
-
const allConfigs = listConfigs(
|
|
1598
|
+
const allConfigs = await store.listConfigs();
|
|
1096
1599
|
const machine = detectMachineContext();
|
|
1097
1600
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
1098
1601
|
const abs = join3(absDir, pf.file);
|
|
@@ -1114,11 +1617,11 @@ async function syncProject(opts) {
|
|
|
1114
1617
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1115
1618
|
if (!existing) {
|
|
1116
1619
|
if (!opts.dryRun)
|
|
1117
|
-
createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }
|
|
1620
|
+
await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
|
|
1118
1621
|
result.added++;
|
|
1119
1622
|
} else if (existing.content !== content) {
|
|
1120
1623
|
if (!opts.dryRun)
|
|
1121
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
1624
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
1122
1625
|
result.updated++;
|
|
1123
1626
|
} else {
|
|
1124
1627
|
result.unchanged++;
|
|
@@ -1143,11 +1646,11 @@ async function syncProject(opts) {
|
|
|
1143
1646
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug);
|
|
1144
1647
|
if (!existing) {
|
|
1145
1648
|
if (!opts.dryRun)
|
|
1146
|
-
createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }
|
|
1649
|
+
await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
|
|
1147
1650
|
result.added++;
|
|
1148
1651
|
} else if (existing.content !== content) {
|
|
1149
1652
|
if (!opts.dryRun)
|
|
1150
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
1653
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
1151
1654
|
result.updated++;
|
|
1152
1655
|
} else {
|
|
1153
1656
|
result.unchanged++;
|
|
@@ -1157,7 +1660,7 @@ async function syncProject(opts) {
|
|
|
1157
1660
|
return result;
|
|
1158
1661
|
}
|
|
1159
1662
|
async function syncKnown(opts = {}) {
|
|
1160
|
-
const
|
|
1663
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1161
1664
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1162
1665
|
const home = getConfigHome();
|
|
1163
1666
|
const machine = detectMachineContext();
|
|
@@ -1166,7 +1669,7 @@ async function syncKnown(opts = {}) {
|
|
|
1166
1669
|
targets = targets.filter((k) => k.agent === opts.agent);
|
|
1167
1670
|
if (opts.category)
|
|
1168
1671
|
targets = targets.filter((k) => k.category === opts.category);
|
|
1169
|
-
const allConfigs = listConfigs(
|
|
1672
|
+
const allConfigs = await store.listConfigs();
|
|
1170
1673
|
const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
|
|
1171
1674
|
for (const known of targets) {
|
|
1172
1675
|
if (known.rulesDir) {
|
|
@@ -1195,15 +1698,15 @@ async function syncKnown(opts = {}) {
|
|
|
1195
1698
|
const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
|
|
1196
1699
|
if (!existing) {
|
|
1197
1700
|
if (!opts.dryRun)
|
|
1198
|
-
createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }
|
|
1701
|
+
await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
|
|
1199
1702
|
result.added++;
|
|
1200
1703
|
} else if (existing.content !== content) {
|
|
1201
1704
|
if (!opts.dryRun)
|
|
1202
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs }
|
|
1705
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
|
|
1203
1706
|
result.updated++;
|
|
1204
1707
|
} else if (!outputsEqual(existing.outputs, outputs)) {
|
|
1205
1708
|
if (!opts.dryRun)
|
|
1206
|
-
updateConfig(existing.id, { outputs }
|
|
1709
|
+
await store.updateConfig(existing.id, { outputs });
|
|
1207
1710
|
result.updated++;
|
|
1208
1711
|
} else {
|
|
1209
1712
|
result.unchanged++;
|
|
@@ -1235,7 +1738,7 @@ async function syncKnown(opts = {}) {
|
|
|
1235
1738
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
|
|
1236
1739
|
if (!existing) {
|
|
1237
1740
|
if (!opts.dryRun) {
|
|
1238
|
-
createConfig({
|
|
1741
|
+
await store.createConfig({
|
|
1239
1742
|
name: known.name,
|
|
1240
1743
|
category: known.category,
|
|
1241
1744
|
agent: known.agent,
|
|
@@ -1246,16 +1749,16 @@ async function syncKnown(opts = {}) {
|
|
|
1246
1749
|
description: known.description,
|
|
1247
1750
|
is_template: isTemplate2,
|
|
1248
1751
|
outputs: known.outputs
|
|
1249
|
-
}
|
|
1752
|
+
});
|
|
1250
1753
|
}
|
|
1251
1754
|
result.added++;
|
|
1252
1755
|
} else if (existing.content !== content) {
|
|
1253
1756
|
if (!opts.dryRun)
|
|
1254
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }
|
|
1757
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
|
|
1255
1758
|
result.updated++;
|
|
1256
1759
|
} else if (!outputsEqual(existing.outputs, known.outputs)) {
|
|
1257
1760
|
if (!opts.dryRun)
|
|
1258
|
-
updateConfig(existing.id, { outputs: known.outputs }
|
|
1761
|
+
await store.updateConfig(existing.id, { outputs: known.outputs });
|
|
1259
1762
|
result.updated++;
|
|
1260
1763
|
} else {
|
|
1261
1764
|
result.unchanged++;
|
|
@@ -1267,9 +1770,9 @@ async function syncKnown(opts = {}) {
|
|
|
1267
1770
|
return result;
|
|
1268
1771
|
}
|
|
1269
1772
|
async function syncToDisk(opts = {}) {
|
|
1270
|
-
const
|
|
1773
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1271
1774
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1272
|
-
const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }
|
|
1775
|
+
const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
|
|
1273
1776
|
const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
|
|
1274
1777
|
let configs = allFileConfigs.filter((config) => {
|
|
1275
1778
|
return !isGeneratedOutputTarget2(config, outputOwners);
|
|
@@ -1282,7 +1785,7 @@ async function syncToDisk(opts = {}) {
|
|
|
1282
1785
|
if (!config.target_path && config.outputs.length === 0)
|
|
1283
1786
|
continue;
|
|
1284
1787
|
try {
|
|
1285
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
1788
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
|
|
1286
1789
|
r.changed ? result.updated++ : result.unchanged++;
|
|
1287
1790
|
} catch {
|
|
1288
1791
|
result.skipped.push(config.target_path ?? config.id);
|
|
@@ -1319,12 +1822,12 @@ function buildDiff(expectedContent, targetPath) {
|
|
|
1319
1822
|
return lines.join(`
|
|
1320
1823
|
`);
|
|
1321
1824
|
}
|
|
1322
|
-
function diffConfig(config, opts = {}) {
|
|
1825
|
+
async function diffConfig(config, opts = {}) {
|
|
1323
1826
|
if (!config.target_path && config.outputs.length === 0)
|
|
1324
1827
|
return "(reference \u2014 no target path)";
|
|
1325
1828
|
const diffs = [];
|
|
1326
|
-
const
|
|
1327
|
-
const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(
|
|
1829
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1830
|
+
const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
1328
1831
|
if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
|
|
1329
1832
|
return "(generated output \u2014 managed by fan-out)";
|
|
1330
1833
|
}
|
|
@@ -1403,8 +1906,7 @@ function detectFormat(filePath) {
|
|
|
1403
1906
|
}
|
|
1404
1907
|
var CLAUDE_PROMPT_OUTPUTS, KNOWN_CONFIGS, PROJECT_CONFIG_FILES;
|
|
1405
1908
|
var init_sync = __esm(() => {
|
|
1406
|
-
|
|
1407
|
-
init_configs();
|
|
1909
|
+
init_config_store();
|
|
1408
1910
|
init_apply();
|
|
1409
1911
|
init_redact();
|
|
1410
1912
|
init_machine();
|
|
@@ -1467,14 +1969,14 @@ function shouldSkip(p) {
|
|
|
1467
1969
|
return SKIP.some((s) => p.includes(s));
|
|
1468
1970
|
}
|
|
1469
1971
|
async function syncFromDir(dir, opts = {}) {
|
|
1470
|
-
const
|
|
1972
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1471
1973
|
const absDir = expandPath(dir);
|
|
1472
1974
|
if (!existsSync5(absDir))
|
|
1473
1975
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
1474
1976
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync2(absDir).map((f) => join4(absDir, f)).filter((f) => statSync2(f).isFile());
|
|
1475
1977
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1476
1978
|
const home = homedir3();
|
|
1477
|
-
const allConfigs = listConfigs(
|
|
1979
|
+
const allConfigs = await store.listConfigs();
|
|
1478
1980
|
for (const file of files) {
|
|
1479
1981
|
if (shouldSkip(file)) {
|
|
1480
1982
|
result.skipped.push(file);
|
|
@@ -1490,11 +1992,11 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
1490
1992
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
1491
1993
|
if (!existing) {
|
|
1492
1994
|
if (!opts.dryRun)
|
|
1493
|
-
createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }
|
|
1995
|
+
await store.createConfig({ name: relative(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
|
|
1494
1996
|
result.added++;
|
|
1495
1997
|
} else if (existing.content !== content) {
|
|
1496
1998
|
if (!opts.dryRun)
|
|
1497
|
-
updateConfig(existing.id, { content }
|
|
1999
|
+
await store.updateConfig(existing.id, { content });
|
|
1498
2000
|
result.updated++;
|
|
1499
2001
|
} else {
|
|
1500
2002
|
result.unchanged++;
|
|
@@ -1506,17 +2008,17 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
1506
2008
|
return result;
|
|
1507
2009
|
}
|
|
1508
2010
|
async function syncToDir(dir, opts = {}) {
|
|
1509
|
-
const
|
|
2011
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1510
2012
|
const home = homedir3();
|
|
1511
2013
|
const absDir = expandPath(dir);
|
|
1512
2014
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
1513
|
-
const configs = listConfigs(
|
|
2015
|
+
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
1514
2016
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
1515
2017
|
for (const config of configs) {
|
|
1516
2018
|
if (config.kind === "reference")
|
|
1517
2019
|
continue;
|
|
1518
2020
|
try {
|
|
1519
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
2021
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store });
|
|
1520
2022
|
r.changed ? result.updated++ : result.unchanged++;
|
|
1521
2023
|
} catch {
|
|
1522
2024
|
result.skipped.push(config.target_path || config.id);
|
|
@@ -1538,8 +2040,7 @@ function walkDir(dir, files = []) {
|
|
|
1538
2040
|
}
|
|
1539
2041
|
var SKIP;
|
|
1540
2042
|
var init_sync_dir = __esm(() => {
|
|
1541
|
-
|
|
1542
|
-
init_configs();
|
|
2043
|
+
init_config_store();
|
|
1543
2044
|
init_apply();
|
|
1544
2045
|
init_sync();
|
|
1545
2046
|
SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
|
|
@@ -1549,7 +2050,7 @@ var init_sync_dir = __esm(() => {
|
|
|
1549
2050
|
var require_package = __commonJS((exports, module) => {
|
|
1550
2051
|
module.exports = {
|
|
1551
2052
|
name: "@hasna/instructions",
|
|
1552
|
-
version: "0.
|
|
2053
|
+
version: "0.4.0",
|
|
1553
2054
|
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
2055
|
type: "module",
|
|
1555
2056
|
main: "dist/index.js",
|
|
@@ -1564,10 +2065,6 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1564
2065
|
".": {
|
|
1565
2066
|
types: "./dist/index.d.ts",
|
|
1566
2067
|
import: "./dist/index.js"
|
|
1567
|
-
},
|
|
1568
|
-
"./storage": {
|
|
1569
|
-
types: "./dist/storage.d.ts",
|
|
1570
|
-
import: "./dist/storage.js"
|
|
1571
2068
|
}
|
|
1572
2069
|
},
|
|
1573
2070
|
files: [
|
|
@@ -1578,14 +2075,15 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1578
2075
|
],
|
|
1579
2076
|
scripts: {
|
|
1580
2077
|
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
|
|
2078
|
+
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",
|
|
2079
|
+
"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
2080
|
"build:dashboard": "cd dashboard && bun run build",
|
|
1584
2081
|
migrate: "bun run src/server/index.ts migrate",
|
|
1585
2082
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
1586
2083
|
"kit:check": "bunx @hasna/contracts vendor-kit --check",
|
|
1587
2084
|
typecheck: "tsc --noEmit",
|
|
1588
2085
|
test: "bun test",
|
|
2086
|
+
"check:package-secrets": "bun run src/cli/index.tsx package-manager-scan --fail-on-findings --home .",
|
|
1589
2087
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
1590
2088
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
1591
2089
|
"dev:serve": "bun run src/server/index.ts",
|
|
@@ -1625,7 +2123,7 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1625
2123
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
1626
2124
|
license: "Apache-2.0",
|
|
1627
2125
|
dependencies: {
|
|
1628
|
-
"@hasna/contracts": "
|
|
2126
|
+
"@hasna/contracts": "0.4.2",
|
|
1629
2127
|
"@hasna/events": "^0.1.6",
|
|
1630
2128
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
1631
2129
|
chalk: "^5.4.1",
|
|
@@ -1649,420 +2147,12 @@ var require_package = __commonJS((exports, module) => {
|
|
|
1649
2147
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1650
2148
|
|
|
1651
2149
|
// 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();
|
|
2150
|
+
init_config_store();
|
|
2003
2151
|
init_apply();
|
|
2004
2152
|
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
2153
|
init_machine();
|
|
2154
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2155
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2066
2156
|
|
|
2067
2157
|
// src/lib/compact-output.ts
|
|
2068
2158
|
var DEFAULT_LIST_LIMIT = 20;
|
|
@@ -2208,11 +2298,7 @@ var ALL_LEAN_TOOLS = [
|
|
|
2208
2298
|
{ name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
|
|
2209
2299
|
{ name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
|
|
2210
2300
|
{ 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" } } } } }
|
|
2301
|
+
{ 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
2302
|
];
|
|
2217
2303
|
function ok(data) {
|
|
2218
2304
|
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
@@ -2227,10 +2313,11 @@ function buildServer() {
|
|
|
2227
2313
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: LEAN_TOOLS }));
|
|
2228
2314
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
2229
2315
|
const { name, arguments: args = {} } = req.params;
|
|
2316
|
+
const store = resolveConfigStore();
|
|
2230
2317
|
try {
|
|
2231
2318
|
switch (name) {
|
|
2232
2319
|
case "list_configs": {
|
|
2233
|
-
const configs = listConfigs({
|
|
2320
|
+
const configs = await store.listConfigs({
|
|
2234
2321
|
category: args["category"] || undefined,
|
|
2235
2322
|
agent: args["agent"] || undefined,
|
|
2236
2323
|
kind: args["kind"] || undefined,
|
|
@@ -2244,11 +2331,11 @@ function buildServer() {
|
|
|
2244
2331
|
}));
|
|
2245
2332
|
}
|
|
2246
2333
|
case "get_config": {
|
|
2247
|
-
const c = getConfig(args["id_or_slug"]);
|
|
2334
|
+
const c = await store.getConfig(args["id_or_slug"]);
|
|
2248
2335
|
return ok(c);
|
|
2249
2336
|
}
|
|
2250
2337
|
case "create_config": {
|
|
2251
|
-
const c = createConfig({
|
|
2338
|
+
const c = await store.createConfig({
|
|
2252
2339
|
name: args["name"],
|
|
2253
2340
|
content: args["content"],
|
|
2254
2341
|
category: args["category"],
|
|
@@ -2264,7 +2351,7 @@ function buildServer() {
|
|
|
2264
2351
|
return ok({ id: c.id, slug: c.slug, name: c.name });
|
|
2265
2352
|
}
|
|
2266
2353
|
case "update_config": {
|
|
2267
|
-
const c = updateConfig(args["id_or_slug"], {
|
|
2354
|
+
const c = await store.updateConfig(args["id_or_slug"], {
|
|
2268
2355
|
content: args["content"],
|
|
2269
2356
|
name: args["name"],
|
|
2270
2357
|
tags: args["tags"],
|
|
@@ -2277,23 +2364,22 @@ function buildServer() {
|
|
|
2277
2364
|
return ok({ id: c.id, slug: c.slug, version: c.version });
|
|
2278
2365
|
}
|
|
2279
2366
|
case "delete_config": {
|
|
2280
|
-
|
|
2281
|
-
deleteConfig2(args["id_or_slug"]);
|
|
2367
|
+
await store.deleteConfig(args["id_or_slug"]);
|
|
2282
2368
|
return ok({ deleted: true });
|
|
2283
2369
|
}
|
|
2284
2370
|
case "apply_config": {
|
|
2285
|
-
const config = getConfig(args["id_or_slug"]);
|
|
2286
|
-
const result = await applyConfig(config, { dryRun: args["dry_run"] });
|
|
2371
|
+
const config = await store.getConfig(args["id_or_slug"]);
|
|
2372
|
+
const result = await applyConfig(config, { dryRun: args["dry_run"], store });
|
|
2287
2373
|
return ok(args["verbose"] ? result : summarizeApplyResult(result));
|
|
2288
2374
|
}
|
|
2289
2375
|
case "sync_directory": {
|
|
2290
2376
|
const dir = args["dir"];
|
|
2291
2377
|
const direction = args["direction"] || "from_disk";
|
|
2292
|
-
const result = direction === "to_disk" ? await syncToDir(dir) : await syncFromDir(dir);
|
|
2378
|
+
const result = direction === "to_disk" ? await syncToDir(dir, { store }) : await syncFromDir(dir, { store });
|
|
2293
2379
|
return ok(result);
|
|
2294
2380
|
}
|
|
2295
2381
|
case "list_profiles": {
|
|
2296
|
-
const profiles = listProfiles().map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
|
|
2382
|
+
const profiles = (await store.listProfiles()).map((profile) => summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }));
|
|
2297
2383
|
return ok(pagedPayload(profiles, {
|
|
2298
2384
|
limit: args["limit"],
|
|
2299
2385
|
cursor: args["cursor"],
|
|
@@ -2308,12 +2394,12 @@ function buildServer() {
|
|
|
2308
2394
|
});
|
|
2309
2395
|
if (!args["auto"] && !args["id_or_slug"])
|
|
2310
2396
|
return err("id_or_slug is required unless auto=true");
|
|
2311
|
-
const profile = args["auto"] ? resolveProfileForMachine(machine) : getProfile(args["id_or_slug"]);
|
|
2397
|
+
const profile = args["auto"] ? await store.resolveProfileForMachine(machine) : await store.getProfile(args["id_or_slug"]);
|
|
2312
2398
|
if (!profile)
|
|
2313
2399
|
return err("No matching machine-aware profile found");
|
|
2314
|
-
const configs = getProfileConfigs(profile.id);
|
|
2400
|
+
const configs = await store.getProfileConfigs(profile.id);
|
|
2315
2401
|
const vars = resolveProfileVariables(profile, machine);
|
|
2316
|
-
const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars });
|
|
2402
|
+
const results = await applyConfigs(configs, { dryRun: args["dry_run"], vars, store });
|
|
2317
2403
|
return ok({
|
|
2318
2404
|
profile: summarizeProfile(profile, { verbose: Boolean(args["verbose"]) }),
|
|
2319
2405
|
machine: {
|
|
@@ -2328,17 +2414,17 @@ function buildServer() {
|
|
|
2328
2414
|
});
|
|
2329
2415
|
}
|
|
2330
2416
|
case "get_snapshot": {
|
|
2331
|
-
const config = getConfig(args["config_id_or_slug"]);
|
|
2417
|
+
const config = await store.getConfig(args["config_id_or_slug"]);
|
|
2332
2418
|
if (args["version"]) {
|
|
2333
|
-
const snap = getSnapshotByVersion(config.id, args["version"]);
|
|
2419
|
+
const snap = await store.getSnapshotByVersion(config.id, args["version"]);
|
|
2334
2420
|
return snap ? ok(snap) : err("Snapshot not found");
|
|
2335
2421
|
}
|
|
2336
|
-
const snaps = listSnapshots(config.id);
|
|
2422
|
+
const snaps = await store.listSnapshots(config.id);
|
|
2337
2423
|
return ok(snaps[0] ?? null);
|
|
2338
2424
|
}
|
|
2339
2425
|
case "get_status": {
|
|
2340
|
-
const stats = getConfigStats();
|
|
2341
|
-
const allConfigs = listConfigs({ kind: "file" });
|
|
2426
|
+
const stats = await store.getConfigStats();
|
|
2427
|
+
const allConfigs = await store.listConfigs({ kind: "file" });
|
|
2342
2428
|
const { existsSync: ex, readFileSync: rf } = await import("fs");
|
|
2343
2429
|
const { expandPath: expandPath2 } = await Promise.resolve().then(() => (init_apply(), exports_apply));
|
|
2344
2430
|
const { redactContent: redactContent2 } = await Promise.resolve().then(() => (init_redact(), exports_redact));
|
|
@@ -2374,6 +2460,7 @@ function buildServer() {
|
|
|
2374
2460
|
case "sync_known": {
|
|
2375
2461
|
const { syncKnown: syncKnown2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
2376
2462
|
const result = await syncKnown2({
|
|
2463
|
+
store,
|
|
2377
2464
|
agent: args["agent"] || undefined,
|
|
2378
2465
|
category: args["category"] || undefined
|
|
2379
2466
|
});
|
|
@@ -2382,12 +2469,12 @@ function buildServer() {
|
|
|
2382
2469
|
case "sync_project": {
|
|
2383
2470
|
const { syncProject: syncProject2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
|
|
2384
2471
|
const dir = args["project_dir"] || process.cwd();
|
|
2385
|
-
const result = await syncProject2({ projectDir: dir });
|
|
2472
|
+
const result = await syncProject2({ store, projectDir: dir });
|
|
2386
2473
|
return ok(result);
|
|
2387
2474
|
}
|
|
2388
2475
|
case "render_template": {
|
|
2389
2476
|
const { renderTemplate: renderTemplate2 } = await Promise.resolve().then(() => (init_template(), exports_template));
|
|
2390
|
-
const config = getConfig(args["id_or_slug"]);
|
|
2477
|
+
const config = await store.getConfig(args["id_or_slug"]);
|
|
2391
2478
|
const vars = args["vars"] || {};
|
|
2392
2479
|
if (args["use_env"]) {
|
|
2393
2480
|
const { extractTemplateVars: extractTemplateVars2 } = await Promise.resolve().then(() => (init_template(), exports_template));
|
|
@@ -2402,7 +2489,7 @@ function buildServer() {
|
|
|
2402
2489
|
}
|
|
2403
2490
|
case "scan_secrets": {
|
|
2404
2491
|
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" });
|
|
2492
|
+
const configs = args["id_or_slug"] ? [await store.getConfig(args["id_or_slug"])] : await store.listConfigs({ kind: "file" });
|
|
2406
2493
|
const findings = [];
|
|
2407
2494
|
for (const c of configs) {
|
|
2408
2495
|
const fmt = c.format;
|
|
@@ -2411,7 +2498,7 @@ function buildServer() {
|
|
|
2411
2498
|
findings.push({ slug: c.slug, secrets: secrets.length, vars: secrets.map((s) => s.varName) });
|
|
2412
2499
|
if (args["fix"]) {
|
|
2413
2500
|
const { content, isTemplate: isTemplate2 } = redactContent2(c.content, fmt);
|
|
2414
|
-
updateConfig(c.id, { content, is_template: isTemplate2 });
|
|
2501
|
+
await store.updateConfig(c.id, { content, is_template: isTemplate2 });
|
|
2415
2502
|
}
|
|
2416
2503
|
}
|
|
2417
2504
|
}
|
|
@@ -2467,27 +2554,15 @@ function buildServer() {
|
|
|
2467
2554
|
return ok([..._cfgAgents.values()]);
|
|
2468
2555
|
}
|
|
2469
2556
|
case "send_feedback": {
|
|
2470
|
-
const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
2471
|
-
const db = getDatabase2();
|
|
2472
2557
|
const pkg = require_package();
|
|
2473
|
-
|
|
2558
|
+
await store.sendFeedback({
|
|
2559
|
+
message: args["message"],
|
|
2560
|
+
email: args["email"] || null,
|
|
2561
|
+
category: args["category"] || "general",
|
|
2562
|
+
version: pkg.version
|
|
2563
|
+
});
|
|
2474
2564
|
return ok({ message: "Feedback saved. Thank you!" });
|
|
2475
2565
|
}
|
|
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
2566
|
default:
|
|
2492
2567
|
return err(`Unknown tool: ${name}`);
|
|
2493
2568
|
}
|