@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/index.js
CHANGED
|
@@ -68,9 +68,12 @@ class TemplateRenderError extends Error {
|
|
|
68
68
|
this.name = "TemplateRenderError";
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
+
// src/data/config-store.ts
|
|
72
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
73
|
+
|
|
71
74
|
// src/db/database.ts
|
|
72
75
|
import { Database } from "bun:sqlite";
|
|
73
|
-
import { cpSync, existsSync, mkdirSync, statSync } from "fs";
|
|
76
|
+
import { cpSync, existsSync, mkdirSync, rmSync, statSync } from "fs";
|
|
74
77
|
import { join } from "path";
|
|
75
78
|
import { randomUUID } from "crypto";
|
|
76
79
|
function getDbPath() {
|
|
@@ -167,6 +170,9 @@ var _db = null;
|
|
|
167
170
|
function getDatabase(path) {
|
|
168
171
|
if (_db)
|
|
169
172
|
return _db;
|
|
173
|
+
if (!path && process.env["HASNA_INSTRUCTIONS_API_URL"] && process.env["HASNA_INSTRUCTIONS_API_KEY"]) {
|
|
174
|
+
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.");
|
|
175
|
+
}
|
|
170
176
|
const dbPath = path || getDbPath();
|
|
171
177
|
const db = new Database(dbPath);
|
|
172
178
|
db.run("PRAGMA journal_mode = WAL");
|
|
@@ -184,6 +190,16 @@ function resetDatabase() {
|
|
|
184
190
|
}
|
|
185
191
|
_db = null;
|
|
186
192
|
}
|
|
193
|
+
function resetLocalDatabase() {
|
|
194
|
+
resetDatabase();
|
|
195
|
+
const dbPath = getDbPath();
|
|
196
|
+
if (dbPath === ":memory:")
|
|
197
|
+
return;
|
|
198
|
+
for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
199
|
+
if (existsSync(p))
|
|
200
|
+
rmSync(p);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
187
203
|
function applyMigrations(db) {
|
|
188
204
|
let currentVersion = 0;
|
|
189
205
|
try {
|
|
@@ -210,6 +226,10 @@ function ensureFeedbackTable(db) {
|
|
|
210
226
|
)
|
|
211
227
|
`);
|
|
212
228
|
}
|
|
229
|
+
function insertFeedback(input, db) {
|
|
230
|
+
const d = db || getDatabase();
|
|
231
|
+
d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
|
|
232
|
+
}
|
|
213
233
|
function migrateDotfile() {
|
|
214
234
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
215
235
|
const oldDirs = [join(home, ".open-configs"), join(home, ".configs")];
|
|
@@ -409,33 +429,7 @@ function getConfigStats(db) {
|
|
|
409
429
|
}
|
|
410
430
|
return stats;
|
|
411
431
|
}
|
|
412
|
-
|
|
413
|
-
function createSnapshot(configId, content, version, db) {
|
|
414
|
-
const d = db || getDatabase();
|
|
415
|
-
const id = uuid();
|
|
416
|
-
const ts = now();
|
|
417
|
-
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
418
|
-
return { id, config_id: configId, content, version, created_at: ts };
|
|
419
|
-
}
|
|
420
|
-
function listSnapshots(configId, db) {
|
|
421
|
-
const d = db || getDatabase();
|
|
422
|
-
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
423
|
-
}
|
|
424
|
-
function getSnapshot(id, db) {
|
|
425
|
-
const d = db || getDatabase();
|
|
426
|
-
return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
|
|
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
|
-
function pruneSnapshots(configId, keep = 10, db) {
|
|
433
|
-
const d = db || getDatabase();
|
|
434
|
-
const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
|
|
435
|
-
SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
|
|
436
|
-
)`, [configId, configId, keep]);
|
|
437
|
-
return result.changes;
|
|
438
|
-
}
|
|
432
|
+
|
|
439
433
|
// src/lib/machine.ts
|
|
440
434
|
import { arch as currentArch, homedir, hostname as currentHostname, type as currentOsType } from "os";
|
|
441
435
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -701,6 +695,35 @@ function resolveProfileForMachine(machine = detectMachineContext(), db) {
|
|
|
701
695
|
}).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
|
|
702
696
|
return matches[0]?.profile ?? null;
|
|
703
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
|
+
|
|
704
727
|
// src/db/machines.ts
|
|
705
728
|
import { arch, hostname, type } from "os";
|
|
706
729
|
function currentHostname2() {
|
|
@@ -739,358 +762,319 @@ function listMachines(db) {
|
|
|
739
762
|
const d = db || getDatabase();
|
|
740
763
|
return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
|
|
741
764
|
}
|
|
742
|
-
// src/db/pg-migrations.ts
|
|
743
|
-
var PG_MIGRATIONS = [
|
|
744
|
-
`CREATE TABLE IF NOT EXISTS configs (
|
|
745
|
-
id TEXT PRIMARY KEY,
|
|
746
|
-
name TEXT NOT NULL,
|
|
747
|
-
slug TEXT NOT NULL UNIQUE,
|
|
748
|
-
kind TEXT NOT NULL DEFAULT 'file',
|
|
749
|
-
category TEXT NOT NULL,
|
|
750
|
-
agent TEXT NOT NULL DEFAULT 'global',
|
|
751
|
-
target_path TEXT,
|
|
752
|
-
outputs TEXT NOT NULL DEFAULT '[]',
|
|
753
|
-
format TEXT NOT NULL DEFAULT 'text',
|
|
754
|
-
content TEXT NOT NULL DEFAULT '',
|
|
755
|
-
description TEXT,
|
|
756
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
757
|
-
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
758
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
759
|
-
created_at TEXT NOT NULL,
|
|
760
|
-
updated_at TEXT NOT NULL,
|
|
761
|
-
synced_at TEXT
|
|
762
|
-
)`,
|
|
763
|
-
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
764
|
-
id TEXT PRIMARY KEY,
|
|
765
|
-
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
766
|
-
content TEXT NOT NULL,
|
|
767
|
-
version INTEGER NOT NULL,
|
|
768
|
-
created_at TEXT NOT NULL
|
|
769
|
-
)`,
|
|
770
|
-
`CREATE TABLE IF NOT EXISTS profiles (
|
|
771
|
-
id TEXT PRIMARY KEY,
|
|
772
|
-
name TEXT NOT NULL,
|
|
773
|
-
slug TEXT NOT NULL UNIQUE,
|
|
774
|
-
description TEXT,
|
|
775
|
-
selectors TEXT NOT NULL DEFAULT '{}',
|
|
776
|
-
variables TEXT NOT NULL DEFAULT '{}',
|
|
777
|
-
created_at TEXT NOT NULL,
|
|
778
|
-
updated_at TEXT NOT NULL
|
|
779
|
-
)`,
|
|
780
|
-
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
781
|
-
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
782
|
-
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
783
|
-
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
784
|
-
PRIMARY KEY (profile_id, config_id)
|
|
785
|
-
)`,
|
|
786
|
-
`CREATE TABLE IF NOT EXISTS machines (
|
|
787
|
-
id TEXT PRIMARY KEY,
|
|
788
|
-
hostname TEXT NOT NULL UNIQUE,
|
|
789
|
-
os TEXT,
|
|
790
|
-
arch TEXT,
|
|
791
|
-
last_applied_at TEXT,
|
|
792
|
-
created_at TEXT NOT NULL
|
|
793
|
-
)`,
|
|
794
|
-
`CREATE TABLE IF NOT EXISTS feedback (
|
|
795
|
-
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
796
|
-
message TEXT NOT NULL,
|
|
797
|
-
email TEXT,
|
|
798
|
-
category TEXT DEFAULT 'general',
|
|
799
|
-
version TEXT,
|
|
800
|
-
machine_id TEXT,
|
|
801
|
-
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
802
|
-
)`,
|
|
803
|
-
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
804
|
-
];
|
|
805
765
|
|
|
806
|
-
// src/
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
return flat.map((value) => value === undefined ? null : value);
|
|
816
|
-
}
|
|
817
|
-
function normalizeHost(hostname2) {
|
|
818
|
-
const stripped = hostname2.replace(/^\[/, "").replace(/\]$/, "");
|
|
819
|
-
try {
|
|
820
|
-
return decodeURIComponent(stripped).toLowerCase();
|
|
821
|
-
} catch {
|
|
822
|
-
return stripped.toLowerCase();
|
|
766
|
+
// src/data/config-store.ts
|
|
767
|
+
class CloudHttpError extends Error {
|
|
768
|
+
status;
|
|
769
|
+
body;
|
|
770
|
+
constructor(status, message, body) {
|
|
771
|
+
super(message);
|
|
772
|
+
this.status = status;
|
|
773
|
+
this.body = body;
|
|
774
|
+
this.name = "CloudHttpError";
|
|
823
775
|
}
|
|
824
776
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
777
|
+
var API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL";
|
|
778
|
+
var API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
|
|
779
|
+
function resolveCloudConfig(env = process.env) {
|
|
780
|
+
const apiUrl = env[API_URL_ENV]?.trim();
|
|
781
|
+
const apiKey = env[API_KEY_ENV]?.trim();
|
|
782
|
+
if (!apiUrl && !apiKey)
|
|
783
|
+
return null;
|
|
784
|
+
if (!apiUrl || !apiKey) {
|
|
785
|
+
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.`);
|
|
786
|
+
}
|
|
787
|
+
return { apiUrl, apiKey };
|
|
833
788
|
}
|
|
834
|
-
function
|
|
835
|
-
|
|
836
|
-
try {
|
|
837
|
-
url = new URL(connectionString);
|
|
838
|
-
} catch {
|
|
839
|
-
throw new Error("Invalid PostgreSQL connection string");
|
|
840
|
-
}
|
|
841
|
-
const sslMode = url.searchParams.get("sslmode")?.trim().toLowerCase();
|
|
842
|
-
const sslValue = url.searchParams.get("ssl")?.trim().toLowerCase();
|
|
843
|
-
const isLocal = isLocalPostgresHost(effectivePgHost(url));
|
|
844
|
-
const hasDisabledSsl = sslMode === DISABLED_SSL_MODE || sslValue === "false";
|
|
845
|
-
if (!isLocal && hasDisabledSsl) {
|
|
846
|
-
throw new Error("Refusing remote PostgreSQL connection with TLS disabled");
|
|
847
|
-
}
|
|
848
|
-
const shouldUseSsl = !isLocal || sslMode === "require" || sslMode === "verify-ca" || sslMode === "verify-full" || sslValue === "true";
|
|
849
|
-
url.searchParams.delete("sslmode");
|
|
850
|
-
url.searchParams.delete("ssl");
|
|
851
|
-
return {
|
|
852
|
-
connectionString: url.toString(),
|
|
853
|
-
ssl: shouldUseSsl ? { rejectUnauthorized: true } : undefined
|
|
854
|
-
};
|
|
789
|
+
function isCloudMode(env = process.env) {
|
|
790
|
+
return resolveCloudConfig(env) !== null;
|
|
855
791
|
}
|
|
856
792
|
|
|
857
|
-
class
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
793
|
+
class LocalConfigStore {
|
|
794
|
+
db;
|
|
795
|
+
mode = "local";
|
|
796
|
+
constructor(db) {
|
|
797
|
+
this.db = db;
|
|
798
|
+
}
|
|
799
|
+
async listConfigs(filter) {
|
|
800
|
+
return listConfigs(filter, this.db);
|
|
801
|
+
}
|
|
802
|
+
async getConfig(idOrSlug) {
|
|
803
|
+
return getConfig(idOrSlug, this.db);
|
|
804
|
+
}
|
|
805
|
+
async getConfigById(id) {
|
|
806
|
+
return getConfigById(id, this.db);
|
|
807
|
+
}
|
|
808
|
+
async createConfig(input) {
|
|
809
|
+
return createConfig(input, this.db);
|
|
810
|
+
}
|
|
811
|
+
async updateConfig(idOrSlug, input) {
|
|
812
|
+
return updateConfig(idOrSlug, input, this.db);
|
|
813
|
+
}
|
|
814
|
+
async deleteConfig(idOrSlug) {
|
|
815
|
+
deleteConfig(idOrSlug, this.db);
|
|
816
|
+
}
|
|
817
|
+
async getConfigStats() {
|
|
818
|
+
return getConfigStats(this.db);
|
|
861
819
|
}
|
|
862
|
-
async
|
|
863
|
-
|
|
864
|
-
return { changes: result.rowCount ?? 0 };
|
|
820
|
+
async listSnapshots(configId) {
|
|
821
|
+
return listSnapshots(configId, this.db);
|
|
865
822
|
}
|
|
866
|
-
async
|
|
867
|
-
|
|
868
|
-
return result.rows;
|
|
823
|
+
async getSnapshot(id) {
|
|
824
|
+
return getSnapshot(id, this.db);
|
|
869
825
|
}
|
|
870
|
-
async
|
|
871
|
-
|
|
826
|
+
async getSnapshotByVersion(configId, version) {
|
|
827
|
+
return getSnapshotByVersion(configId, version, this.db);
|
|
828
|
+
}
|
|
829
|
+
async createSnapshot(configId, content, version) {
|
|
830
|
+
return createSnapshot(configId, content, version, this.db);
|
|
831
|
+
}
|
|
832
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
833
|
+
return pruneSnapshots(configId, keep, this.db);
|
|
834
|
+
}
|
|
835
|
+
async listProfiles() {
|
|
836
|
+
return listProfiles(this.db);
|
|
837
|
+
}
|
|
838
|
+
async getProfile(idOrSlug) {
|
|
839
|
+
return getProfile(idOrSlug, this.db);
|
|
840
|
+
}
|
|
841
|
+
async getProfileConfigs(idOrSlug) {
|
|
842
|
+
return getProfileConfigs(idOrSlug, this.db);
|
|
843
|
+
}
|
|
844
|
+
async createProfile(input) {
|
|
845
|
+
return createProfile(input, this.db);
|
|
846
|
+
}
|
|
847
|
+
async updateProfile(idOrSlug, input) {
|
|
848
|
+
return updateProfile(idOrSlug, input, this.db);
|
|
849
|
+
}
|
|
850
|
+
async deleteProfile(idOrSlug) {
|
|
851
|
+
deleteProfile(idOrSlug, this.db);
|
|
852
|
+
}
|
|
853
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
854
|
+
addConfigToProfile(profileIdOrSlug, configId, this.db);
|
|
855
|
+
}
|
|
856
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
857
|
+
removeConfigFromProfile(profileIdOrSlug, configId, this.db);
|
|
858
|
+
}
|
|
859
|
+
async resolveProfileForMachine(machine) {
|
|
860
|
+
return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
|
|
861
|
+
}
|
|
862
|
+
async registerMachine(hostname2, os, arch2) {
|
|
863
|
+
return registerMachine(hostname2, os, arch2, this.db);
|
|
864
|
+
}
|
|
865
|
+
async updateMachineApplied(hostname2) {
|
|
866
|
+
updateMachineApplied(hostname2, this.db);
|
|
867
|
+
}
|
|
868
|
+
async listMachines() {
|
|
869
|
+
return listMachines(this.db);
|
|
870
|
+
}
|
|
871
|
+
async sendFeedback(input) {
|
|
872
|
+
insertFeedback(input, this.db);
|
|
873
|
+
}
|
|
874
|
+
async reset() {
|
|
875
|
+
resetLocalDatabase();
|
|
872
876
|
}
|
|
873
877
|
}
|
|
874
878
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
879
|
+
class CloudConfigStore {
|
|
880
|
+
mode = "api";
|
|
881
|
+
base;
|
|
882
|
+
apiKey;
|
|
883
|
+
timeoutMs;
|
|
884
|
+
constructor(config) {
|
|
885
|
+
this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
|
|
886
|
+
this.apiKey = config.apiKey;
|
|
887
|
+
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
888
|
+
}
|
|
889
|
+
async request(method, path, body, opts = {}) {
|
|
890
|
+
const controller = new AbortController;
|
|
891
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
892
|
+
const headers = {
|
|
893
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
894
|
+
Accept: "application/json"
|
|
895
|
+
};
|
|
896
|
+
if (body !== undefined)
|
|
897
|
+
headers["Content-Type"] = "application/json";
|
|
898
|
+
if (opts.idempotent)
|
|
899
|
+
headers["Idempotency-Key"] = randomUUID2();
|
|
900
|
+
try {
|
|
901
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
902
|
+
method,
|
|
903
|
+
headers,
|
|
904
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
905
|
+
signal: controller.signal
|
|
906
|
+
});
|
|
907
|
+
if (res.status === 404 && opts.allow404)
|
|
908
|
+
return { status: 404, data: null };
|
|
909
|
+
const text = await res.text();
|
|
910
|
+
let parsed = null;
|
|
911
|
+
if (text) {
|
|
912
|
+
try {
|
|
913
|
+
parsed = JSON.parse(text);
|
|
914
|
+
} catch {
|
|
915
|
+
parsed = text;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
if (!res.ok) {
|
|
919
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
920
|
+
throw new CloudHttpError(res.status, message, parsed);
|
|
921
|
+
}
|
|
922
|
+
return { status: res.status, data: parsed };
|
|
923
|
+
} finally {
|
|
924
|
+
clearTimeout(timer);
|
|
925
|
+
}
|
|
897
926
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (
|
|
912
|
-
|
|
927
|
+
async listConfigs(filter = {}) {
|
|
928
|
+
const params = new URLSearchParams;
|
|
929
|
+
if (filter.category)
|
|
930
|
+
params.set("category", filter.category);
|
|
931
|
+
if (filter.agent)
|
|
932
|
+
params.set("agent", filter.agent);
|
|
933
|
+
if (filter.kind)
|
|
934
|
+
params.set("kind", filter.kind);
|
|
935
|
+
if (filter.search)
|
|
936
|
+
params.set("search", filter.search);
|
|
937
|
+
const qs = params.toString();
|
|
938
|
+
const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
|
|
939
|
+
let configs = data?.configs ?? [];
|
|
940
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
941
|
+
configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
|
|
942
|
+
}
|
|
943
|
+
if (filter.is_template !== undefined) {
|
|
944
|
+
configs = configs.filter((c) => c.is_template === filter.is_template);
|
|
945
|
+
}
|
|
946
|
+
return configs;
|
|
913
947
|
}
|
|
914
|
-
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
return mode;
|
|
920
|
-
return getStorageDatabaseUrl() ? "hybrid" : "local";
|
|
921
|
-
}
|
|
922
|
-
async function getStoragePg() {
|
|
923
|
-
const url = getStorageDatabaseUrl();
|
|
924
|
-
if (!url)
|
|
925
|
-
throw new Error("Missing HASNA_CONFIGS_DATABASE_URL or CONFIGS_DATABASE_URL");
|
|
926
|
-
return new PgAdapterAsync(url);
|
|
927
|
-
}
|
|
928
|
-
async function runStorageMigrations(remote) {
|
|
929
|
-
await remote.run("CREATE EXTENSION IF NOT EXISTS pgcrypto");
|
|
930
|
-
for (const sql of PG_MIGRATIONS)
|
|
931
|
-
await remote.run(sql);
|
|
932
|
-
}
|
|
933
|
-
async function storagePush(options) {
|
|
934
|
-
const remote = await getStoragePg();
|
|
935
|
-
const db = getDatabase();
|
|
936
|
-
try {
|
|
937
|
-
await runStorageMigrations(remote);
|
|
938
|
-
const results = [];
|
|
939
|
-
for (const table of resolveTables(options?.tables))
|
|
940
|
-
results.push(await pushTable(db, remote, table));
|
|
941
|
-
recordSyncMeta(db, "push", results);
|
|
942
|
-
return results;
|
|
943
|
-
} finally {
|
|
944
|
-
await remote.close();
|
|
948
|
+
async getConfig(idOrSlug) {
|
|
949
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
950
|
+
if (status === 404 || !data?.config)
|
|
951
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
952
|
+
return data.config;
|
|
945
953
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const remote = await getStoragePg();
|
|
949
|
-
const db = getDatabase();
|
|
950
|
-
try {
|
|
951
|
-
await runStorageMigrations(remote);
|
|
952
|
-
const results = [];
|
|
953
|
-
for (const table of resolveTables(options?.tables))
|
|
954
|
-
results.push(await pullTable(remote, db, table));
|
|
955
|
-
recordSyncMeta(db, "pull", results);
|
|
956
|
-
return results;
|
|
957
|
-
} finally {
|
|
958
|
-
await remote.close();
|
|
954
|
+
async getConfigById(id) {
|
|
955
|
+
return this.getConfig(id);
|
|
959
956
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
}
|
|
966
|
-
function getStorageSyncMetaAll() {
|
|
967
|
-
const db = getDatabase();
|
|
968
|
-
ensureSyncMetaTable(db);
|
|
969
|
-
return db.query("SELECT table_name, last_synced_at, direction FROM _configs_sync_meta ORDER BY table_name, direction").all();
|
|
970
|
-
}
|
|
971
|
-
function getSyncMetaAll() {
|
|
972
|
-
return getStorageSyncMetaAll();
|
|
973
|
-
}
|
|
974
|
-
function getStorageStatus() {
|
|
975
|
-
return {
|
|
976
|
-
configured: Boolean(getStorageDatabaseUrl()),
|
|
977
|
-
mode: getStorageMode(),
|
|
978
|
-
env: STORAGE_DATABASE_ENV,
|
|
979
|
-
service: "configs",
|
|
980
|
-
tables: STORAGE_TABLES,
|
|
981
|
-
sync: getStorageSyncMetaAll()
|
|
982
|
-
};
|
|
983
|
-
}
|
|
984
|
-
function resolveTables(tables) {
|
|
985
|
-
if (!tables || tables.length === 0)
|
|
986
|
-
return [...STORAGE_TABLES];
|
|
987
|
-
const allowed = new Set(STORAGE_TABLES);
|
|
988
|
-
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
989
|
-
const invalid = requested.filter((table) => !allowed.has(table));
|
|
990
|
-
if (invalid.length > 0)
|
|
991
|
-
throw new Error(`Unknown configs sync table(s): ${invalid.join(", ")}`);
|
|
992
|
-
return requested;
|
|
993
|
-
}
|
|
994
|
-
async function pushTable(db, remote, table) {
|
|
995
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
996
|
-
try {
|
|
997
|
-
const rows = db.query(`SELECT * FROM ${quoteIdent(table)}`).all();
|
|
998
|
-
result.rowsRead = rows.length;
|
|
999
|
-
if (rows.length === 0)
|
|
1000
|
-
return result;
|
|
1001
|
-
const columns = await filterRemoteColumns(remote, table, Object.keys(rows[0]));
|
|
1002
|
-
result.rowsWritten = await upsertPg(remote, table, columns, rows);
|
|
1003
|
-
} catch (error) {
|
|
1004
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
957
|
+
async createConfig(input) {
|
|
958
|
+
const { data } = await this.request("POST", "/configs", input, {
|
|
959
|
+
idempotent: true
|
|
960
|
+
});
|
|
961
|
+
return data.config;
|
|
1005
962
|
}
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
const result = { table, rowsRead: 0, rowsWritten: 0, errors: [] };
|
|
1010
|
-
try {
|
|
1011
|
-
const rows = await remote.all(`SELECT * FROM ${quoteIdent(table)}`);
|
|
1012
|
-
result.rowsRead = rows.length;
|
|
1013
|
-
if (rows.length === 0)
|
|
1014
|
-
return result;
|
|
1015
|
-
const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
|
|
1016
|
-
result.rowsWritten = upsertSqlite(db, table, columns, rows);
|
|
1017
|
-
} catch (error) {
|
|
1018
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
963
|
+
async updateConfig(idOrSlug, input) {
|
|
964
|
+
const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
|
|
965
|
+
return data.config;
|
|
1019
966
|
}
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
return 0;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
967
|
+
async deleteConfig(idOrSlug) {
|
|
968
|
+
const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
969
|
+
if (status === 404)
|
|
970
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
971
|
+
}
|
|
972
|
+
async getConfigStats() {
|
|
973
|
+
const { data } = await this.request("GET", "/stats");
|
|
974
|
+
return data ?? { total: 0 };
|
|
975
|
+
}
|
|
976
|
+
async listSnapshots(configId) {
|
|
977
|
+
const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
|
|
978
|
+
return data?.snapshots ?? [];
|
|
979
|
+
}
|
|
980
|
+
async getSnapshot(id) {
|
|
981
|
+
const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
982
|
+
if (status === 404 || !data?.snapshot)
|
|
983
|
+
return null;
|
|
984
|
+
return data.snapshot;
|
|
985
|
+
}
|
|
986
|
+
async getSnapshotByVersion(configId, version) {
|
|
987
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
|
|
988
|
+
if (status === 404 || !data?.snapshot)
|
|
989
|
+
return null;
|
|
990
|
+
return data.snapshot;
|
|
991
|
+
}
|
|
992
|
+
async createSnapshot(configId, content, version) {
|
|
993
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
|
|
994
|
+
return data.snapshot;
|
|
995
|
+
}
|
|
996
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
997
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
|
|
998
|
+
return data?.pruned ?? 0;
|
|
999
|
+
}
|
|
1000
|
+
async listProfiles() {
|
|
1001
|
+
const { data } = await this.request("GET", "/profiles");
|
|
1002
|
+
return data?.profiles ?? [];
|
|
1003
|
+
}
|
|
1004
|
+
async getProfile(idOrSlug) {
|
|
1005
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1006
|
+
if (status === 404 || !data?.profile)
|
|
1007
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1008
|
+
const { configs: _configs, ...profile } = data.profile;
|
|
1009
|
+
return profile;
|
|
1010
|
+
}
|
|
1011
|
+
async getProfileConfigs(idOrSlug) {
|
|
1012
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1013
|
+
if (status === 404 || !data?.profile)
|
|
1014
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1015
|
+
return data.profile.configs ?? [];
|
|
1016
|
+
}
|
|
1017
|
+
async createProfile(input) {
|
|
1018
|
+
const { data } = await this.request("POST", "/profiles", input, {
|
|
1019
|
+
idempotent: true
|
|
1020
|
+
});
|
|
1021
|
+
return data.profile;
|
|
1022
|
+
}
|
|
1023
|
+
async updateProfile(idOrSlug, input) {
|
|
1024
|
+
const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
|
|
1025
|
+
return data.profile;
|
|
1026
|
+
}
|
|
1027
|
+
async deleteProfile(idOrSlug) {
|
|
1028
|
+
const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1029
|
+
if (status === 404)
|
|
1030
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1031
|
+
}
|
|
1032
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
1033
|
+
await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
|
|
1034
|
+
}
|
|
1035
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
1036
|
+
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
1037
|
+
}
|
|
1038
|
+
async resolveProfileForMachine(machine) {
|
|
1039
|
+
const params = new URLSearchParams;
|
|
1040
|
+
if (machine?.hostname)
|
|
1041
|
+
params.set("hostname", machine.hostname);
|
|
1042
|
+
if (machine?.os)
|
|
1043
|
+
params.set("os", machine.os);
|
|
1044
|
+
if (machine?.arch)
|
|
1045
|
+
params.set("arch", machine.arch);
|
|
1046
|
+
const qs = params.toString();
|
|
1047
|
+
const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
|
|
1048
|
+
if (status === 404 || !data?.profile)
|
|
1049
|
+
return null;
|
|
1050
|
+
return data.profile;
|
|
1051
|
+
}
|
|
1052
|
+
async registerMachine(hostname2, os, arch2) {
|
|
1053
|
+
const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
|
|
1054
|
+
return data.machine;
|
|
1055
|
+
}
|
|
1056
|
+
async updateMachineApplied(hostname2) {
|
|
1057
|
+
await this.request("POST", "/machines/applied", { hostname: hostname2 });
|
|
1058
|
+
}
|
|
1059
|
+
async listMachines() {
|
|
1060
|
+
const { data } = await this.request("GET", "/machines");
|
|
1061
|
+
return data?.machines ?? [];
|
|
1062
|
+
}
|
|
1063
|
+
async sendFeedback(input) {
|
|
1064
|
+
await this.request("POST", "/feedback", {
|
|
1065
|
+
message: input.message,
|
|
1066
|
+
email: input.email ?? undefined,
|
|
1067
|
+
category: input.category ?? undefined,
|
|
1068
|
+
version: input.version ?? undefined
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
async reset() {
|
|
1072
|
+
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.");
|
|
1074
1073
|
}
|
|
1075
1074
|
}
|
|
1076
|
-
function
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
function quoteIdent(identifier) {
|
|
1080
|
-
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1081
|
-
}
|
|
1082
|
-
function coerceForSqlite(value) {
|
|
1083
|
-
if (value === undefined || value === null)
|
|
1084
|
-
return null;
|
|
1085
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
1086
|
-
return value;
|
|
1087
|
-
if (value instanceof Date)
|
|
1088
|
-
return value.toISOString();
|
|
1089
|
-
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
1090
|
-
return value;
|
|
1091
|
-
if (typeof value === "object")
|
|
1092
|
-
return JSON.stringify(value);
|
|
1093
|
-
return String(value);
|
|
1075
|
+
function resolveConfigStore(env = process.env) {
|
|
1076
|
+
const cloud = resolveCloudConfig(env);
|
|
1077
|
+
return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
|
|
1094
1078
|
}
|
|
1095
1079
|
// src/status.ts
|
|
1096
1080
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
@@ -1255,8 +1239,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
1255
1239
|
mkdirSync2(dir, { recursive: true });
|
|
1256
1240
|
}
|
|
1257
1241
|
if (previousContent !== null && changed) {
|
|
1258
|
-
const
|
|
1259
|
-
createSnapshot(config.id, previousContent, config.version
|
|
1242
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1243
|
+
await store.createSnapshot(config.id, previousContent, config.version);
|
|
1260
1244
|
}
|
|
1261
1245
|
writeFileSync(path, renderedContent, "utf-8");
|
|
1262
1246
|
}
|
|
@@ -1282,8 +1266,8 @@ async function applyConfig(config, opts = {}) {
|
|
|
1282
1266
|
if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
|
|
1283
1267
|
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
1284
1268
|
}
|
|
1285
|
-
const
|
|
1286
|
-
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(
|
|
1269
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1270
|
+
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
1287
1271
|
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
1288
1272
|
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
1289
1273
|
}
|
|
@@ -1304,7 +1288,7 @@ async function applyConfig(config, opts = {}) {
|
|
|
1304
1288
|
};
|
|
1305
1289
|
}
|
|
1306
1290
|
if (!opts.dryRun) {
|
|
1307
|
-
updateConfig(config.id, { synced_at:
|
|
1291
|
+
await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
|
|
1308
1292
|
}
|
|
1309
1293
|
return result;
|
|
1310
1294
|
}
|
|
@@ -1420,9 +1404,9 @@ function redactIni(content) {
|
|
|
1420
1404
|
for (let i = 0;i < lines.length; i++) {
|
|
1421
1405
|
const line = lines[i];
|
|
1422
1406
|
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
1423
|
-
if (authM && !authM[2].
|
|
1424
|
-
redacted.push({ varName: "
|
|
1425
|
-
out.push(`${authM[1]}{
|
|
1407
|
+
if (authM && !isReferenceValue(authM[2].trim())) {
|
|
1408
|
+
redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
1409
|
+
out.push(`${authM[1]}\${NPM_TOKEN}`);
|
|
1426
1410
|
continue;
|
|
1427
1411
|
}
|
|
1428
1412
|
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
@@ -1462,6 +1446,8 @@ function redactGeneric(content) {
|
|
|
1462
1446
|
function shouldRedactKeyValue(key, value) {
|
|
1463
1447
|
if (!value || value.startsWith("{{"))
|
|
1464
1448
|
return false;
|
|
1449
|
+
if (isReferenceValue(value.trim()))
|
|
1450
|
+
return false;
|
|
1465
1451
|
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
1466
1452
|
return false;
|
|
1467
1453
|
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
@@ -1483,6 +1469,9 @@ function reasonFor(key, value) {
|
|
|
1483
1469
|
}
|
|
1484
1470
|
return "secret value pattern";
|
|
1485
1471
|
}
|
|
1472
|
+
function isReferenceValue(value) {
|
|
1473
|
+
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);
|
|
1474
|
+
}
|
|
1486
1475
|
function redactContent(content, format) {
|
|
1487
1476
|
switch (format) {
|
|
1488
1477
|
case "shell":
|
|
@@ -1529,21 +1518,13 @@ function countBy(items, getValue) {
|
|
|
1529
1518
|
}
|
|
1530
1519
|
return counts;
|
|
1531
1520
|
}
|
|
1532
|
-
function
|
|
1533
|
-
try {
|
|
1534
|
-
const row = db.query(`SELECT COUNT(*) AS count FROM ${table}`).get();
|
|
1535
|
-
return Number(row?.count ?? 0);
|
|
1536
|
-
} catch {
|
|
1537
|
-
return 0;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
function getConfigsStatus(db = getDatabase()) {
|
|
1521
|
+
async function getConfigsStatus(store = resolveConfigStore()) {
|
|
1541
1522
|
let databaseReachable = true;
|
|
1542
1523
|
let configs = [];
|
|
1543
1524
|
let categoryStats = { total: 0 };
|
|
1544
1525
|
try {
|
|
1545
|
-
configs = listConfigs(
|
|
1546
|
-
categoryStats = getConfigStats(
|
|
1526
|
+
configs = await store.listConfigs();
|
|
1527
|
+
categoryStats = await store.getConfigStats();
|
|
1547
1528
|
} catch {
|
|
1548
1529
|
databaseReachable = false;
|
|
1549
1530
|
}
|
|
@@ -1568,10 +1549,25 @@ function getConfigsStatus(db = getDatabase()) {
|
|
|
1568
1549
|
driftedTargets += 1;
|
|
1569
1550
|
}
|
|
1570
1551
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1552
|
+
let profiles = 0;
|
|
1553
|
+
let machines = 0;
|
|
1554
|
+
let profileLinks = 0;
|
|
1555
|
+
let snapshots = 0;
|
|
1556
|
+
if (databaseReachable) {
|
|
1557
|
+
try {
|
|
1558
|
+
const profileList = await store.listProfiles();
|
|
1559
|
+
profiles = profileList.length;
|
|
1560
|
+
machines = (await store.listMachines()).length;
|
|
1561
|
+
for (const profile of profileList) {
|
|
1562
|
+
profileLinks += (await store.getProfileConfigs(profile.id)).length;
|
|
1563
|
+
}
|
|
1564
|
+
for (const config of configs) {
|
|
1565
|
+
snapshots += (await store.listSnapshots(config.id)).length;
|
|
1566
|
+
}
|
|
1567
|
+
} catch {
|
|
1568
|
+
databaseReachable = false;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1575
1571
|
const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
|
|
1576
1572
|
const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
|
|
1577
1573
|
return {
|
|
@@ -1624,6 +1620,69 @@ function getConfigsStatus(db = getDatabase()) {
|
|
|
1624
1620
|
}
|
|
1625
1621
|
};
|
|
1626
1622
|
}
|
|
1623
|
+
// src/db/pg-migrations.ts
|
|
1624
|
+
var PG_MIGRATIONS = [
|
|
1625
|
+
`CREATE TABLE IF NOT EXISTS configs (
|
|
1626
|
+
id TEXT PRIMARY KEY,
|
|
1627
|
+
name TEXT NOT NULL,
|
|
1628
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1629
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
1630
|
+
category TEXT NOT NULL,
|
|
1631
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
1632
|
+
target_path TEXT,
|
|
1633
|
+
outputs TEXT NOT NULL DEFAULT '[]',
|
|
1634
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
1635
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1636
|
+
description TEXT,
|
|
1637
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
1638
|
+
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
1639
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1640
|
+
created_at TEXT NOT NULL,
|
|
1641
|
+
updated_at TEXT NOT NULL,
|
|
1642
|
+
synced_at TEXT
|
|
1643
|
+
)`,
|
|
1644
|
+
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
1645
|
+
id TEXT PRIMARY KEY,
|
|
1646
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1647
|
+
content TEXT NOT NULL,
|
|
1648
|
+
version INTEGER NOT NULL,
|
|
1649
|
+
created_at TEXT NOT NULL
|
|
1650
|
+
)`,
|
|
1651
|
+
`CREATE TABLE IF NOT EXISTS profiles (
|
|
1652
|
+
id TEXT PRIMARY KEY,
|
|
1653
|
+
name TEXT NOT NULL,
|
|
1654
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1655
|
+
description TEXT,
|
|
1656
|
+
selectors TEXT NOT NULL DEFAULT '{}',
|
|
1657
|
+
variables TEXT NOT NULL DEFAULT '{}',
|
|
1658
|
+
created_at TEXT NOT NULL,
|
|
1659
|
+
updated_at TEXT NOT NULL
|
|
1660
|
+
)`,
|
|
1661
|
+
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
1662
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
1663
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1664
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
1665
|
+
PRIMARY KEY (profile_id, config_id)
|
|
1666
|
+
)`,
|
|
1667
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
1668
|
+
id TEXT PRIMARY KEY,
|
|
1669
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
1670
|
+
os TEXT,
|
|
1671
|
+
arch TEXT,
|
|
1672
|
+
last_applied_at TEXT,
|
|
1673
|
+
created_at TEXT NOT NULL
|
|
1674
|
+
)`,
|
|
1675
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1676
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1677
|
+
message TEXT NOT NULL,
|
|
1678
|
+
email TEXT,
|
|
1679
|
+
category TEXT DEFAULT 'general',
|
|
1680
|
+
version TEXT,
|
|
1681
|
+
machine_id TEXT,
|
|
1682
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1683
|
+
)`,
|
|
1684
|
+
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
1685
|
+
];
|
|
1627
1686
|
// src/lib/session-render.ts
|
|
1628
1687
|
import { createHash } from "crypto";
|
|
1629
1688
|
import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
|
|
@@ -2501,14 +2560,14 @@ function asStringArray(value) {
|
|
|
2501
2560
|
return value.filter((item) => typeof item === "string");
|
|
2502
2561
|
}
|
|
2503
2562
|
// src/lib/session-apply.ts
|
|
2504
|
-
import { createHash as createHash2, randomUUID as
|
|
2563
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
2505
2564
|
import {
|
|
2506
2565
|
existsSync as existsSync6,
|
|
2507
2566
|
lstatSync,
|
|
2508
2567
|
mkdirSync as mkdirSync3,
|
|
2509
2568
|
readFileSync as readFileSync4,
|
|
2510
2569
|
renameSync,
|
|
2511
|
-
rmSync,
|
|
2570
|
+
rmSync as rmSync2,
|
|
2512
2571
|
writeFileSync as writeFileSync2
|
|
2513
2572
|
} from "fs";
|
|
2514
2573
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
|
|
@@ -2562,7 +2621,7 @@ function applySessionRender(plan, options = {}) {
|
|
|
2562
2621
|
continue;
|
|
2563
2622
|
assertNoSymlinkSegments(targetHome, result.path);
|
|
2564
2623
|
if (existsSync6(result.path))
|
|
2565
|
-
|
|
2624
|
+
rmSync2(result.path);
|
|
2566
2625
|
}
|
|
2567
2626
|
}
|
|
2568
2627
|
return {
|
|
@@ -2796,7 +2855,7 @@ function writePlannedFile(path, content, targetHome) {
|
|
|
2796
2855
|
const dir = dirname3(path);
|
|
2797
2856
|
mkdirSync3(dir, { recursive: true });
|
|
2798
2857
|
assertNoSymlinkSegments(targetHome, path);
|
|
2799
|
-
const tmp = join4(dir, `.session-${
|
|
2858
|
+
const tmp = join4(dir, `.session-${randomUUID3()}.tmp`);
|
|
2800
2859
|
writeFileSync2(tmp, content, "utf-8");
|
|
2801
2860
|
renameSync(tmp, path);
|
|
2802
2861
|
}
|
|
@@ -2814,7 +2873,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
2814
2873
|
if (!previousManifest && existingFiles.length === 0)
|
|
2815
2874
|
return null;
|
|
2816
2875
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2817
|
-
const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${
|
|
2876
|
+
const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID3()}.json`);
|
|
2818
2877
|
const snapshot = {
|
|
2819
2878
|
schema: "hasna.configs.session-render-snapshot/v1",
|
|
2820
2879
|
createdAt: new Date().toISOString(),
|
|
@@ -2948,7 +3007,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
|
|
|
2948
3007
|
ids, passport numbers, credentials, and contract clauses unless an explicit
|
|
2949
3008
|
approved storage policy exists.
|
|
2950
3009
|
`;
|
|
2951
|
-
function ensureProjectDashboardStandardConfig(
|
|
3010
|
+
async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
|
|
2952
3011
|
const input = {
|
|
2953
3012
|
name: "Agent Managed Project Dashboard Standard",
|
|
2954
3013
|
category: "workspace",
|
|
@@ -2960,17 +3019,21 @@ function ensureProjectDashboardStandardConfig(db) {
|
|
|
2960
3019
|
tags: ["projects-dashboard", "agent-projects", "json-render"]
|
|
2961
3020
|
};
|
|
2962
3021
|
try {
|
|
2963
|
-
const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG
|
|
3022
|
+
const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
|
|
2964
3023
|
if (existing.content !== input.content || existing.description !== input.description || existing.category !== input.category || existing.agent !== input.agent || existing.format !== input.format || existing.kind !== input.kind) {
|
|
2965
|
-
return updateConfig(existing.id, input
|
|
3024
|
+
return await store.updateConfig(existing.id, input);
|
|
2966
3025
|
}
|
|
2967
3026
|
return existing;
|
|
2968
3027
|
} catch {
|
|
2969
|
-
return createConfig(input
|
|
3028
|
+
return await store.createConfig(input);
|
|
2970
3029
|
}
|
|
2971
3030
|
}
|
|
2972
3031
|
|
|
2973
3032
|
// src/lib/platform-profiles.ts
|
|
3033
|
+
function profileHasSelectors2(profile) {
|
|
3034
|
+
const selectors = profile.selectors ?? {};
|
|
3035
|
+
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
3036
|
+
}
|
|
2974
3037
|
var PLATFORM_PROFILE_PRESETS = [
|
|
2975
3038
|
{
|
|
2976
3039
|
name: "linux-arm64",
|
|
@@ -2997,25 +3060,25 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
2997
3060
|
}
|
|
2998
3061
|
}
|
|
2999
3062
|
];
|
|
3000
|
-
function ensurePlatformProfiles(
|
|
3001
|
-
const configs = listConfigs(
|
|
3063
|
+
async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
3064
|
+
const configs = await store.listConfigs();
|
|
3002
3065
|
const ensured = [];
|
|
3003
3066
|
for (const preset of PLATFORM_PROFILE_PRESETS) {
|
|
3004
3067
|
let profile;
|
|
3005
3068
|
try {
|
|
3006
|
-
profile = getProfile(preset.name
|
|
3007
|
-
if (!
|
|
3008
|
-
profile = updateProfile(profile.id, {
|
|
3069
|
+
profile = await store.getProfile(preset.name);
|
|
3070
|
+
if (!profileHasSelectors2(profile) || Object.keys(profile.variables).length === 0) {
|
|
3071
|
+
profile = await store.updateProfile(profile.id, {
|
|
3009
3072
|
description: profile.description ?? preset.description,
|
|
3010
|
-
selectors:
|
|
3073
|
+
selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
|
|
3011
3074
|
variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
|
|
3012
|
-
}
|
|
3075
|
+
});
|
|
3013
3076
|
}
|
|
3014
3077
|
} catch {
|
|
3015
|
-
profile = createProfile(preset
|
|
3078
|
+
profile = await store.createProfile(preset);
|
|
3016
3079
|
}
|
|
3017
3080
|
for (const config of configs) {
|
|
3018
|
-
addConfigToProfile(profile.id, config.id
|
|
3081
|
+
await store.addConfigToProfile(profile.id, config.id);
|
|
3019
3082
|
}
|
|
3020
3083
|
ensured.push(profile);
|
|
3021
3084
|
}
|
|
@@ -3034,14 +3097,14 @@ function shouldSkip(p) {
|
|
|
3034
3097
|
return SKIP.some((s) => p.includes(s));
|
|
3035
3098
|
}
|
|
3036
3099
|
async function syncFromDir(dir, opts = {}) {
|
|
3037
|
-
const
|
|
3100
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3038
3101
|
const absDir = expandPath(dir);
|
|
3039
3102
|
if (!existsSync7(absDir))
|
|
3040
3103
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
3041
3104
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join5(absDir, f)).filter((f) => statSync3(f).isFile());
|
|
3042
3105
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3043
3106
|
const home = homedir4();
|
|
3044
|
-
const allConfigs = listConfigs(
|
|
3107
|
+
const allConfigs = await store.listConfigs();
|
|
3045
3108
|
for (const file of files) {
|
|
3046
3109
|
if (shouldSkip(file)) {
|
|
3047
3110
|
result.skipped.push(file);
|
|
@@ -3057,11 +3120,11 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
3057
3120
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
3058
3121
|
if (!existing) {
|
|
3059
3122
|
if (!opts.dryRun)
|
|
3060
|
-
createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }
|
|
3123
|
+
await store.createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
|
|
3061
3124
|
result.added++;
|
|
3062
3125
|
} else if (existing.content !== content) {
|
|
3063
3126
|
if (!opts.dryRun)
|
|
3064
|
-
updateConfig(existing.id, { content }
|
|
3127
|
+
await store.updateConfig(existing.id, { content });
|
|
3065
3128
|
result.updated++;
|
|
3066
3129
|
} else {
|
|
3067
3130
|
result.unchanged++;
|
|
@@ -3073,17 +3136,17 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
3073
3136
|
return result;
|
|
3074
3137
|
}
|
|
3075
3138
|
async function syncToDir(dir, opts = {}) {
|
|
3076
|
-
const
|
|
3139
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3077
3140
|
const home = homedir4();
|
|
3078
3141
|
const absDir = expandPath(dir);
|
|
3079
3142
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
3080
|
-
const configs = listConfigs(
|
|
3143
|
+
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
3081
3144
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3082
3145
|
for (const config of configs) {
|
|
3083
3146
|
if (config.kind === "reference")
|
|
3084
3147
|
continue;
|
|
3085
3148
|
try {
|
|
3086
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
3149
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store });
|
|
3087
3150
|
r.changed ? result.updated++ : result.unchanged++;
|
|
3088
3151
|
} catch {
|
|
3089
3152
|
result.skipped.push(config.target_path || config.id);
|
|
@@ -3201,11 +3264,11 @@ var PROJECT_CONFIG_FILES = [
|
|
|
3201
3264
|
{ file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" }
|
|
3202
3265
|
];
|
|
3203
3266
|
async function syncProject(opts) {
|
|
3204
|
-
const
|
|
3267
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3205
3268
|
const absDir = expandPath(opts.projectDir);
|
|
3206
3269
|
const projectName = absDir.split("/").pop() || "project";
|
|
3207
3270
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3208
|
-
const allConfigs = listConfigs(
|
|
3271
|
+
const allConfigs = await store.listConfigs();
|
|
3209
3272
|
const machine = detectMachineContext();
|
|
3210
3273
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
3211
3274
|
const abs = join6(absDir, pf.file);
|
|
@@ -3227,11 +3290,11 @@ async function syncProject(opts) {
|
|
|
3227
3290
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
|
|
3228
3291
|
if (!existing) {
|
|
3229
3292
|
if (!opts.dryRun)
|
|
3230
|
-
createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }
|
|
3293
|
+
await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
|
|
3231
3294
|
result.added++;
|
|
3232
3295
|
} else if (existing.content !== content) {
|
|
3233
3296
|
if (!opts.dryRun)
|
|
3234
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
3297
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
3235
3298
|
result.updated++;
|
|
3236
3299
|
} else {
|
|
3237
3300
|
result.unchanged++;
|
|
@@ -3256,11 +3319,11 @@ async function syncProject(opts) {
|
|
|
3256
3319
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
|
|
3257
3320
|
if (!existing) {
|
|
3258
3321
|
if (!opts.dryRun)
|
|
3259
|
-
createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }
|
|
3322
|
+
await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
|
|
3260
3323
|
result.added++;
|
|
3261
3324
|
} else if (existing.content !== content) {
|
|
3262
3325
|
if (!opts.dryRun)
|
|
3263
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
3326
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
3264
3327
|
result.updated++;
|
|
3265
3328
|
} else {
|
|
3266
3329
|
result.unchanged++;
|
|
@@ -3270,7 +3333,7 @@ async function syncProject(opts) {
|
|
|
3270
3333
|
return result;
|
|
3271
3334
|
}
|
|
3272
3335
|
async function syncKnown(opts = {}) {
|
|
3273
|
-
const
|
|
3336
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3274
3337
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3275
3338
|
const home = getConfigHome();
|
|
3276
3339
|
const machine = detectMachineContext();
|
|
@@ -3279,7 +3342,7 @@ async function syncKnown(opts = {}) {
|
|
|
3279
3342
|
targets = targets.filter((k) => k.agent === opts.agent);
|
|
3280
3343
|
if (opts.category)
|
|
3281
3344
|
targets = targets.filter((k) => k.category === opts.category);
|
|
3282
|
-
const allConfigs = listConfigs(
|
|
3345
|
+
const allConfigs = await store.listConfigs();
|
|
3283
3346
|
const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
|
|
3284
3347
|
for (const known of targets) {
|
|
3285
3348
|
if (known.rulesDir) {
|
|
@@ -3308,15 +3371,15 @@ async function syncKnown(opts = {}) {
|
|
|
3308
3371
|
const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
|
|
3309
3372
|
if (!existing) {
|
|
3310
3373
|
if (!opts.dryRun)
|
|
3311
|
-
createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }
|
|
3374
|
+
await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
|
|
3312
3375
|
result.added++;
|
|
3313
3376
|
} else if (existing.content !== content) {
|
|
3314
3377
|
if (!opts.dryRun)
|
|
3315
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs }
|
|
3378
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
|
|
3316
3379
|
result.updated++;
|
|
3317
3380
|
} else if (!outputsEqual(existing.outputs, outputs)) {
|
|
3318
3381
|
if (!opts.dryRun)
|
|
3319
|
-
updateConfig(existing.id, { outputs }
|
|
3382
|
+
await store.updateConfig(existing.id, { outputs });
|
|
3320
3383
|
result.updated++;
|
|
3321
3384
|
} else {
|
|
3322
3385
|
result.unchanged++;
|
|
@@ -3348,7 +3411,7 @@ async function syncKnown(opts = {}) {
|
|
|
3348
3411
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
|
|
3349
3412
|
if (!existing) {
|
|
3350
3413
|
if (!opts.dryRun) {
|
|
3351
|
-
createConfig({
|
|
3414
|
+
await store.createConfig({
|
|
3352
3415
|
name: known.name,
|
|
3353
3416
|
category: known.category,
|
|
3354
3417
|
agent: known.agent,
|
|
@@ -3359,16 +3422,16 @@ async function syncKnown(opts = {}) {
|
|
|
3359
3422
|
description: known.description,
|
|
3360
3423
|
is_template: isTemplate2,
|
|
3361
3424
|
outputs: known.outputs
|
|
3362
|
-
}
|
|
3425
|
+
});
|
|
3363
3426
|
}
|
|
3364
3427
|
result.added++;
|
|
3365
3428
|
} else if (existing.content !== content) {
|
|
3366
3429
|
if (!opts.dryRun)
|
|
3367
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }
|
|
3430
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
|
|
3368
3431
|
result.updated++;
|
|
3369
3432
|
} else if (!outputsEqual(existing.outputs, known.outputs)) {
|
|
3370
3433
|
if (!opts.dryRun)
|
|
3371
|
-
updateConfig(existing.id, { outputs: known.outputs }
|
|
3434
|
+
await store.updateConfig(existing.id, { outputs: known.outputs });
|
|
3372
3435
|
result.updated++;
|
|
3373
3436
|
} else {
|
|
3374
3437
|
result.unchanged++;
|
|
@@ -3380,9 +3443,9 @@ async function syncKnown(opts = {}) {
|
|
|
3380
3443
|
return result;
|
|
3381
3444
|
}
|
|
3382
3445
|
async function syncToDisk(opts = {}) {
|
|
3383
|
-
const
|
|
3446
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3384
3447
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3385
|
-
const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }
|
|
3448
|
+
const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
|
|
3386
3449
|
const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
|
|
3387
3450
|
let configs = allFileConfigs.filter((config) => {
|
|
3388
3451
|
return !isGeneratedOutputTarget2(config, outputOwners);
|
|
@@ -3395,7 +3458,7 @@ async function syncToDisk(opts = {}) {
|
|
|
3395
3458
|
if (!config.target_path && config.outputs.length === 0)
|
|
3396
3459
|
continue;
|
|
3397
3460
|
try {
|
|
3398
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
3461
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
|
|
3399
3462
|
r.changed ? result.updated++ : result.unchanged++;
|
|
3400
3463
|
} catch {
|
|
3401
3464
|
result.skipped.push(config.target_path ?? config.id);
|
|
@@ -3432,12 +3495,12 @@ function buildDiff(expectedContent, targetPath) {
|
|
|
3432
3495
|
return lines.join(`
|
|
3433
3496
|
`);
|
|
3434
3497
|
}
|
|
3435
|
-
function diffConfig(config, opts = {}) {
|
|
3498
|
+
async function diffConfig(config, opts = {}) {
|
|
3436
3499
|
if (!config.target_path && config.outputs.length === 0)
|
|
3437
3500
|
return "(reference \u2014 no target path)";
|
|
3438
3501
|
const diffs = [];
|
|
3439
|
-
const
|
|
3440
|
-
const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(
|
|
3502
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3503
|
+
const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
3441
3504
|
if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
|
|
3442
3505
|
return "(generated output \u2014 managed by fan-out)";
|
|
3443
3506
|
}
|
|
@@ -3515,12 +3578,12 @@ function detectFormat(filePath) {
|
|
|
3515
3578
|
return "text";
|
|
3516
3579
|
}
|
|
3517
3580
|
// src/lib/export.ts
|
|
3518
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as
|
|
3581
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3519
3582
|
import { join as join7, resolve as resolve4 } from "path";
|
|
3520
3583
|
import { tmpdir } from "os";
|
|
3521
3584
|
async function exportConfigs(outputPath, opts = {}) {
|
|
3522
|
-
const
|
|
3523
|
-
const configs = listConfigs(opts.filter
|
|
3585
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3586
|
+
const configs = await store.listConfigs(opts.filter);
|
|
3524
3587
|
const absOutput = resolve4(outputPath);
|
|
3525
3588
|
const tmpDir = join7(tmpdir(), `configs-export-${Date.now()}`);
|
|
3526
3589
|
const contentsDir = join7(tmpDir, "contents");
|
|
@@ -3528,7 +3591,7 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
3528
3591
|
mkdirSync4(contentsDir, { recursive: true });
|
|
3529
3592
|
const manifest = {
|
|
3530
3593
|
version: "1.0.0",
|
|
3531
|
-
exported_at:
|
|
3594
|
+
exported_at: new Date().toISOString(),
|
|
3532
3595
|
configs: configs.map(({ content: _content, ...meta }) => meta)
|
|
3533
3596
|
};
|
|
3534
3597
|
writeFileSync3(join7(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
@@ -3548,16 +3611,16 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
3548
3611
|
return { path: absOutput, count: configs.length };
|
|
3549
3612
|
} finally {
|
|
3550
3613
|
if (existsSync9(tmpDir)) {
|
|
3551
|
-
|
|
3614
|
+
rmSync3(tmpDir, { recursive: true, force: true });
|
|
3552
3615
|
}
|
|
3553
3616
|
}
|
|
3554
3617
|
}
|
|
3555
3618
|
// src/lib/import.ts
|
|
3556
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as
|
|
3619
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
3557
3620
|
import { join as join8, resolve as resolve5 } from "path";
|
|
3558
3621
|
import { tmpdir as tmpdir2 } from "os";
|
|
3559
3622
|
async function importConfigs(bundlePath, opts = {}) {
|
|
3560
|
-
const
|
|
3623
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3561
3624
|
const conflict = opts.conflict ?? "skip";
|
|
3562
3625
|
const absPath = resolve5(bundlePath);
|
|
3563
3626
|
const tmpDir = join8(tmpdir2(), `configs-import-${Date.now()}`);
|
|
@@ -3584,17 +3647,17 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3584
3647
|
const content = existsSync10(contentFile) ? readFileSync7(contentFile, "utf-8") : "";
|
|
3585
3648
|
let existing = null;
|
|
3586
3649
|
try {
|
|
3587
|
-
existing = getConfig(meta.slug
|
|
3650
|
+
existing = await store.getConfig(meta.slug);
|
|
3588
3651
|
} catch {}
|
|
3589
3652
|
if (existing) {
|
|
3590
3653
|
if (conflict === "skip") {
|
|
3591
3654
|
result.skipped++;
|
|
3592
3655
|
} else if (conflict === "overwrite" || conflict === "version") {
|
|
3593
|
-
updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs }
|
|
3656
|
+
await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
|
|
3594
3657
|
result.updated++;
|
|
3595
3658
|
}
|
|
3596
3659
|
} else {
|
|
3597
|
-
createConfig({
|
|
3660
|
+
await store.createConfig({
|
|
3598
3661
|
name: meta.name,
|
|
3599
3662
|
kind: meta.kind,
|
|
3600
3663
|
category: meta.category,
|
|
@@ -3606,7 +3669,7 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3606
3669
|
description: meta.description ?? undefined,
|
|
3607
3670
|
tags: meta.tags,
|
|
3608
3671
|
is_template: meta.is_template
|
|
3609
|
-
}
|
|
3672
|
+
});
|
|
3610
3673
|
result.created++;
|
|
3611
3674
|
}
|
|
3612
3675
|
} catch (err) {
|
|
@@ -3616,15 +3679,397 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3616
3679
|
return result;
|
|
3617
3680
|
} finally {
|
|
3618
3681
|
if (existsSync10(tmpDir)) {
|
|
3619
|
-
|
|
3682
|
+
rmSync4(tmpDir, { recursive: true, force: true });
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
// src/lib/package-manager-guard.ts
|
|
3687
|
+
import { execFileSync } from "child_process";
|
|
3688
|
+
import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
3689
|
+
import { homedir as homedir5 } from "os";
|
|
3690
|
+
import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join9, relative as relative4, resolve as resolve6 } from "path";
|
|
3691
|
+
var SKIP_DIRS = new Set([
|
|
3692
|
+
".git",
|
|
3693
|
+
"node_modules",
|
|
3694
|
+
"dist",
|
|
3695
|
+
"build",
|
|
3696
|
+
"coverage",
|
|
3697
|
+
".next",
|
|
3698
|
+
".turbo",
|
|
3699
|
+
".cache"
|
|
3700
|
+
]);
|
|
3701
|
+
var LOCKFILE_NAMES = new Set([
|
|
3702
|
+
"bun.lock",
|
|
3703
|
+
"package-lock.json",
|
|
3704
|
+
"npm-shrinkwrap.json",
|
|
3705
|
+
"pnpm-lock.yaml",
|
|
3706
|
+
"yarn.lock"
|
|
3707
|
+
]);
|
|
3708
|
+
var HOME_FILES = [
|
|
3709
|
+
".npmrc",
|
|
3710
|
+
".bunfig.toml",
|
|
3711
|
+
"bunfig.toml",
|
|
3712
|
+
".bashrc",
|
|
3713
|
+
".bash_profile",
|
|
3714
|
+
".zshrc",
|
|
3715
|
+
".zprofile",
|
|
3716
|
+
".profile"
|
|
3717
|
+
];
|
|
3718
|
+
var TOKEN_VALUE_PATTERNS = [
|
|
3719
|
+
{ re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
|
|
3720
|
+
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
|
|
3721
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
|
|
3722
|
+
{ re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
|
|
3723
|
+
{ re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
|
|
3724
|
+
{ re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
|
|
3725
|
+
];
|
|
3726
|
+
function scanPackageManagerSecrets(options = {}) {
|
|
3727
|
+
const cwd = options.cwd ? resolve6(options.cwd) : process.cwd();
|
|
3728
|
+
const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve6(cwd, root));
|
|
3729
|
+
const findings = [];
|
|
3730
|
+
let scannedFiles = 0;
|
|
3731
|
+
for (const root of roots) {
|
|
3732
|
+
if (!existsSync11(root))
|
|
3733
|
+
continue;
|
|
3734
|
+
const stat = lstatSync2(root);
|
|
3735
|
+
if (stat.isFile()) {
|
|
3736
|
+
if (!shouldScanRepoFile(root))
|
|
3737
|
+
continue;
|
|
3738
|
+
const text = readTextFile(root);
|
|
3739
|
+
if (text === null)
|
|
3740
|
+
continue;
|
|
3741
|
+
scannedFiles++;
|
|
3742
|
+
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
|
|
3743
|
+
continue;
|
|
3744
|
+
}
|
|
3745
|
+
if (!stat.isDirectory())
|
|
3746
|
+
continue;
|
|
3747
|
+
const tracked = trackedFiles(root);
|
|
3748
|
+
for (const file of collectRepoFiles(root)) {
|
|
3749
|
+
const rel = toPosix(relative4(root, file));
|
|
3750
|
+
const isTracked = tracked.has(rel);
|
|
3751
|
+
const text = readTextFile(file);
|
|
3752
|
+
if (text === null)
|
|
3753
|
+
continue;
|
|
3754
|
+
scannedFiles++;
|
|
3755
|
+
findings.push(...scanFile(file, text, classifyRepoFile(file), isTracked, root));
|
|
3756
|
+
}
|
|
3757
|
+
}
|
|
3758
|
+
if (options.includeHome) {
|
|
3759
|
+
const home = homedir5();
|
|
3760
|
+
for (const name of HOME_FILES) {
|
|
3761
|
+
const file = join9(home, name);
|
|
3762
|
+
if (!existsSync11(file))
|
|
3763
|
+
continue;
|
|
3764
|
+
const text = readTextFile(file);
|
|
3765
|
+
if (text === null)
|
|
3766
|
+
continue;
|
|
3767
|
+
scannedFiles++;
|
|
3768
|
+
findings.push(...scanFile(file, text, classifyHomeFile(name), false, home));
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
|
|
3772
|
+
return {
|
|
3773
|
+
clean: findings.length === 0,
|
|
3774
|
+
scannedFiles,
|
|
3775
|
+
scannedRoots: roots,
|
|
3776
|
+
findings
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
function collectRepoFiles(root) {
|
|
3780
|
+
const out = [];
|
|
3781
|
+
const visit = (dir) => {
|
|
3782
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
3783
|
+
if (entry.isDirectory()) {
|
|
3784
|
+
if (SKIP_DIRS.has(entry.name))
|
|
3785
|
+
continue;
|
|
3786
|
+
visit(join9(dir, entry.name));
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
if (!entry.isFile())
|
|
3790
|
+
continue;
|
|
3791
|
+
const file = join9(dir, entry.name);
|
|
3792
|
+
if (shouldScanRepoFile(file))
|
|
3793
|
+
out.push(file);
|
|
3794
|
+
}
|
|
3795
|
+
};
|
|
3796
|
+
visit(root);
|
|
3797
|
+
return out;
|
|
3798
|
+
}
|
|
3799
|
+
function shouldScanRepoFile(file) {
|
|
3800
|
+
const name = basename5(file);
|
|
3801
|
+
return isNpmrcName(name) || isBunConfigName(name) || LOCKFILE_NAMES.has(name);
|
|
3802
|
+
}
|
|
3803
|
+
function classifyRepoFile(file) {
|
|
3804
|
+
const name = basename5(file);
|
|
3805
|
+
if (isNpmrcName(name))
|
|
3806
|
+
return "repo-npmrc";
|
|
3807
|
+
if (isBunConfigName(name))
|
|
3808
|
+
return "bun-config";
|
|
3809
|
+
return "lockfile";
|
|
3810
|
+
}
|
|
3811
|
+
function classifyHomeFile(name) {
|
|
3812
|
+
if (name === ".npmrc")
|
|
3813
|
+
return "home-npmrc";
|
|
3814
|
+
if (isBunConfigName(name))
|
|
3815
|
+
return "bun-config";
|
|
3816
|
+
return "shell-profile";
|
|
3817
|
+
}
|
|
3818
|
+
function isBunConfigName(name) {
|
|
3819
|
+
return name === "bunfig.toml" || name === ".bunfig.toml";
|
|
3820
|
+
}
|
|
3821
|
+
function isNpmrcName(name) {
|
|
3822
|
+
return name === ".npmrc" || name.startsWith(".npmrc.") || name.endsWith(".npmrc");
|
|
3823
|
+
}
|
|
3824
|
+
function readTextFile(file) {
|
|
3825
|
+
try {
|
|
3826
|
+
const stat = lstatSync2(file);
|
|
3827
|
+
if (!stat.isFile() || stat.size > 5000000)
|
|
3828
|
+
return null;
|
|
3829
|
+
const buf = readFileSync8(file);
|
|
3830
|
+
if (buf.includes(0))
|
|
3831
|
+
return null;
|
|
3832
|
+
return buf.toString("utf-8");
|
|
3833
|
+
} catch {
|
|
3834
|
+
return null;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
function scanFile(file, text, surface, tracked, root) {
|
|
3838
|
+
const findings = [];
|
|
3839
|
+
const path = displayPath(file, root);
|
|
3840
|
+
if (surface === "bun-config")
|
|
3841
|
+
return scanBunConfigFile(text, path, tracked);
|
|
3842
|
+
const lines = text.split(/\r?\n/);
|
|
3843
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3844
|
+
const line = lines[i];
|
|
3845
|
+
const lineNo = i + 1;
|
|
3846
|
+
if (surface === "repo-npmrc" || surface === "home-npmrc") {
|
|
3847
|
+
findings.push(...scanNpmrcLine(line, path, lineNo, surface, tracked));
|
|
3848
|
+
} else if (surface === "shell-profile") {
|
|
3849
|
+
findings.push(...scanShellProfileLine(line, path, lineNo, tracked));
|
|
3850
|
+
} else {
|
|
3851
|
+
findings.push(...scanLockfileLine(line, path, lineNo, tracked));
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
return findings;
|
|
3855
|
+
}
|
|
3856
|
+
function scanNpmrcLine(lineText, path, line, surface, tracked) {
|
|
3857
|
+
const findings = [];
|
|
3858
|
+
const stripped = lineText.trim();
|
|
3859
|
+
if (stripped === "" || stripped.startsWith("#") || stripped.startsWith(";"))
|
|
3860
|
+
return findings;
|
|
3861
|
+
const auth = stripped.match(/(?:^|:)(_[A-Za-z]*(?:auth|password)[A-Za-z]*|password)\s*=\s*(.+)$/i);
|
|
3862
|
+
if (auth) {
|
|
3863
|
+
const value = stripQuotes(stripInlineComment(auth[2].trim()));
|
|
3864
|
+
if (value && !isSafeReference(value)) {
|
|
3865
|
+
findings.push({
|
|
3866
|
+
path,
|
|
3867
|
+
line,
|
|
3868
|
+
rule: "npmrc-literal-auth",
|
|
3869
|
+
surface,
|
|
3870
|
+
severity: "error",
|
|
3871
|
+
tracked,
|
|
3872
|
+
detail: tracked ? "tracked npm auth entry uses a literal value" : "npm auth entry uses a literal value"
|
|
3873
|
+
});
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
findings.push(...scanCredentialedUrl(stripped, path, line, surface, tracked));
|
|
3877
|
+
findings.push(...scanKnownTokenPatterns(stripped, path, line, surface, tracked));
|
|
3878
|
+
return findings;
|
|
3879
|
+
}
|
|
3880
|
+
function scanBunConfigFile(text, path, tracked) {
|
|
3881
|
+
const findings = [];
|
|
3882
|
+
const lines = text.split(/\r?\n/);
|
|
3883
|
+
let inReleaseAgeExcludes = false;
|
|
3884
|
+
let hasMinimumReleaseAge = false;
|
|
3885
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3886
|
+
const lineText = lines[i];
|
|
3887
|
+
const line = i + 1;
|
|
3888
|
+
const stripped = lineText.trim();
|
|
3889
|
+
if (stripped === "" || stripped.startsWith("#"))
|
|
3890
|
+
continue;
|
|
3891
|
+
const releaseAge = stripped.match(/^minimumReleaseAge\s*=\s*(?:"([^"]+)"|'([^']+)'|([0-9]+))\s*(?:#.*)?$/i);
|
|
3892
|
+
if (releaseAge) {
|
|
3893
|
+
hasMinimumReleaseAge = true;
|
|
3894
|
+
const rawValue = releaseAge[1] ?? releaseAge[2] ?? releaseAge[3] ?? "";
|
|
3895
|
+
const value = Number(rawValue);
|
|
3896
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
3897
|
+
findings.push({
|
|
3898
|
+
path,
|
|
3899
|
+
line,
|
|
3900
|
+
rule: "bun-release-age-disabled",
|
|
3901
|
+
surface: "bun-config",
|
|
3902
|
+
severity: "error",
|
|
3903
|
+
tracked,
|
|
3904
|
+
detail: "Bun release-age quarantine is disabled"
|
|
3905
|
+
});
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
const startsReleaseAgeExcludes = /minimumReleaseAgeExcludes/i.test(stripped);
|
|
3909
|
+
const scanExcludes = startsReleaseAgeExcludes || inReleaseAgeExcludes;
|
|
3910
|
+
if (scanExcludes) {
|
|
3911
|
+
const quoted = [...stripped.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
|
|
3912
|
+
for (const item of quoted) {
|
|
3913
|
+
if (!isExactHasnaPackageName(item)) {
|
|
3914
|
+
findings.push({
|
|
3915
|
+
path,
|
|
3916
|
+
line,
|
|
3917
|
+
rule: "bun-release-age-broad-exclude",
|
|
3918
|
+
surface: "bun-config",
|
|
3919
|
+
severity: "error",
|
|
3920
|
+
tracked,
|
|
3921
|
+
detail: "Bun release-age exclude must be an exact @hasna package name"
|
|
3922
|
+
});
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
inReleaseAgeExcludes = startsReleaseAgeExcludes ? stripped.includes("[") && !stripped.includes("]") : inReleaseAgeExcludes && !stripped.includes("]");
|
|
3927
|
+
findings.push(...scanKnownTokenPatterns(stripped, path, line, "bun-config", tracked));
|
|
3928
|
+
}
|
|
3929
|
+
if (!hasMinimumReleaseAge) {
|
|
3930
|
+
findings.push({
|
|
3931
|
+
path,
|
|
3932
|
+
line: 1,
|
|
3933
|
+
rule: "bun-release-age-missing",
|
|
3934
|
+
surface: "bun-config",
|
|
3935
|
+
severity: "error",
|
|
3936
|
+
tracked,
|
|
3937
|
+
detail: "Bun release-age quarantine must be configured with a positive minimumReleaseAge"
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3940
|
+
return findings;
|
|
3941
|
+
}
|
|
3942
|
+
function scanShellProfileLine(lineText, path, line, tracked) {
|
|
3943
|
+
const findings = [];
|
|
3944
|
+
const stripped = lineText.trim();
|
|
3945
|
+
if (stripped === "" || stripped.startsWith("#"))
|
|
3946
|
+
return findings;
|
|
3947
|
+
const assignment = stripped.match(/^(?:export\s+)?(NPM(?:_CONFIG)?_[A-Z0-9_]*TOKEN|NODE_AUTH_TOKEN|NPM_TOKEN)\s*=\s*(.+)$/);
|
|
3948
|
+
if (assignment) {
|
|
3949
|
+
const value = stripQuotes(stripInlineComment(assignment[2].trim()));
|
|
3950
|
+
if (value && !isSafeReference(value)) {
|
|
3951
|
+
findings.push({
|
|
3952
|
+
path,
|
|
3953
|
+
line,
|
|
3954
|
+
rule: "shell-literal-package-token",
|
|
3955
|
+
surface: "shell-profile",
|
|
3956
|
+
severity: "error",
|
|
3957
|
+
tracked,
|
|
3958
|
+
detail: "shell profile package-manager token uses a literal value"
|
|
3959
|
+
});
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
findings.push(...scanKnownTokenPatterns(stripped, path, line, "shell-profile", tracked));
|
|
3963
|
+
return findings;
|
|
3964
|
+
}
|
|
3965
|
+
function scanLockfileLine(lineText, path, line, tracked) {
|
|
3966
|
+
const findings = scanKnownTokenPatterns(lineText, path, line, "lockfile", tracked);
|
|
3967
|
+
if (/(?:^|:)_authToken\s*=\s*/i.test(lineText) && !/\$\{[A-Z0-9_]+\}|\{\{[A-Z0-9_]+\}\}/.test(lineText)) {
|
|
3968
|
+
findings.push({
|
|
3969
|
+
path,
|
|
3970
|
+
line,
|
|
3971
|
+
rule: "lockfile-auth-token",
|
|
3972
|
+
surface: "lockfile",
|
|
3973
|
+
severity: "error",
|
|
3974
|
+
tracked,
|
|
3975
|
+
detail: "lockfile contains package-manager auth token material"
|
|
3976
|
+
});
|
|
3977
|
+
}
|
|
3978
|
+
return findings;
|
|
3979
|
+
}
|
|
3980
|
+
function scanKnownTokenPatterns(lineText, path, line, surface, tracked) {
|
|
3981
|
+
const findings = [];
|
|
3982
|
+
for (const pattern of TOKEN_VALUE_PATTERNS) {
|
|
3983
|
+
if (pattern.re.test(lineText)) {
|
|
3984
|
+
findings.push({
|
|
3985
|
+
path,
|
|
3986
|
+
line,
|
|
3987
|
+
rule: pattern.rule,
|
|
3988
|
+
surface,
|
|
3989
|
+
severity: "error",
|
|
3990
|
+
tracked,
|
|
3991
|
+
detail: pattern.detail
|
|
3992
|
+
});
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
return findings;
|
|
3996
|
+
}
|
|
3997
|
+
function scanCredentialedUrl(lineText, path, line, surface, tracked) {
|
|
3998
|
+
const findings = [];
|
|
3999
|
+
for (const match of lineText.matchAll(/\bhttps?:\/\/([^/\s#;]+)@/gi)) {
|
|
4000
|
+
const userInfo = match[1];
|
|
4001
|
+
const credentialPart = userInfo.includes(":") ? userInfo.split(":").slice(1).join(":") : userInfo;
|
|
4002
|
+
if (credentialPart && !isSafeReference(credentialPart)) {
|
|
4003
|
+
findings.push({
|
|
4004
|
+
path,
|
|
4005
|
+
line,
|
|
4006
|
+
rule: "package-manager-url-credentials",
|
|
4007
|
+
surface,
|
|
4008
|
+
severity: "error",
|
|
4009
|
+
tracked,
|
|
4010
|
+
detail: "package-manager URL embeds literal credentials"
|
|
4011
|
+
});
|
|
3620
4012
|
}
|
|
3621
4013
|
}
|
|
4014
|
+
return findings;
|
|
4015
|
+
}
|
|
4016
|
+
function trackedFiles(root) {
|
|
4017
|
+
try {
|
|
4018
|
+
const output = execFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
4019
|
+
encoding: "utf-8",
|
|
4020
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4021
|
+
});
|
|
4022
|
+
return new Set(output.split("\x00").filter(Boolean).map(toPosix));
|
|
4023
|
+
} catch {
|
|
4024
|
+
return new Set;
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
function isTrackedFile(file) {
|
|
4028
|
+
try {
|
|
4029
|
+
const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
|
|
4030
|
+
encoding: "utf-8",
|
|
4031
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4032
|
+
}).trim();
|
|
4033
|
+
const rel = toPosix(relative4(repoRoot, file));
|
|
4034
|
+
execFileSync("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
|
|
4035
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
4036
|
+
});
|
|
4037
|
+
return true;
|
|
4038
|
+
} catch {
|
|
4039
|
+
return false;
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
function isExactHasnaPackageName(item) {
|
|
4043
|
+
return /^@hasna\/[a-z0-9][a-z0-9._-]*$/.test(item);
|
|
4044
|
+
}
|
|
4045
|
+
function isSafeReference(value) {
|
|
4046
|
+
const trimmed = stripQuotes(value.trim());
|
|
4047
|
+
return /^\$\{[A-Z][A-Z0-9_]*\}$/.test(trimmed) || /^\$[A-Z][A-Z0-9_]*$/.test(trimmed) || /^\{\{[A-Z][A-Z0-9_]*\}\}$/.test(trimmed) || /^%[A-Z][A-Z0-9_]*%$/.test(trimmed);
|
|
4048
|
+
}
|
|
4049
|
+
function stripQuotes(value) {
|
|
4050
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4051
|
+
return value.slice(1, -1);
|
|
4052
|
+
}
|
|
4053
|
+
return value;
|
|
4054
|
+
}
|
|
4055
|
+
function stripInlineComment(value) {
|
|
4056
|
+
return value.replace(/\s[#;].*$/, "").trim();
|
|
4057
|
+
}
|
|
4058
|
+
function displayPath(file, root) {
|
|
4059
|
+
const home = homedir5();
|
|
4060
|
+
if (root === home && (file === home || file.startsWith(home + "/")))
|
|
4061
|
+
return "~/" + toPosix(relative4(home, file));
|
|
4062
|
+
if (isAbsolute3(root) && file.startsWith(root + "/"))
|
|
4063
|
+
return toPosix(relative4(root, file));
|
|
4064
|
+
if (file === home || file.startsWith(home + "/"))
|
|
4065
|
+
return "~/" + toPosix(relative4(home, file));
|
|
4066
|
+
return file;
|
|
4067
|
+
}
|
|
4068
|
+
function toPosix(path) {
|
|
4069
|
+
return path.split("\\").join("/");
|
|
3622
4070
|
}
|
|
3623
4071
|
export {
|
|
3624
4072
|
uuid,
|
|
3625
|
-
updateProfile,
|
|
3626
|
-
updateMachineApplied,
|
|
3627
|
-
updateConfig,
|
|
3628
4073
|
transformSkillContent,
|
|
3629
4074
|
templateizeMachineContent,
|
|
3630
4075
|
syncToDisk,
|
|
@@ -3633,58 +4078,30 @@ export {
|
|
|
3633
4078
|
syncKnown,
|
|
3634
4079
|
syncFromDir,
|
|
3635
4080
|
stripClaudeOnlySections,
|
|
3636
|
-
storageSync,
|
|
3637
|
-
storagePush,
|
|
3638
|
-
storagePull,
|
|
3639
4081
|
sourcesFromIdentityExport,
|
|
3640
4082
|
sourceFromFilePath,
|
|
3641
4083
|
sourceFromConfig,
|
|
3642
4084
|
slugify,
|
|
3643
4085
|
scanSecrets,
|
|
3644
|
-
|
|
3645
|
-
resolveTables,
|
|
4086
|
+
scanPackageManagerSecrets,
|
|
3646
4087
|
resolveSessionTargetOwnership,
|
|
3647
4088
|
resolveSessionPath,
|
|
3648
4089
|
resolveProfileVariables,
|
|
3649
|
-
|
|
3650
|
-
|
|
4090
|
+
resolveConfigStore,
|
|
4091
|
+
resolveCloudConfig,
|
|
3651
4092
|
renderTemplate,
|
|
3652
4093
|
renderMachineAwareContent,
|
|
3653
|
-
removeConfigFromProfile,
|
|
3654
|
-
registerMachine,
|
|
3655
4094
|
redactContent,
|
|
3656
|
-
pruneSnapshots,
|
|
3657
|
-
profileMatchesMachine,
|
|
3658
|
-
profileHasSelectors,
|
|
3659
4095
|
planSessionRender,
|
|
3660
4096
|
parseTemplateVars,
|
|
3661
4097
|
now,
|
|
3662
4098
|
normalizeOsFamily,
|
|
3663
4099
|
machineContextToVariables,
|
|
3664
|
-
listSnapshots,
|
|
3665
|
-
listProfiles,
|
|
3666
|
-
listMachines,
|
|
3667
|
-
listConfigs,
|
|
3668
4100
|
isTemplate,
|
|
3669
|
-
|
|
4101
|
+
isCloudMode,
|
|
3670
4102
|
importConfigs,
|
|
3671
4103
|
hasSecrets,
|
|
3672
|
-
getSyncMetaAll,
|
|
3673
|
-
getStorageSyncMetaAll,
|
|
3674
|
-
getStorageStatus,
|
|
3675
|
-
getStoragePg,
|
|
3676
|
-
getStorageMode,
|
|
3677
|
-
getStorageDatabaseUrl,
|
|
3678
|
-
getStorageDatabaseEnvName,
|
|
3679
|
-
getSnapshotByVersion,
|
|
3680
|
-
getSnapshot,
|
|
3681
|
-
getProfileConfigs,
|
|
3682
|
-
getProfile,
|
|
3683
|
-
getDatabase,
|
|
3684
4104
|
getConfigsStatus,
|
|
3685
|
-
getConfigStats,
|
|
3686
|
-
getConfigById,
|
|
3687
|
-
getConfig,
|
|
3688
4105
|
extractTemplateVars,
|
|
3689
4106
|
exportConfigs,
|
|
3690
4107
|
expandPath,
|
|
@@ -3695,17 +4112,11 @@ export {
|
|
|
3695
4112
|
detectFormat,
|
|
3696
4113
|
detectCategory,
|
|
3697
4114
|
detectAgent,
|
|
3698
|
-
deleteProfile,
|
|
3699
|
-
deleteConfig,
|
|
3700
4115
|
currentOs,
|
|
3701
4116
|
currentHostname2 as currentHostname,
|
|
3702
4117
|
currentArch2 as currentArch,
|
|
3703
|
-
createSnapshot,
|
|
3704
|
-
createProfile,
|
|
3705
|
-
createConfig,
|
|
3706
4118
|
cleanSessionPathInput,
|
|
3707
4119
|
checkSessionRenderDrift,
|
|
3708
|
-
buildPgPoolConfig,
|
|
3709
4120
|
buildOpenCodeAgentsMd,
|
|
3710
4121
|
buildCursorMdc,
|
|
3711
4122
|
buildCodexAgentsMd,
|
|
@@ -3713,37 +4124,30 @@ export {
|
|
|
3713
4124
|
applySessionRender,
|
|
3714
4125
|
applyConfigs,
|
|
3715
4126
|
applyConfig,
|
|
3716
|
-
addConfigToProfile,
|
|
3717
4127
|
TemplateRenderError,
|
|
3718
4128
|
SessionApplyError,
|
|
3719
|
-
STORAGE_TABLES,
|
|
3720
|
-
STORAGE_MODE_ENV,
|
|
3721
|
-
STORAGE_DATABASE_ENV,
|
|
3722
4129
|
SESSION_TOOL_ADAPTERS,
|
|
3723
4130
|
SESSION_RENDER_TOOLS,
|
|
3724
4131
|
SESSION_RENDER_SCHEMA,
|
|
3725
4132
|
SESSION_RENDER_MANAGED_MARKER,
|
|
3726
4133
|
RAW_STORE_ROOT_ENV,
|
|
3727
4134
|
ProfileNotFoundError,
|
|
3728
|
-
PgAdapterAsync,
|
|
3729
4135
|
PROJECT_DASHBOARD_STANDARD_SLUG,
|
|
3730
4136
|
PROJECT_DASHBOARD_STANDARD_CONTENT,
|
|
3731
4137
|
PROJECT_DASHBOARD_PROFILE_VARIABLES,
|
|
3732
4138
|
PROJECT_CONFIG_FILES,
|
|
3733
4139
|
PLATFORM_PROFILE_PRESETS,
|
|
3734
4140
|
PG_MIGRATIONS,
|
|
4141
|
+
LocalConfigStore,
|
|
3735
4142
|
KNOWN_CONFIGS,
|
|
3736
4143
|
ConfigNotFoundError,
|
|
3737
4144
|
ConfigApplyError,
|
|
4145
|
+
CloudHttpError,
|
|
4146
|
+
CloudConfigStore,
|
|
3738
4147
|
CONFIG_TRANSFORMS,
|
|
3739
4148
|
CONFIG_KINDS,
|
|
3740
4149
|
CONFIG_FORMATS,
|
|
3741
4150
|
CONFIG_CATEGORIES,
|
|
3742
4151
|
CONFIG_AGENTS,
|
|
3743
|
-
CONFIGS_STORAGE_TABLES,
|
|
3744
|
-
CONFIGS_STORAGE_MODE_FALLBACK_ENV,
|
|
3745
|
-
CONFIGS_STORAGE_MODE_ENV,
|
|
3746
|
-
CONFIGS_STORAGE_FALLBACK_ENV,
|
|
3747
|
-
CONFIGS_STORAGE_ENV,
|
|
3748
4152
|
CODEWITH_NATIVE_IMPORTS_ENV
|
|
3749
4153
|
};
|