@hasna/instructions 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -12
- package/dashboard/README.md +73 -0
- package/dist/cli/index.js +1305 -837
- package/dist/data/config-store.d.ts +134 -0
- package/dist/data/config-store.d.ts.map +1 -0
- package/dist/data/config-store.test.d.ts +2 -0
- package/dist/data/config-store.test.d.ts.map +1 -0
- package/dist/db/database.d.ts +15 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +908 -492
- package/dist/lib/apply.d.ts +2 -2
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/export.d.ts +2 -2
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +2 -2
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/package-manager-guard.d.ts +24 -0
- package/dist/lib/package-manager-guard.d.ts.map +1 -0
- package/dist/lib/package-manager-guard.test.d.ts +2 -0
- package/dist/lib/package-manager-guard.test.d.ts.map +1 -0
- package/dist/lib/platform-profiles.d.ts +2 -2
- package/dist/lib/platform-profiles.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +2 -2
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/lib/redact.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts +3 -3
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/lib/sync.d.ts +6 -6
- package/dist/lib/sync.d.ts.map +1 -1
- package/dist/mcp/http.d.ts +0 -13
- package/dist/mcp/http.d.ts.map +1 -1
- package/dist/mcp/index.js +661 -574
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1757 -17541
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/status.d.ts +2 -2
- package/dist/status.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +21 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/schema.d.ts.map +1 -1
- package/package.json +4 -7
- package/dashboard/dist/assets/index-D7p6fFQw.js +0 -11
- package/dashboard/dist/assets/index-DQ3P1g1z.css +0 -1
- package/dashboard/dist/index.html +0 -14
- package/dashboard/dist/vite.svg +0 -1
- package/dist/cli/storage.d.ts +0 -3
- package/dist/cli/storage.d.ts.map +0 -1
- package/dist/cli/storage.test.d.ts +0 -2
- package/dist/cli/storage.test.d.ts.map +0 -1
- package/dist/db/remote-storage.d.ts +0 -13
- package/dist/db/remote-storage.d.ts.map +0 -1
- package/dist/db/storage-sync.d.ts +0 -53
- package/dist/db/storage-sync.d.ts.map +0 -1
- package/dist/db/storage-sync.test.d.ts +0 -2
- package/dist/db/storage-sync.test.d.ts.map +0 -1
- package/dist/server/server.test.d.ts +0 -2
- package/dist/server/server.test.d.ts.map +0 -1
- package/dist/storage.d.ts +0 -5
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -537
package/dist/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 {
|
|
@@ -209,6 +225,22 @@ function ensureFeedbackTable(db) {
|
|
|
209
225
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
210
226
|
)
|
|
211
227
|
`);
|
|
228
|
+
const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
|
|
229
|
+
const required = [
|
|
230
|
+
["email", "TEXT"],
|
|
231
|
+
["category", "TEXT DEFAULT 'general'"],
|
|
232
|
+
["version", "TEXT"],
|
|
233
|
+
["machine_id", "TEXT"],
|
|
234
|
+
["created_at", "TEXT"]
|
|
235
|
+
];
|
|
236
|
+
for (const [name, def] of required) {
|
|
237
|
+
if (!existing.has(name))
|
|
238
|
+
db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function insertFeedback(input, db) {
|
|
242
|
+
const d = db || getDatabase();
|
|
243
|
+
d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
|
|
212
244
|
}
|
|
213
245
|
function migrateDotfile() {
|
|
214
246
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
@@ -409,33 +441,7 @@ function getConfigStats(db) {
|
|
|
409
441
|
}
|
|
410
442
|
return stats;
|
|
411
443
|
}
|
|
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
|
-
}
|
|
444
|
+
|
|
439
445
|
// src/lib/machine.ts
|
|
440
446
|
import { arch as currentArch, homedir, hostname as currentHostname, type as currentOsType } from "os";
|
|
441
447
|
import { existsSync as existsSync2 } from "fs";
|
|
@@ -701,6 +707,35 @@ function resolveProfileForMachine(machine = detectMachineContext(), db) {
|
|
|
701
707
|
}).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
|
|
702
708
|
return matches[0]?.profile ?? null;
|
|
703
709
|
}
|
|
710
|
+
|
|
711
|
+
// src/db/snapshots.ts
|
|
712
|
+
function createSnapshot(configId, content, version, db) {
|
|
713
|
+
const d = db || getDatabase();
|
|
714
|
+
const id = uuid();
|
|
715
|
+
const ts = now();
|
|
716
|
+
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
717
|
+
return { id, config_id: configId, content, version, created_at: ts };
|
|
718
|
+
}
|
|
719
|
+
function listSnapshots(configId, db) {
|
|
720
|
+
const d = db || getDatabase();
|
|
721
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
722
|
+
}
|
|
723
|
+
function getSnapshot(id, db) {
|
|
724
|
+
const d = db || getDatabase();
|
|
725
|
+
return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
|
|
726
|
+
}
|
|
727
|
+
function getSnapshotByVersion(configId, version, db) {
|
|
728
|
+
const d = db || getDatabase();
|
|
729
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
730
|
+
}
|
|
731
|
+
function pruneSnapshots(configId, keep = 10, db) {
|
|
732
|
+
const d = db || getDatabase();
|
|
733
|
+
const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
|
|
734
|
+
SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
|
|
735
|
+
)`, [configId, configId, keep]);
|
|
736
|
+
return result.changes;
|
|
737
|
+
}
|
|
738
|
+
|
|
704
739
|
// src/db/machines.ts
|
|
705
740
|
import { arch, hostname, type } from "os";
|
|
706
741
|
function currentHostname2() {
|
|
@@ -739,358 +774,319 @@ function listMachines(db) {
|
|
|
739
774
|
const d = db || getDatabase();
|
|
740
775
|
return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
|
|
741
776
|
}
|
|
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
777
|
|
|
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();
|
|
778
|
+
// src/data/config-store.ts
|
|
779
|
+
class CloudHttpError extends Error {
|
|
780
|
+
status;
|
|
781
|
+
body;
|
|
782
|
+
constructor(status, message, body) {
|
|
783
|
+
super(message);
|
|
784
|
+
this.status = status;
|
|
785
|
+
this.body = body;
|
|
786
|
+
this.name = "CloudHttpError";
|
|
823
787
|
}
|
|
824
788
|
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
789
|
+
var API_URL_ENV = "HASNA_INSTRUCTIONS_API_URL";
|
|
790
|
+
var API_KEY_ENV = "HASNA_INSTRUCTIONS_API_KEY";
|
|
791
|
+
function resolveCloudConfig(env = process.env) {
|
|
792
|
+
const apiUrl = env[API_URL_ENV]?.trim();
|
|
793
|
+
const apiKey = env[API_KEY_ENV]?.trim();
|
|
794
|
+
if (!apiUrl && !apiKey)
|
|
795
|
+
return null;
|
|
796
|
+
if (!apiUrl || !apiKey) {
|
|
797
|
+
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.`);
|
|
798
|
+
}
|
|
799
|
+
return { apiUrl, apiKey };
|
|
833
800
|
}
|
|
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
|
-
};
|
|
801
|
+
function isCloudMode(env = process.env) {
|
|
802
|
+
return resolveCloudConfig(env) !== null;
|
|
855
803
|
}
|
|
856
804
|
|
|
857
|
-
class
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
805
|
+
class LocalConfigStore {
|
|
806
|
+
db;
|
|
807
|
+
mode = "local";
|
|
808
|
+
constructor(db) {
|
|
809
|
+
this.db = db;
|
|
810
|
+
}
|
|
811
|
+
async listConfigs(filter) {
|
|
812
|
+
return listConfigs(filter, this.db);
|
|
813
|
+
}
|
|
814
|
+
async getConfig(idOrSlug) {
|
|
815
|
+
return getConfig(idOrSlug, this.db);
|
|
816
|
+
}
|
|
817
|
+
async getConfigById(id) {
|
|
818
|
+
return getConfigById(id, this.db);
|
|
819
|
+
}
|
|
820
|
+
async createConfig(input) {
|
|
821
|
+
return createConfig(input, this.db);
|
|
822
|
+
}
|
|
823
|
+
async updateConfig(idOrSlug, input) {
|
|
824
|
+
return updateConfig(idOrSlug, input, this.db);
|
|
825
|
+
}
|
|
826
|
+
async deleteConfig(idOrSlug) {
|
|
827
|
+
deleteConfig(idOrSlug, this.db);
|
|
861
828
|
}
|
|
862
|
-
async
|
|
863
|
-
|
|
864
|
-
return { changes: result.rowCount ?? 0 };
|
|
829
|
+
async getConfigStats() {
|
|
830
|
+
return getConfigStats(this.db);
|
|
865
831
|
}
|
|
866
|
-
async
|
|
867
|
-
|
|
868
|
-
return result.rows;
|
|
832
|
+
async listSnapshots(configId) {
|
|
833
|
+
return listSnapshots(configId, this.db);
|
|
869
834
|
}
|
|
870
|
-
async
|
|
871
|
-
|
|
835
|
+
async getSnapshot(id) {
|
|
836
|
+
return getSnapshot(id, this.db);
|
|
837
|
+
}
|
|
838
|
+
async getSnapshotByVersion(configId, version) {
|
|
839
|
+
return getSnapshotByVersion(configId, version, this.db);
|
|
840
|
+
}
|
|
841
|
+
async createSnapshot(configId, content, version) {
|
|
842
|
+
return createSnapshot(configId, content, version, this.db);
|
|
843
|
+
}
|
|
844
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
845
|
+
return pruneSnapshots(configId, keep, this.db);
|
|
846
|
+
}
|
|
847
|
+
async listProfiles() {
|
|
848
|
+
return listProfiles(this.db);
|
|
849
|
+
}
|
|
850
|
+
async getProfile(idOrSlug) {
|
|
851
|
+
return getProfile(idOrSlug, this.db);
|
|
852
|
+
}
|
|
853
|
+
async getProfileConfigs(idOrSlug) {
|
|
854
|
+
return getProfileConfigs(idOrSlug, this.db);
|
|
855
|
+
}
|
|
856
|
+
async createProfile(input) {
|
|
857
|
+
return createProfile(input, this.db);
|
|
858
|
+
}
|
|
859
|
+
async updateProfile(idOrSlug, input) {
|
|
860
|
+
return updateProfile(idOrSlug, input, this.db);
|
|
861
|
+
}
|
|
862
|
+
async deleteProfile(idOrSlug) {
|
|
863
|
+
deleteProfile(idOrSlug, this.db);
|
|
864
|
+
}
|
|
865
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
866
|
+
addConfigToProfile(profileIdOrSlug, configId, this.db);
|
|
867
|
+
}
|
|
868
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
869
|
+
removeConfigFromProfile(profileIdOrSlug, configId, this.db);
|
|
870
|
+
}
|
|
871
|
+
async resolveProfileForMachine(machine) {
|
|
872
|
+
return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
|
|
873
|
+
}
|
|
874
|
+
async registerMachine(hostname2, os, arch2) {
|
|
875
|
+
return registerMachine(hostname2, os, arch2, this.db);
|
|
876
|
+
}
|
|
877
|
+
async updateMachineApplied(hostname2) {
|
|
878
|
+
updateMachineApplied(hostname2, this.db);
|
|
879
|
+
}
|
|
880
|
+
async listMachines() {
|
|
881
|
+
return listMachines(this.db);
|
|
882
|
+
}
|
|
883
|
+
async sendFeedback(input) {
|
|
884
|
+
insertFeedback(input, this.db);
|
|
885
|
+
}
|
|
886
|
+
async reset() {
|
|
887
|
+
resetLocalDatabase();
|
|
872
888
|
}
|
|
873
889
|
}
|
|
874
890
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
891
|
+
class CloudConfigStore {
|
|
892
|
+
mode = "api";
|
|
893
|
+
base;
|
|
894
|
+
apiKey;
|
|
895
|
+
timeoutMs;
|
|
896
|
+
constructor(config) {
|
|
897
|
+
this.base = `${config.apiUrl.replace(/\/+$/, "")}/v1`;
|
|
898
|
+
this.apiKey = config.apiKey;
|
|
899
|
+
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
900
|
+
}
|
|
901
|
+
async request(method, path, body, opts = {}) {
|
|
902
|
+
const controller = new AbortController;
|
|
903
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
904
|
+
const headers = {
|
|
905
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
906
|
+
Accept: "application/json"
|
|
907
|
+
};
|
|
908
|
+
if (body !== undefined)
|
|
909
|
+
headers["Content-Type"] = "application/json";
|
|
910
|
+
if (opts.idempotent)
|
|
911
|
+
headers["Idempotency-Key"] = randomUUID2();
|
|
912
|
+
try {
|
|
913
|
+
const res = await fetch(`${this.base}${path}`, {
|
|
914
|
+
method,
|
|
915
|
+
headers,
|
|
916
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
917
|
+
signal: controller.signal
|
|
918
|
+
});
|
|
919
|
+
if (res.status === 404 && opts.allow404)
|
|
920
|
+
return { status: 404, data: null };
|
|
921
|
+
const text = await res.text();
|
|
922
|
+
let parsed = null;
|
|
923
|
+
if (text) {
|
|
924
|
+
try {
|
|
925
|
+
parsed = JSON.parse(text);
|
|
926
|
+
} catch {
|
|
927
|
+
parsed = text;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (!res.ok) {
|
|
931
|
+
const message = (parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : undefined) ?? `HTTP ${res.status} on ${method} ${path}`;
|
|
932
|
+
throw new CloudHttpError(res.status, message, parsed);
|
|
933
|
+
}
|
|
934
|
+
return { status: res.status, data: parsed };
|
|
935
|
+
} finally {
|
|
936
|
+
clearTimeout(timer);
|
|
937
|
+
}
|
|
897
938
|
}
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
if (
|
|
912
|
-
|
|
939
|
+
async listConfigs(filter = {}) {
|
|
940
|
+
const params = new URLSearchParams;
|
|
941
|
+
if (filter.category)
|
|
942
|
+
params.set("category", filter.category);
|
|
943
|
+
if (filter.agent)
|
|
944
|
+
params.set("agent", filter.agent);
|
|
945
|
+
if (filter.kind)
|
|
946
|
+
params.set("kind", filter.kind);
|
|
947
|
+
if (filter.search)
|
|
948
|
+
params.set("search", filter.search);
|
|
949
|
+
const qs = params.toString();
|
|
950
|
+
const { data } = await this.request("GET", `/configs${qs ? `?${qs}` : ""}`);
|
|
951
|
+
let configs = data?.configs ?? [];
|
|
952
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
953
|
+
configs = configs.filter((c) => filter.tags.every((t) => c.tags.includes(t)));
|
|
954
|
+
}
|
|
955
|
+
if (filter.is_template !== undefined) {
|
|
956
|
+
configs = configs.filter((c) => c.is_template === filter.is_template);
|
|
957
|
+
}
|
|
958
|
+
return configs;
|
|
913
959
|
}
|
|
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();
|
|
960
|
+
async getConfig(idOrSlug) {
|
|
961
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
962
|
+
if (status === 404 || !data?.config)
|
|
963
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
964
|
+
return data.config;
|
|
945
965
|
}
|
|
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();
|
|
966
|
+
async getConfigById(id) {
|
|
967
|
+
return this.getConfig(id);
|
|
959
968
|
}
|
|
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));
|
|
969
|
+
async createConfig(input) {
|
|
970
|
+
const { data } = await this.request("POST", "/configs", input, {
|
|
971
|
+
idempotent: true
|
|
972
|
+
});
|
|
973
|
+
return data.config;
|
|
1005
974
|
}
|
|
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));
|
|
975
|
+
async updateConfig(idOrSlug, input) {
|
|
976
|
+
const { data } = await this.request("PATCH", `/configs/${encodeURIComponent(idOrSlug)}`, input);
|
|
977
|
+
return data.config;
|
|
1019
978
|
}
|
|
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
|
-
|
|
979
|
+
async deleteConfig(idOrSlug) {
|
|
980
|
+
const { status } = await this.request("DELETE", `/configs/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
981
|
+
if (status === 404)
|
|
982
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
983
|
+
}
|
|
984
|
+
async getConfigStats() {
|
|
985
|
+
const { data } = await this.request("GET", "/stats");
|
|
986
|
+
return data ?? { total: 0 };
|
|
987
|
+
}
|
|
988
|
+
async listSnapshots(configId) {
|
|
989
|
+
const { data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots`);
|
|
990
|
+
return data?.snapshots ?? [];
|
|
991
|
+
}
|
|
992
|
+
async getSnapshot(id) {
|
|
993
|
+
const { status, data } = await this.request("GET", `/snapshots/${encodeURIComponent(id)}`, undefined, { allow404: true });
|
|
994
|
+
if (status === 404 || !data?.snapshot)
|
|
995
|
+
return null;
|
|
996
|
+
return data.snapshot;
|
|
997
|
+
}
|
|
998
|
+
async getSnapshotByVersion(configId, version) {
|
|
999
|
+
const { status, data } = await this.request("GET", `/configs/${encodeURIComponent(configId)}/snapshots/${version}`, undefined, { allow404: true });
|
|
1000
|
+
if (status === 404 || !data?.snapshot)
|
|
1001
|
+
return null;
|
|
1002
|
+
return data.snapshot;
|
|
1003
|
+
}
|
|
1004
|
+
async createSnapshot(configId, content, version) {
|
|
1005
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots`, { content, version }, { idempotent: true });
|
|
1006
|
+
return data.snapshot;
|
|
1007
|
+
}
|
|
1008
|
+
async pruneSnapshots(configId, keep = 10) {
|
|
1009
|
+
const { data } = await this.request("POST", `/configs/${encodeURIComponent(configId)}/snapshots/prune`, { keep });
|
|
1010
|
+
return data?.pruned ?? 0;
|
|
1011
|
+
}
|
|
1012
|
+
async listProfiles() {
|
|
1013
|
+
const { data } = await this.request("GET", "/profiles");
|
|
1014
|
+
return data?.profiles ?? [];
|
|
1015
|
+
}
|
|
1016
|
+
async getProfile(idOrSlug) {
|
|
1017
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1018
|
+
if (status === 404 || !data?.profile)
|
|
1019
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1020
|
+
const { configs: _configs, ...profile } = data.profile;
|
|
1021
|
+
return profile;
|
|
1022
|
+
}
|
|
1023
|
+
async getProfileConfigs(idOrSlug) {
|
|
1024
|
+
const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1025
|
+
if (status === 404 || !data?.profile)
|
|
1026
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1027
|
+
return data.profile.configs ?? [];
|
|
1028
|
+
}
|
|
1029
|
+
async createProfile(input) {
|
|
1030
|
+
const { data } = await this.request("POST", "/profiles", input, {
|
|
1031
|
+
idempotent: true
|
|
1032
|
+
});
|
|
1033
|
+
return data.profile;
|
|
1034
|
+
}
|
|
1035
|
+
async updateProfile(idOrSlug, input) {
|
|
1036
|
+
const { data } = await this.request("PATCH", `/profiles/${encodeURIComponent(idOrSlug)}`, input);
|
|
1037
|
+
return data.profile;
|
|
1038
|
+
}
|
|
1039
|
+
async deleteProfile(idOrSlug) {
|
|
1040
|
+
const { status } = await this.request("DELETE", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
|
|
1041
|
+
if (status === 404)
|
|
1042
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
1043
|
+
}
|
|
1044
|
+
async addConfigToProfile(profileIdOrSlug, configId) {
|
|
1045
|
+
await this.request("POST", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs`, { config_id: configId }, { idempotent: true });
|
|
1046
|
+
}
|
|
1047
|
+
async removeConfigFromProfile(profileIdOrSlug, configId) {
|
|
1048
|
+
await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
|
|
1049
|
+
}
|
|
1050
|
+
async resolveProfileForMachine(machine) {
|
|
1051
|
+
const params = new URLSearchParams;
|
|
1052
|
+
if (machine?.hostname)
|
|
1053
|
+
params.set("hostname", machine.hostname);
|
|
1054
|
+
if (machine?.os)
|
|
1055
|
+
params.set("os", machine.os);
|
|
1056
|
+
if (machine?.arch)
|
|
1057
|
+
params.set("arch", machine.arch);
|
|
1058
|
+
const qs = params.toString();
|
|
1059
|
+
const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
|
|
1060
|
+
if (status === 404 || !data?.profile)
|
|
1061
|
+
return null;
|
|
1062
|
+
return data.profile;
|
|
1063
|
+
}
|
|
1064
|
+
async registerMachine(hostname2, os, arch2) {
|
|
1065
|
+
const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
|
|
1066
|
+
return data.machine;
|
|
1067
|
+
}
|
|
1068
|
+
async updateMachineApplied(hostname2) {
|
|
1069
|
+
await this.request("POST", "/machines/applied", { hostname: hostname2 });
|
|
1070
|
+
}
|
|
1071
|
+
async listMachines() {
|
|
1072
|
+
const { data } = await this.request("GET", "/machines");
|
|
1073
|
+
return data?.machines ?? [];
|
|
1074
|
+
}
|
|
1075
|
+
async sendFeedback(input) {
|
|
1076
|
+
await this.request("POST", "/feedback", {
|
|
1077
|
+
message: input.message,
|
|
1078
|
+
email: input.email ?? undefined,
|
|
1079
|
+
category: input.category ?? undefined,
|
|
1080
|
+
version: input.version ?? undefined
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
async reset() {
|
|
1084
|
+
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
1085
|
}
|
|
1075
1086
|
}
|
|
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);
|
|
1087
|
+
function resolveConfigStore(env = process.env) {
|
|
1088
|
+
const cloud = resolveCloudConfig(env);
|
|
1089
|
+
return cloud ? new CloudConfigStore(cloud) : new LocalConfigStore;
|
|
1094
1090
|
}
|
|
1095
1091
|
// src/status.ts
|
|
1096
1092
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
@@ -1255,8 +1251,8 @@ async function writeConfigResult(config, targetPath, content, opts, meta = {}) {
|
|
|
1255
1251
|
mkdirSync2(dir, { recursive: true });
|
|
1256
1252
|
}
|
|
1257
1253
|
if (previousContent !== null && changed) {
|
|
1258
|
-
const
|
|
1259
|
-
createSnapshot(config.id, previousContent, config.version
|
|
1254
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1255
|
+
await store.createSnapshot(config.id, previousContent, config.version);
|
|
1260
1256
|
}
|
|
1261
1257
|
writeFileSync(path, renderedContent, "utf-8");
|
|
1262
1258
|
}
|
|
@@ -1282,8 +1278,8 @@ async function applyConfig(config, opts = {}) {
|
|
|
1282
1278
|
if (config.kind === "reference" || (!config.target_path || !shouldApplyPrimary) && selectedOutputs.length === 0) {
|
|
1283
1279
|
throw new ConfigApplyError(`Config "${config.name}" is a reference (kind=reference) and has no target_path \u2014 cannot apply to disk.`);
|
|
1284
1280
|
}
|
|
1285
|
-
const
|
|
1286
|
-
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? listConfigs(
|
|
1281
|
+
const store = opts.store ?? resolveConfigStore();
|
|
1282
|
+
const contextConfigs = selectedOutputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
1287
1283
|
if (isGeneratedOutputTarget(config, contextConfigs)) {
|
|
1288
1284
|
throw new ConfigApplyError(`Config "${config.name}" targets a generated output path. Apply the canonical source config instead.`);
|
|
1289
1285
|
}
|
|
@@ -1304,7 +1300,7 @@ async function applyConfig(config, opts = {}) {
|
|
|
1304
1300
|
};
|
|
1305
1301
|
}
|
|
1306
1302
|
if (!opts.dryRun) {
|
|
1307
|
-
updateConfig(config.id, { synced_at:
|
|
1303
|
+
await store.updateConfig(config.id, { synced_at: new Date().toISOString() });
|
|
1308
1304
|
}
|
|
1309
1305
|
return result;
|
|
1310
1306
|
}
|
|
@@ -1420,9 +1416,9 @@ function redactIni(content) {
|
|
|
1420
1416
|
for (let i = 0;i < lines.length; i++) {
|
|
1421
1417
|
const line = lines[i];
|
|
1422
1418
|
const authM = line.match(/^(\/\/[^:]+:_authToken=)(.+)$/);
|
|
1423
|
-
if (authM && !authM[2].
|
|
1424
|
-
redacted.push({ varName: "
|
|
1425
|
-
out.push(`${authM[1]}{
|
|
1419
|
+
if (authM && !isReferenceValue(authM[2].trim())) {
|
|
1420
|
+
redacted.push({ varName: "NPM_TOKEN", line: i + 1, reason: "npm auth token" });
|
|
1421
|
+
out.push(`${authM[1]}\${NPM_TOKEN}`);
|
|
1426
1422
|
continue;
|
|
1427
1423
|
}
|
|
1428
1424
|
const m = line.match(/^(\s*)([a-zA-Z][a-zA-Z0-9_\-]*)(\s*=\s*)(.+?)\s*$/);
|
|
@@ -1462,6 +1458,8 @@ function redactGeneric(content) {
|
|
|
1462
1458
|
function shouldRedactKeyValue(key, value) {
|
|
1463
1459
|
if (!value || value.startsWith("{{"))
|
|
1464
1460
|
return false;
|
|
1461
|
+
if (isReferenceValue(value.trim()))
|
|
1462
|
+
return false;
|
|
1465
1463
|
if (value.length < MIN_SECRET_VALUE_LEN)
|
|
1466
1464
|
return false;
|
|
1467
1465
|
if (/^(true|false|yes|no|on|off|null|undefined|\d+)$/i.test(value))
|
|
@@ -1483,6 +1481,9 @@ function reasonFor(key, value) {
|
|
|
1483
1481
|
}
|
|
1484
1482
|
return "secret value pattern";
|
|
1485
1483
|
}
|
|
1484
|
+
function isReferenceValue(value) {
|
|
1485
|
+
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);
|
|
1486
|
+
}
|
|
1486
1487
|
function redactContent(content, format) {
|
|
1487
1488
|
switch (format) {
|
|
1488
1489
|
case "shell":
|
|
@@ -1529,21 +1530,13 @@ function countBy(items, getValue) {
|
|
|
1529
1530
|
}
|
|
1530
1531
|
return counts;
|
|
1531
1532
|
}
|
|
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()) {
|
|
1533
|
+
async function getConfigsStatus(store = resolveConfigStore()) {
|
|
1541
1534
|
let databaseReachable = true;
|
|
1542
1535
|
let configs = [];
|
|
1543
1536
|
let categoryStats = { total: 0 };
|
|
1544
1537
|
try {
|
|
1545
|
-
configs = listConfigs(
|
|
1546
|
-
categoryStats = getConfigStats(
|
|
1538
|
+
configs = await store.listConfigs();
|
|
1539
|
+
categoryStats = await store.getConfigStats();
|
|
1547
1540
|
} catch {
|
|
1548
1541
|
databaseReachable = false;
|
|
1549
1542
|
}
|
|
@@ -1568,10 +1561,25 @@ function getConfigsStatus(db = getDatabase()) {
|
|
|
1568
1561
|
driftedTargets += 1;
|
|
1569
1562
|
}
|
|
1570
1563
|
}
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1564
|
+
let profiles = 0;
|
|
1565
|
+
let machines = 0;
|
|
1566
|
+
let profileLinks = 0;
|
|
1567
|
+
let snapshots = 0;
|
|
1568
|
+
if (databaseReachable) {
|
|
1569
|
+
try {
|
|
1570
|
+
const profileList = await store.listProfiles();
|
|
1571
|
+
profiles = profileList.length;
|
|
1572
|
+
machines = (await store.listMachines()).length;
|
|
1573
|
+
for (const profile of profileList) {
|
|
1574
|
+
profileLinks += (await store.getProfileConfigs(profile.id)).length;
|
|
1575
|
+
}
|
|
1576
|
+
for (const config of configs) {
|
|
1577
|
+
snapshots += (await store.listSnapshots(config.id)).length;
|
|
1578
|
+
}
|
|
1579
|
+
} catch {
|
|
1580
|
+
databaseReachable = false;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1575
1583
|
const byCategory = Object.fromEntries(Object.entries(categoryStats).filter(([key]) => key !== "total"));
|
|
1576
1584
|
const status = databaseReachable && driftedTargets === 0 && missingTargets === 0 && unredactedSecretFindings === 0 ? "ok" : "warn";
|
|
1577
1585
|
return {
|
|
@@ -1624,6 +1632,69 @@ function getConfigsStatus(db = getDatabase()) {
|
|
|
1624
1632
|
}
|
|
1625
1633
|
};
|
|
1626
1634
|
}
|
|
1635
|
+
// src/db/pg-migrations.ts
|
|
1636
|
+
var PG_MIGRATIONS = [
|
|
1637
|
+
`CREATE TABLE IF NOT EXISTS configs (
|
|
1638
|
+
id TEXT PRIMARY KEY,
|
|
1639
|
+
name TEXT NOT NULL,
|
|
1640
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1641
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
1642
|
+
category TEXT NOT NULL,
|
|
1643
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
1644
|
+
target_path TEXT,
|
|
1645
|
+
outputs TEXT NOT NULL DEFAULT '[]',
|
|
1646
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
1647
|
+
content TEXT NOT NULL DEFAULT '',
|
|
1648
|
+
description TEXT,
|
|
1649
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
1650
|
+
is_template BOOLEAN NOT NULL DEFAULT FALSE,
|
|
1651
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
1652
|
+
created_at TEXT NOT NULL,
|
|
1653
|
+
updated_at TEXT NOT NULL,
|
|
1654
|
+
synced_at TEXT
|
|
1655
|
+
)`,
|
|
1656
|
+
`CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
1657
|
+
id TEXT PRIMARY KEY,
|
|
1658
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1659
|
+
content TEXT NOT NULL,
|
|
1660
|
+
version INTEGER NOT NULL,
|
|
1661
|
+
created_at TEXT NOT NULL
|
|
1662
|
+
)`,
|
|
1663
|
+
`CREATE TABLE IF NOT EXISTS profiles (
|
|
1664
|
+
id TEXT PRIMARY KEY,
|
|
1665
|
+
name TEXT NOT NULL,
|
|
1666
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1667
|
+
description TEXT,
|
|
1668
|
+
selectors TEXT NOT NULL DEFAULT '{}',
|
|
1669
|
+
variables TEXT NOT NULL DEFAULT '{}',
|
|
1670
|
+
created_at TEXT NOT NULL,
|
|
1671
|
+
updated_at TEXT NOT NULL
|
|
1672
|
+
)`,
|
|
1673
|
+
`CREATE TABLE IF NOT EXISTS profile_configs (
|
|
1674
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
1675
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
1676
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
1677
|
+
PRIMARY KEY (profile_id, config_id)
|
|
1678
|
+
)`,
|
|
1679
|
+
`CREATE TABLE IF NOT EXISTS machines (
|
|
1680
|
+
id TEXT PRIMARY KEY,
|
|
1681
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
1682
|
+
os TEXT,
|
|
1683
|
+
arch TEXT,
|
|
1684
|
+
last_applied_at TEXT,
|
|
1685
|
+
created_at TEXT NOT NULL
|
|
1686
|
+
)`,
|
|
1687
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1688
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1689
|
+
message TEXT NOT NULL,
|
|
1690
|
+
email TEXT,
|
|
1691
|
+
category TEXT DEFAULT 'general',
|
|
1692
|
+
version TEXT,
|
|
1693
|
+
machine_id TEXT,
|
|
1694
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1695
|
+
)`,
|
|
1696
|
+
`ALTER TABLE configs ADD COLUMN IF NOT EXISTS outputs TEXT NOT NULL DEFAULT '[]'`
|
|
1697
|
+
];
|
|
1627
1698
|
// src/lib/session-render.ts
|
|
1628
1699
|
import { createHash } from "crypto";
|
|
1629
1700
|
import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync2, statSync as statSync2 } from "fs";
|
|
@@ -2501,14 +2572,14 @@ function asStringArray(value) {
|
|
|
2501
2572
|
return value.filter((item) => typeof item === "string");
|
|
2502
2573
|
}
|
|
2503
2574
|
// src/lib/session-apply.ts
|
|
2504
|
-
import { createHash as createHash2, randomUUID as
|
|
2575
|
+
import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
|
|
2505
2576
|
import {
|
|
2506
2577
|
existsSync as existsSync6,
|
|
2507
2578
|
lstatSync,
|
|
2508
2579
|
mkdirSync as mkdirSync3,
|
|
2509
2580
|
readFileSync as readFileSync4,
|
|
2510
2581
|
renameSync,
|
|
2511
|
-
rmSync,
|
|
2582
|
+
rmSync as rmSync2,
|
|
2512
2583
|
writeFileSync as writeFileSync2
|
|
2513
2584
|
} from "fs";
|
|
2514
2585
|
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join4, parse as parse2, relative as relative2, resolve as resolve3 } from "path";
|
|
@@ -2562,7 +2633,7 @@ function applySessionRender(plan, options = {}) {
|
|
|
2562
2633
|
continue;
|
|
2563
2634
|
assertNoSymlinkSegments(targetHome, result.path);
|
|
2564
2635
|
if (existsSync6(result.path))
|
|
2565
|
-
|
|
2636
|
+
rmSync2(result.path);
|
|
2566
2637
|
}
|
|
2567
2638
|
}
|
|
2568
2639
|
return {
|
|
@@ -2796,7 +2867,7 @@ function writePlannedFile(path, content, targetHome) {
|
|
|
2796
2867
|
const dir = dirname3(path);
|
|
2797
2868
|
mkdirSync3(dir, { recursive: true });
|
|
2798
2869
|
assertNoSymlinkSegments(targetHome, path);
|
|
2799
|
-
const tmp = join4(dir, `.session-${
|
|
2870
|
+
const tmp = join4(dir, `.session-${randomUUID3()}.tmp`);
|
|
2800
2871
|
writeFileSync2(tmp, content, "utf-8");
|
|
2801
2872
|
renameSync(tmp, path);
|
|
2802
2873
|
}
|
|
@@ -2814,7 +2885,7 @@ function writeSessionSnapshot(plan, targetHome, manifestPath, results, previousM
|
|
|
2814
2885
|
if (!previousManifest && existingFiles.length === 0)
|
|
2815
2886
|
return null;
|
|
2816
2887
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2817
|
-
const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${
|
|
2888
|
+
const snapshotPath = resolve3(targetHome, ".hasna", "session-render-snapshots", `${timestamp}-${randomUUID3()}.json`);
|
|
2818
2889
|
const snapshot = {
|
|
2819
2890
|
schema: "hasna.configs.session-render-snapshot/v1",
|
|
2820
2891
|
createdAt: new Date().toISOString(),
|
|
@@ -2948,7 +3019,7 @@ ids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax
|
|
|
2948
3019
|
ids, passport numbers, credentials, and contract clauses unless an explicit
|
|
2949
3020
|
approved storage policy exists.
|
|
2950
3021
|
`;
|
|
2951
|
-
function ensureProjectDashboardStandardConfig(
|
|
3022
|
+
async function ensureProjectDashboardStandardConfig(store = resolveConfigStore()) {
|
|
2952
3023
|
const input = {
|
|
2953
3024
|
name: "Agent Managed Project Dashboard Standard",
|
|
2954
3025
|
category: "workspace",
|
|
@@ -2960,17 +3031,21 @@ function ensureProjectDashboardStandardConfig(db) {
|
|
|
2960
3031
|
tags: ["projects-dashboard", "agent-projects", "json-render"]
|
|
2961
3032
|
};
|
|
2962
3033
|
try {
|
|
2963
|
-
const existing = getConfig(PROJECT_DASHBOARD_STANDARD_SLUG
|
|
3034
|
+
const existing = await store.getConfig(PROJECT_DASHBOARD_STANDARD_SLUG);
|
|
2964
3035
|
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
|
|
3036
|
+
return await store.updateConfig(existing.id, input);
|
|
2966
3037
|
}
|
|
2967
3038
|
return existing;
|
|
2968
3039
|
} catch {
|
|
2969
|
-
return createConfig(input
|
|
3040
|
+
return await store.createConfig(input);
|
|
2970
3041
|
}
|
|
2971
3042
|
}
|
|
2972
3043
|
|
|
2973
3044
|
// src/lib/platform-profiles.ts
|
|
3045
|
+
function profileHasSelectors2(profile) {
|
|
3046
|
+
const selectors = profile.selectors ?? {};
|
|
3047
|
+
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
3048
|
+
}
|
|
2974
3049
|
var PLATFORM_PROFILE_PRESETS = [
|
|
2975
3050
|
{
|
|
2976
3051
|
name: "linux-arm64",
|
|
@@ -2997,25 +3072,25 @@ var PLATFORM_PROFILE_PRESETS = [
|
|
|
2997
3072
|
}
|
|
2998
3073
|
}
|
|
2999
3074
|
];
|
|
3000
|
-
function ensurePlatformProfiles(
|
|
3001
|
-
const configs = listConfigs(
|
|
3075
|
+
async function ensurePlatformProfiles(store = resolveConfigStore()) {
|
|
3076
|
+
const configs = await store.listConfigs();
|
|
3002
3077
|
const ensured = [];
|
|
3003
3078
|
for (const preset of PLATFORM_PROFILE_PRESETS) {
|
|
3004
3079
|
let profile;
|
|
3005
3080
|
try {
|
|
3006
|
-
profile = getProfile(preset.name
|
|
3007
|
-
if (!
|
|
3008
|
-
profile = updateProfile(profile.id, {
|
|
3081
|
+
profile = await store.getProfile(preset.name);
|
|
3082
|
+
if (!profileHasSelectors2(profile) || Object.keys(profile.variables).length === 0) {
|
|
3083
|
+
profile = await store.updateProfile(profile.id, {
|
|
3009
3084
|
description: profile.description ?? preset.description,
|
|
3010
|
-
selectors:
|
|
3085
|
+
selectors: profileHasSelectors2(profile) ? profile.selectors : preset.selectors,
|
|
3011
3086
|
variables: Object.keys(profile.variables).length > 0 ? profile.variables : preset.variables
|
|
3012
|
-
}
|
|
3087
|
+
});
|
|
3013
3088
|
}
|
|
3014
3089
|
} catch {
|
|
3015
|
-
profile = createProfile(preset
|
|
3090
|
+
profile = await store.createProfile(preset);
|
|
3016
3091
|
}
|
|
3017
3092
|
for (const config of configs) {
|
|
3018
|
-
addConfigToProfile(profile.id, config.id
|
|
3093
|
+
await store.addConfigToProfile(profile.id, config.id);
|
|
3019
3094
|
}
|
|
3020
3095
|
ensured.push(profile);
|
|
3021
3096
|
}
|
|
@@ -3034,14 +3109,14 @@ function shouldSkip(p) {
|
|
|
3034
3109
|
return SKIP.some((s) => p.includes(s));
|
|
3035
3110
|
}
|
|
3036
3111
|
async function syncFromDir(dir, opts = {}) {
|
|
3037
|
-
const
|
|
3112
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3038
3113
|
const absDir = expandPath(dir);
|
|
3039
3114
|
if (!existsSync7(absDir))
|
|
3040
3115
|
return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
|
|
3041
3116
|
const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join5(absDir, f)).filter((f) => statSync3(f).isFile());
|
|
3042
3117
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3043
3118
|
const home = homedir4();
|
|
3044
|
-
const allConfigs = listConfigs(
|
|
3119
|
+
const allConfigs = await store.listConfigs();
|
|
3045
3120
|
for (const file of files) {
|
|
3046
3121
|
if (shouldSkip(file)) {
|
|
3047
3122
|
result.skipped.push(file);
|
|
@@ -3057,11 +3132,11 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
3057
3132
|
const existing = allConfigs.find((c) => c.target_path === targetPath);
|
|
3058
3133
|
if (!existing) {
|
|
3059
3134
|
if (!opts.dryRun)
|
|
3060
|
-
createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content }
|
|
3135
|
+
await store.createConfig({ name: relative3(absDir, file), category: detectCategory(file), agent: detectAgent(file), target_path: targetPath, format: detectFormat(file), content });
|
|
3061
3136
|
result.added++;
|
|
3062
3137
|
} else if (existing.content !== content) {
|
|
3063
3138
|
if (!opts.dryRun)
|
|
3064
|
-
updateConfig(existing.id, { content }
|
|
3139
|
+
await store.updateConfig(existing.id, { content });
|
|
3065
3140
|
result.updated++;
|
|
3066
3141
|
} else {
|
|
3067
3142
|
result.unchanged++;
|
|
@@ -3073,17 +3148,17 @@ async function syncFromDir(dir, opts = {}) {
|
|
|
3073
3148
|
return result;
|
|
3074
3149
|
}
|
|
3075
3150
|
async function syncToDir(dir, opts = {}) {
|
|
3076
|
-
const
|
|
3151
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3077
3152
|
const home = homedir4();
|
|
3078
3153
|
const absDir = expandPath(dir);
|
|
3079
3154
|
const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
|
|
3080
|
-
const configs = listConfigs(
|
|
3155
|
+
const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
|
|
3081
3156
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3082
3157
|
for (const config of configs) {
|
|
3083
3158
|
if (config.kind === "reference")
|
|
3084
3159
|
continue;
|
|
3085
3160
|
try {
|
|
3086
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
3161
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store });
|
|
3087
3162
|
r.changed ? result.updated++ : result.unchanged++;
|
|
3088
3163
|
} catch {
|
|
3089
3164
|
result.skipped.push(config.target_path || config.id);
|
|
@@ -3201,11 +3276,11 @@ var PROJECT_CONFIG_FILES = [
|
|
|
3201
3276
|
{ file: ".cursor/mcp.json", category: "mcp", agent: "cursor", format: "json" }
|
|
3202
3277
|
];
|
|
3203
3278
|
async function syncProject(opts) {
|
|
3204
|
-
const
|
|
3279
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3205
3280
|
const absDir = expandPath(opts.projectDir);
|
|
3206
3281
|
const projectName = absDir.split("/").pop() || "project";
|
|
3207
3282
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3208
|
-
const allConfigs = listConfigs(
|
|
3283
|
+
const allConfigs = await store.listConfigs();
|
|
3209
3284
|
const machine = detectMachineContext();
|
|
3210
3285
|
for (const pf of PROJECT_CONFIG_FILES) {
|
|
3211
3286
|
const abs = join6(absDir, pf.file);
|
|
@@ -3227,11 +3302,11 @@ async function syncProject(opts) {
|
|
|
3227
3302
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
|
|
3228
3303
|
if (!existing) {
|
|
3229
3304
|
if (!opts.dryRun)
|
|
3230
|
-
createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 }
|
|
3305
|
+
await store.createConfig({ name, category: pf.category, agent: pf.agent, format: pf.format, content, target_path: targetPath, is_template: isTemplate2 });
|
|
3231
3306
|
result.added++;
|
|
3232
3307
|
} else if (existing.content !== content) {
|
|
3233
3308
|
if (!opts.dryRun)
|
|
3234
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
3309
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
3235
3310
|
result.updated++;
|
|
3236
3311
|
} else {
|
|
3237
3312
|
result.unchanged++;
|
|
@@ -3256,11 +3331,11 @@ async function syncProject(opts) {
|
|
|
3256
3331
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === slug2);
|
|
3257
3332
|
if (!existing) {
|
|
3258
3333
|
if (!opts.dryRun)
|
|
3259
|
-
createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 }
|
|
3334
|
+
await store.createConfig({ name, category: "rules", agent: "claude", format: "markdown", content, target_path: targetPath, is_template: isTemplate2 });
|
|
3260
3335
|
result.added++;
|
|
3261
3336
|
} else if (existing.content !== content) {
|
|
3262
3337
|
if (!opts.dryRun)
|
|
3263
|
-
updateConfig(existing.id, { content, is_template: isTemplate2 }
|
|
3338
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2 });
|
|
3264
3339
|
result.updated++;
|
|
3265
3340
|
} else {
|
|
3266
3341
|
result.unchanged++;
|
|
@@ -3270,7 +3345,7 @@ async function syncProject(opts) {
|
|
|
3270
3345
|
return result;
|
|
3271
3346
|
}
|
|
3272
3347
|
async function syncKnown(opts = {}) {
|
|
3273
|
-
const
|
|
3348
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3274
3349
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3275
3350
|
const home = getConfigHome();
|
|
3276
3351
|
const machine = detectMachineContext();
|
|
@@ -3279,7 +3354,7 @@ async function syncKnown(opts = {}) {
|
|
|
3279
3354
|
targets = targets.filter((k) => k.agent === opts.agent);
|
|
3280
3355
|
if (opts.category)
|
|
3281
3356
|
targets = targets.filter((k) => k.category === opts.category);
|
|
3282
|
-
const allConfigs = listConfigs(
|
|
3357
|
+
const allConfigs = await store.listConfigs();
|
|
3283
3358
|
const existingOutputOwners = outputOwnerIdsByTarget(allConfigs);
|
|
3284
3359
|
for (const known of targets) {
|
|
3285
3360
|
if (known.rulesDir) {
|
|
@@ -3308,15 +3383,15 @@ async function syncKnown(opts = {}) {
|
|
|
3308
3383
|
const outputs = known.agent === "claude" ? claudeRuleOutputs(f) : known.outputs;
|
|
3309
3384
|
if (!existing) {
|
|
3310
3385
|
if (!opts.dryRun)
|
|
3311
|
-
createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs }
|
|
3386
|
+
await store.createConfig({ name, category: known.category, agent: known.agent, format: "markdown", content, target_path: targetPath, is_template: isTemplate2, outputs });
|
|
3312
3387
|
result.added++;
|
|
3313
3388
|
} else if (existing.content !== content) {
|
|
3314
3389
|
if (!opts.dryRun)
|
|
3315
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs }
|
|
3390
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs });
|
|
3316
3391
|
result.updated++;
|
|
3317
3392
|
} else if (!outputsEqual(existing.outputs, outputs)) {
|
|
3318
3393
|
if (!opts.dryRun)
|
|
3319
|
-
updateConfig(existing.id, { outputs }
|
|
3394
|
+
await store.updateConfig(existing.id, { outputs });
|
|
3320
3395
|
result.updated++;
|
|
3321
3396
|
} else {
|
|
3322
3397
|
result.unchanged++;
|
|
@@ -3348,7 +3423,7 @@ async function syncKnown(opts = {}) {
|
|
|
3348
3423
|
const existing = allConfigs.find((c) => c.target_path === targetPath || c.slug === known.name);
|
|
3349
3424
|
if (!existing) {
|
|
3350
3425
|
if (!opts.dryRun) {
|
|
3351
|
-
createConfig({
|
|
3426
|
+
await store.createConfig({
|
|
3352
3427
|
name: known.name,
|
|
3353
3428
|
category: known.category,
|
|
3354
3429
|
agent: known.agent,
|
|
@@ -3359,16 +3434,16 @@ async function syncKnown(opts = {}) {
|
|
|
3359
3434
|
description: known.description,
|
|
3360
3435
|
is_template: isTemplate2,
|
|
3361
3436
|
outputs: known.outputs
|
|
3362
|
-
}
|
|
3437
|
+
});
|
|
3363
3438
|
}
|
|
3364
3439
|
result.added++;
|
|
3365
3440
|
} else if (existing.content !== content) {
|
|
3366
3441
|
if (!opts.dryRun)
|
|
3367
|
-
updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs }
|
|
3442
|
+
await store.updateConfig(existing.id, { content, is_template: isTemplate2, outputs: known.outputs });
|
|
3368
3443
|
result.updated++;
|
|
3369
3444
|
} else if (!outputsEqual(existing.outputs, known.outputs)) {
|
|
3370
3445
|
if (!opts.dryRun)
|
|
3371
|
-
updateConfig(existing.id, { outputs: known.outputs }
|
|
3446
|
+
await store.updateConfig(existing.id, { outputs: known.outputs });
|
|
3372
3447
|
result.updated++;
|
|
3373
3448
|
} else {
|
|
3374
3449
|
result.unchanged++;
|
|
@@ -3380,9 +3455,9 @@ async function syncKnown(opts = {}) {
|
|
|
3380
3455
|
return result;
|
|
3381
3456
|
}
|
|
3382
3457
|
async function syncToDisk(opts = {}) {
|
|
3383
|
-
const
|
|
3458
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3384
3459
|
const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
|
|
3385
|
-
const allFileConfigs = listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} }
|
|
3460
|
+
const allFileConfigs = await store.listConfigs({ kind: "file", ...opts.category ? { category: opts.category } : {} });
|
|
3386
3461
|
const outputOwners = outputOwnerIdsByTarget(allFileConfigs);
|
|
3387
3462
|
let configs = allFileConfigs.filter((config) => {
|
|
3388
3463
|
return !isGeneratedOutputTarget2(config, outputOwners);
|
|
@@ -3395,7 +3470,7 @@ async function syncToDisk(opts = {}) {
|
|
|
3395
3470
|
if (!config.target_path && config.outputs.length === 0)
|
|
3396
3471
|
continue;
|
|
3397
3472
|
try {
|
|
3398
|
-
const r = await applyConfig(config, { dryRun: opts.dryRun,
|
|
3473
|
+
const r = await applyConfig(config, { dryRun: opts.dryRun, store, outputAgent: opts.agent });
|
|
3399
3474
|
r.changed ? result.updated++ : result.unchanged++;
|
|
3400
3475
|
} catch {
|
|
3401
3476
|
result.skipped.push(config.target_path ?? config.id);
|
|
@@ -3432,12 +3507,12 @@ function buildDiff(expectedContent, targetPath) {
|
|
|
3432
3507
|
return lines.join(`
|
|
3433
3508
|
`);
|
|
3434
3509
|
}
|
|
3435
|
-
function diffConfig(config, opts = {}) {
|
|
3510
|
+
async function diffConfig(config, opts = {}) {
|
|
3436
3511
|
if (!config.target_path && config.outputs.length === 0)
|
|
3437
3512
|
return "(reference \u2014 no target path)";
|
|
3438
3513
|
const diffs = [];
|
|
3439
|
-
const
|
|
3440
|
-
const contextConfigs = config.outputs.length > 0 || config.target_path ? listConfigs(
|
|
3514
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3515
|
+
const contextConfigs = config.outputs.length > 0 || config.target_path ? await store.listConfigs() : [config];
|
|
3441
3516
|
if (isGeneratedOutputTarget2(config, outputOwnerIdsByTarget(contextConfigs))) {
|
|
3442
3517
|
return "(generated output \u2014 managed by fan-out)";
|
|
3443
3518
|
}
|
|
@@ -3515,12 +3590,12 @@ function detectFormat(filePath) {
|
|
|
3515
3590
|
return "text";
|
|
3516
3591
|
}
|
|
3517
3592
|
// src/lib/export.ts
|
|
3518
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as
|
|
3593
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
3519
3594
|
import { join as join7, resolve as resolve4 } from "path";
|
|
3520
3595
|
import { tmpdir } from "os";
|
|
3521
3596
|
async function exportConfigs(outputPath, opts = {}) {
|
|
3522
|
-
const
|
|
3523
|
-
const configs = listConfigs(opts.filter
|
|
3597
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3598
|
+
const configs = await store.listConfigs(opts.filter);
|
|
3524
3599
|
const absOutput = resolve4(outputPath);
|
|
3525
3600
|
const tmpDir = join7(tmpdir(), `configs-export-${Date.now()}`);
|
|
3526
3601
|
const contentsDir = join7(tmpDir, "contents");
|
|
@@ -3528,7 +3603,7 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
3528
3603
|
mkdirSync4(contentsDir, { recursive: true });
|
|
3529
3604
|
const manifest = {
|
|
3530
3605
|
version: "1.0.0",
|
|
3531
|
-
exported_at:
|
|
3606
|
+
exported_at: new Date().toISOString(),
|
|
3532
3607
|
configs: configs.map(({ content: _content, ...meta }) => meta)
|
|
3533
3608
|
};
|
|
3534
3609
|
writeFileSync3(join7(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
@@ -3548,16 +3623,16 @@ async function exportConfigs(outputPath, opts = {}) {
|
|
|
3548
3623
|
return { path: absOutput, count: configs.length };
|
|
3549
3624
|
} finally {
|
|
3550
3625
|
if (existsSync9(tmpDir)) {
|
|
3551
|
-
|
|
3626
|
+
rmSync3(tmpDir, { recursive: true, force: true });
|
|
3552
3627
|
}
|
|
3553
3628
|
}
|
|
3554
3629
|
}
|
|
3555
3630
|
// src/lib/import.ts
|
|
3556
|
-
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as
|
|
3631
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync5, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
|
|
3557
3632
|
import { join as join8, resolve as resolve5 } from "path";
|
|
3558
3633
|
import { tmpdir as tmpdir2 } from "os";
|
|
3559
3634
|
async function importConfigs(bundlePath, opts = {}) {
|
|
3560
|
-
const
|
|
3635
|
+
const store = opts.store ?? resolveConfigStore();
|
|
3561
3636
|
const conflict = opts.conflict ?? "skip";
|
|
3562
3637
|
const absPath = resolve5(bundlePath);
|
|
3563
3638
|
const tmpDir = join8(tmpdir2(), `configs-import-${Date.now()}`);
|
|
@@ -3584,17 +3659,17 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3584
3659
|
const content = existsSync10(contentFile) ? readFileSync7(contentFile, "utf-8") : "";
|
|
3585
3660
|
let existing = null;
|
|
3586
3661
|
try {
|
|
3587
|
-
existing = getConfig(meta.slug
|
|
3662
|
+
existing = await store.getConfig(meta.slug);
|
|
3588
3663
|
} catch {}
|
|
3589
3664
|
if (existing) {
|
|
3590
3665
|
if (conflict === "skip") {
|
|
3591
3666
|
result.skipped++;
|
|
3592
3667
|
} else if (conflict === "overwrite" || conflict === "version") {
|
|
3593
|
-
updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs }
|
|
3668
|
+
await store.updateConfig(existing.id, { content, description: meta.description ?? undefined, tags: meta.tags, outputs: meta.outputs });
|
|
3594
3669
|
result.updated++;
|
|
3595
3670
|
}
|
|
3596
3671
|
} else {
|
|
3597
|
-
createConfig({
|
|
3672
|
+
await store.createConfig({
|
|
3598
3673
|
name: meta.name,
|
|
3599
3674
|
kind: meta.kind,
|
|
3600
3675
|
category: meta.category,
|
|
@@ -3606,7 +3681,7 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3606
3681
|
description: meta.description ?? undefined,
|
|
3607
3682
|
tags: meta.tags,
|
|
3608
3683
|
is_template: meta.is_template
|
|
3609
|
-
}
|
|
3684
|
+
});
|
|
3610
3685
|
result.created++;
|
|
3611
3686
|
}
|
|
3612
3687
|
} catch (err) {
|
|
@@ -3616,15 +3691,397 @@ async function importConfigs(bundlePath, opts = {}) {
|
|
|
3616
3691
|
return result;
|
|
3617
3692
|
} finally {
|
|
3618
3693
|
if (existsSync10(tmpDir)) {
|
|
3619
|
-
|
|
3694
|
+
rmSync4(tmpDir, { recursive: true, force: true });
|
|
3695
|
+
}
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
// src/lib/package-manager-guard.ts
|
|
3699
|
+
import { execFileSync } from "child_process";
|
|
3700
|
+
import { existsSync as existsSync11, lstatSync as lstatSync2, readdirSync as readdirSync3, readFileSync as readFileSync8 } from "fs";
|
|
3701
|
+
import { homedir as homedir5 } from "os";
|
|
3702
|
+
import { basename as basename5, dirname as dirname4, isAbsolute as isAbsolute3, join as join9, relative as relative4, resolve as resolve6 } from "path";
|
|
3703
|
+
var SKIP_DIRS = new Set([
|
|
3704
|
+
".git",
|
|
3705
|
+
"node_modules",
|
|
3706
|
+
"dist",
|
|
3707
|
+
"build",
|
|
3708
|
+
"coverage",
|
|
3709
|
+
".next",
|
|
3710
|
+
".turbo",
|
|
3711
|
+
".cache"
|
|
3712
|
+
]);
|
|
3713
|
+
var LOCKFILE_NAMES = new Set([
|
|
3714
|
+
"bun.lock",
|
|
3715
|
+
"package-lock.json",
|
|
3716
|
+
"npm-shrinkwrap.json",
|
|
3717
|
+
"pnpm-lock.yaml",
|
|
3718
|
+
"yarn.lock"
|
|
3719
|
+
]);
|
|
3720
|
+
var HOME_FILES = [
|
|
3721
|
+
".npmrc",
|
|
3722
|
+
".bunfig.toml",
|
|
3723
|
+
"bunfig.toml",
|
|
3724
|
+
".bashrc",
|
|
3725
|
+
".bash_profile",
|
|
3726
|
+
".zshrc",
|
|
3727
|
+
".zprofile",
|
|
3728
|
+
".profile"
|
|
3729
|
+
];
|
|
3730
|
+
var TOKEN_VALUE_PATTERNS = [
|
|
3731
|
+
{ re: /npm_[A-Za-z0-9]{36,}/, rule: "literal-npm-token", detail: "literal npm token-like value" },
|
|
3732
|
+
{ re: /gh[pousr]_[A-Za-z0-9_]{36,}/, rule: "literal-github-token", detail: "literal GitHub token-like value" },
|
|
3733
|
+
{ re: /sk-ant-[A-Za-z0-9\-_]{40,}/, rule: "literal-anthropic-key", detail: "literal Anthropic key-like value" },
|
|
3734
|
+
{ re: /sk-[A-Za-z0-9]{48,}/, rule: "literal-openai-key", detail: "literal OpenAI key-like value" },
|
|
3735
|
+
{ re: /AKIA[0-9A-Z]{16}/, rule: "literal-aws-access-key", detail: "literal AWS access-key-like value" },
|
|
3736
|
+
{ re: /xoxb-[0-9]+-[A-Za-z0-9-]+/, rule: "literal-slack-token", detail: "literal Slack token-like value" }
|
|
3737
|
+
];
|
|
3738
|
+
function scanPackageManagerSecrets(options = {}) {
|
|
3739
|
+
const cwd = options.cwd ? resolve6(options.cwd) : process.cwd();
|
|
3740
|
+
const roots = (options.roots && options.roots.length > 0 ? options.roots : [cwd]).map((root) => resolve6(cwd, root));
|
|
3741
|
+
const findings = [];
|
|
3742
|
+
let scannedFiles = 0;
|
|
3743
|
+
for (const root of roots) {
|
|
3744
|
+
if (!existsSync11(root))
|
|
3745
|
+
continue;
|
|
3746
|
+
const stat = lstatSync2(root);
|
|
3747
|
+
if (stat.isFile()) {
|
|
3748
|
+
if (!shouldScanRepoFile(root))
|
|
3749
|
+
continue;
|
|
3750
|
+
const text = readTextFile(root);
|
|
3751
|
+
if (text === null)
|
|
3752
|
+
continue;
|
|
3753
|
+
scannedFiles++;
|
|
3754
|
+
findings.push(...scanFile(root, text, classifyRepoFile(root), isTrackedFile(root), dirname4(root)));
|
|
3755
|
+
continue;
|
|
3756
|
+
}
|
|
3757
|
+
if (!stat.isDirectory())
|
|
3758
|
+
continue;
|
|
3759
|
+
const tracked = trackedFiles(root);
|
|
3760
|
+
for (const file of collectRepoFiles(root)) {
|
|
3761
|
+
const rel = toPosix(relative4(root, file));
|
|
3762
|
+
const isTracked = tracked.has(rel);
|
|
3763
|
+
const text = readTextFile(file);
|
|
3764
|
+
if (text === null)
|
|
3765
|
+
continue;
|
|
3766
|
+
scannedFiles++;
|
|
3767
|
+
findings.push(...scanFile(file, text, classifyRepoFile(file), isTracked, root));
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
if (options.includeHome) {
|
|
3771
|
+
const home = homedir5();
|
|
3772
|
+
for (const name of HOME_FILES) {
|
|
3773
|
+
const file = join9(home, name);
|
|
3774
|
+
if (!existsSync11(file))
|
|
3775
|
+
continue;
|
|
3776
|
+
const text = readTextFile(file);
|
|
3777
|
+
if (text === null)
|
|
3778
|
+
continue;
|
|
3779
|
+
scannedFiles++;
|
|
3780
|
+
findings.push(...scanFile(file, text, classifyHomeFile(name), false, home));
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
findings.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line || a.rule.localeCompare(b.rule));
|
|
3784
|
+
return {
|
|
3785
|
+
clean: findings.length === 0,
|
|
3786
|
+
scannedFiles,
|
|
3787
|
+
scannedRoots: roots,
|
|
3788
|
+
findings
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
function collectRepoFiles(root) {
|
|
3792
|
+
const out = [];
|
|
3793
|
+
const visit = (dir) => {
|
|
3794
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
3795
|
+
if (entry.isDirectory()) {
|
|
3796
|
+
if (SKIP_DIRS.has(entry.name))
|
|
3797
|
+
continue;
|
|
3798
|
+
visit(join9(dir, entry.name));
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
if (!entry.isFile())
|
|
3802
|
+
continue;
|
|
3803
|
+
const file = join9(dir, entry.name);
|
|
3804
|
+
if (shouldScanRepoFile(file))
|
|
3805
|
+
out.push(file);
|
|
3806
|
+
}
|
|
3807
|
+
};
|
|
3808
|
+
visit(root);
|
|
3809
|
+
return out;
|
|
3810
|
+
}
|
|
3811
|
+
function shouldScanRepoFile(file) {
|
|
3812
|
+
const name = basename5(file);
|
|
3813
|
+
return isNpmrcName(name) || isBunConfigName(name) || LOCKFILE_NAMES.has(name);
|
|
3814
|
+
}
|
|
3815
|
+
function classifyRepoFile(file) {
|
|
3816
|
+
const name = basename5(file);
|
|
3817
|
+
if (isNpmrcName(name))
|
|
3818
|
+
return "repo-npmrc";
|
|
3819
|
+
if (isBunConfigName(name))
|
|
3820
|
+
return "bun-config";
|
|
3821
|
+
return "lockfile";
|
|
3822
|
+
}
|
|
3823
|
+
function classifyHomeFile(name) {
|
|
3824
|
+
if (name === ".npmrc")
|
|
3825
|
+
return "home-npmrc";
|
|
3826
|
+
if (isBunConfigName(name))
|
|
3827
|
+
return "bun-config";
|
|
3828
|
+
return "shell-profile";
|
|
3829
|
+
}
|
|
3830
|
+
function isBunConfigName(name) {
|
|
3831
|
+
return name === "bunfig.toml" || name === ".bunfig.toml";
|
|
3832
|
+
}
|
|
3833
|
+
function isNpmrcName(name) {
|
|
3834
|
+
return name === ".npmrc" || name.startsWith(".npmrc.") || name.endsWith(".npmrc");
|
|
3835
|
+
}
|
|
3836
|
+
function readTextFile(file) {
|
|
3837
|
+
try {
|
|
3838
|
+
const stat = lstatSync2(file);
|
|
3839
|
+
if (!stat.isFile() || stat.size > 5000000)
|
|
3840
|
+
return null;
|
|
3841
|
+
const buf = readFileSync8(file);
|
|
3842
|
+
if (buf.includes(0))
|
|
3843
|
+
return null;
|
|
3844
|
+
return buf.toString("utf-8");
|
|
3845
|
+
} catch {
|
|
3846
|
+
return null;
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
function scanFile(file, text, surface, tracked, root) {
|
|
3850
|
+
const findings = [];
|
|
3851
|
+
const path = displayPath(file, root);
|
|
3852
|
+
if (surface === "bun-config")
|
|
3853
|
+
return scanBunConfigFile(text, path, tracked);
|
|
3854
|
+
const lines = text.split(/\r?\n/);
|
|
3855
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3856
|
+
const line = lines[i];
|
|
3857
|
+
const lineNo = i + 1;
|
|
3858
|
+
if (surface === "repo-npmrc" || surface === "home-npmrc") {
|
|
3859
|
+
findings.push(...scanNpmrcLine(line, path, lineNo, surface, tracked));
|
|
3860
|
+
} else if (surface === "shell-profile") {
|
|
3861
|
+
findings.push(...scanShellProfileLine(line, path, lineNo, tracked));
|
|
3862
|
+
} else {
|
|
3863
|
+
findings.push(...scanLockfileLine(line, path, lineNo, tracked));
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
return findings;
|
|
3867
|
+
}
|
|
3868
|
+
function scanNpmrcLine(lineText, path, line, surface, tracked) {
|
|
3869
|
+
const findings = [];
|
|
3870
|
+
const stripped = lineText.trim();
|
|
3871
|
+
if (stripped === "" || stripped.startsWith("#") || stripped.startsWith(";"))
|
|
3872
|
+
return findings;
|
|
3873
|
+
const auth = stripped.match(/(?:^|:)(_[A-Za-z]*(?:auth|password)[A-Za-z]*|password)\s*=\s*(.+)$/i);
|
|
3874
|
+
if (auth) {
|
|
3875
|
+
const value = stripQuotes(stripInlineComment(auth[2].trim()));
|
|
3876
|
+
if (value && !isSafeReference(value)) {
|
|
3877
|
+
findings.push({
|
|
3878
|
+
path,
|
|
3879
|
+
line,
|
|
3880
|
+
rule: "npmrc-literal-auth",
|
|
3881
|
+
surface,
|
|
3882
|
+
severity: "error",
|
|
3883
|
+
tracked,
|
|
3884
|
+
detail: tracked ? "tracked npm auth entry uses a literal value" : "npm auth entry uses a literal value"
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
findings.push(...scanCredentialedUrl(stripped, path, line, surface, tracked));
|
|
3889
|
+
findings.push(...scanKnownTokenPatterns(stripped, path, line, surface, tracked));
|
|
3890
|
+
return findings;
|
|
3891
|
+
}
|
|
3892
|
+
function scanBunConfigFile(text, path, tracked) {
|
|
3893
|
+
const findings = [];
|
|
3894
|
+
const lines = text.split(/\r?\n/);
|
|
3895
|
+
let inReleaseAgeExcludes = false;
|
|
3896
|
+
let hasMinimumReleaseAge = false;
|
|
3897
|
+
for (let i = 0;i < lines.length; i++) {
|
|
3898
|
+
const lineText = lines[i];
|
|
3899
|
+
const line = i + 1;
|
|
3900
|
+
const stripped = lineText.trim();
|
|
3901
|
+
if (stripped === "" || stripped.startsWith("#"))
|
|
3902
|
+
continue;
|
|
3903
|
+
const releaseAge = stripped.match(/^minimumReleaseAge\s*=\s*(?:"([^"]+)"|'([^']+)'|([0-9]+))\s*(?:#.*)?$/i);
|
|
3904
|
+
if (releaseAge) {
|
|
3905
|
+
hasMinimumReleaseAge = true;
|
|
3906
|
+
const rawValue = releaseAge[1] ?? releaseAge[2] ?? releaseAge[3] ?? "";
|
|
3907
|
+
const value = Number(rawValue);
|
|
3908
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
3909
|
+
findings.push({
|
|
3910
|
+
path,
|
|
3911
|
+
line,
|
|
3912
|
+
rule: "bun-release-age-disabled",
|
|
3913
|
+
surface: "bun-config",
|
|
3914
|
+
severity: "error",
|
|
3915
|
+
tracked,
|
|
3916
|
+
detail: "Bun release-age quarantine is disabled"
|
|
3917
|
+
});
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
const startsReleaseAgeExcludes = /minimumReleaseAgeExcludes/i.test(stripped);
|
|
3921
|
+
const scanExcludes = startsReleaseAgeExcludes || inReleaseAgeExcludes;
|
|
3922
|
+
if (scanExcludes) {
|
|
3923
|
+
const quoted = [...stripped.matchAll(/["']([^"']+)["']/g)].map((m) => m[1]);
|
|
3924
|
+
for (const item of quoted) {
|
|
3925
|
+
if (!isExactHasnaPackageName(item)) {
|
|
3926
|
+
findings.push({
|
|
3927
|
+
path,
|
|
3928
|
+
line,
|
|
3929
|
+
rule: "bun-release-age-broad-exclude",
|
|
3930
|
+
surface: "bun-config",
|
|
3931
|
+
severity: "error",
|
|
3932
|
+
tracked,
|
|
3933
|
+
detail: "Bun release-age exclude must be an exact @hasna package name"
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
inReleaseAgeExcludes = startsReleaseAgeExcludes ? stripped.includes("[") && !stripped.includes("]") : inReleaseAgeExcludes && !stripped.includes("]");
|
|
3939
|
+
findings.push(...scanKnownTokenPatterns(stripped, path, line, "bun-config", tracked));
|
|
3940
|
+
}
|
|
3941
|
+
if (!hasMinimumReleaseAge) {
|
|
3942
|
+
findings.push({
|
|
3943
|
+
path,
|
|
3944
|
+
line: 1,
|
|
3945
|
+
rule: "bun-release-age-missing",
|
|
3946
|
+
surface: "bun-config",
|
|
3947
|
+
severity: "error",
|
|
3948
|
+
tracked,
|
|
3949
|
+
detail: "Bun release-age quarantine must be configured with a positive minimumReleaseAge"
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
return findings;
|
|
3953
|
+
}
|
|
3954
|
+
function scanShellProfileLine(lineText, path, line, tracked) {
|
|
3955
|
+
const findings = [];
|
|
3956
|
+
const stripped = lineText.trim();
|
|
3957
|
+
if (stripped === "" || stripped.startsWith("#"))
|
|
3958
|
+
return findings;
|
|
3959
|
+
const assignment = stripped.match(/^(?:export\s+)?(NPM(?:_CONFIG)?_[A-Z0-9_]*TOKEN|NODE_AUTH_TOKEN|NPM_TOKEN)\s*=\s*(.+)$/);
|
|
3960
|
+
if (assignment) {
|
|
3961
|
+
const value = stripQuotes(stripInlineComment(assignment[2].trim()));
|
|
3962
|
+
if (value && !isSafeReference(value)) {
|
|
3963
|
+
findings.push({
|
|
3964
|
+
path,
|
|
3965
|
+
line,
|
|
3966
|
+
rule: "shell-literal-package-token",
|
|
3967
|
+
surface: "shell-profile",
|
|
3968
|
+
severity: "error",
|
|
3969
|
+
tracked,
|
|
3970
|
+
detail: "shell profile package-manager token uses a literal value"
|
|
3971
|
+
});
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
findings.push(...scanKnownTokenPatterns(stripped, path, line, "shell-profile", tracked));
|
|
3975
|
+
return findings;
|
|
3976
|
+
}
|
|
3977
|
+
function scanLockfileLine(lineText, path, line, tracked) {
|
|
3978
|
+
const findings = scanKnownTokenPatterns(lineText, path, line, "lockfile", tracked);
|
|
3979
|
+
if (/(?:^|:)_authToken\s*=\s*/i.test(lineText) && !/\$\{[A-Z0-9_]+\}|\{\{[A-Z0-9_]+\}\}/.test(lineText)) {
|
|
3980
|
+
findings.push({
|
|
3981
|
+
path,
|
|
3982
|
+
line,
|
|
3983
|
+
rule: "lockfile-auth-token",
|
|
3984
|
+
surface: "lockfile",
|
|
3985
|
+
severity: "error",
|
|
3986
|
+
tracked,
|
|
3987
|
+
detail: "lockfile contains package-manager auth token material"
|
|
3988
|
+
});
|
|
3989
|
+
}
|
|
3990
|
+
return findings;
|
|
3991
|
+
}
|
|
3992
|
+
function scanKnownTokenPatterns(lineText, path, line, surface, tracked) {
|
|
3993
|
+
const findings = [];
|
|
3994
|
+
for (const pattern of TOKEN_VALUE_PATTERNS) {
|
|
3995
|
+
if (pattern.re.test(lineText)) {
|
|
3996
|
+
findings.push({
|
|
3997
|
+
path,
|
|
3998
|
+
line,
|
|
3999
|
+
rule: pattern.rule,
|
|
4000
|
+
surface,
|
|
4001
|
+
severity: "error",
|
|
4002
|
+
tracked,
|
|
4003
|
+
detail: pattern.detail
|
|
4004
|
+
});
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return findings;
|
|
4008
|
+
}
|
|
4009
|
+
function scanCredentialedUrl(lineText, path, line, surface, tracked) {
|
|
4010
|
+
const findings = [];
|
|
4011
|
+
for (const match of lineText.matchAll(/\bhttps?:\/\/([^/\s#;]+)@/gi)) {
|
|
4012
|
+
const userInfo = match[1];
|
|
4013
|
+
const credentialPart = userInfo.includes(":") ? userInfo.split(":").slice(1).join(":") : userInfo;
|
|
4014
|
+
if (credentialPart && !isSafeReference(credentialPart)) {
|
|
4015
|
+
findings.push({
|
|
4016
|
+
path,
|
|
4017
|
+
line,
|
|
4018
|
+
rule: "package-manager-url-credentials",
|
|
4019
|
+
surface,
|
|
4020
|
+
severity: "error",
|
|
4021
|
+
tracked,
|
|
4022
|
+
detail: "package-manager URL embeds literal credentials"
|
|
4023
|
+
});
|
|
3620
4024
|
}
|
|
3621
4025
|
}
|
|
4026
|
+
return findings;
|
|
4027
|
+
}
|
|
4028
|
+
function trackedFiles(root) {
|
|
4029
|
+
try {
|
|
4030
|
+
const output = execFileSync("git", ["-C", root, "ls-files", "-z"], {
|
|
4031
|
+
encoding: "utf-8",
|
|
4032
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4033
|
+
});
|
|
4034
|
+
return new Set(output.split("\x00").filter(Boolean).map(toPosix));
|
|
4035
|
+
} catch {
|
|
4036
|
+
return new Set;
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
function isTrackedFile(file) {
|
|
4040
|
+
try {
|
|
4041
|
+
const repoRoot = execFileSync("git", ["-C", dirname4(file), "rev-parse", "--show-toplevel"], {
|
|
4042
|
+
encoding: "utf-8",
|
|
4043
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4044
|
+
}).trim();
|
|
4045
|
+
const rel = toPosix(relative4(repoRoot, file));
|
|
4046
|
+
execFileSync("git", ["-C", repoRoot, "ls-files", "--error-unmatch", "--", rel], {
|
|
4047
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
4048
|
+
});
|
|
4049
|
+
return true;
|
|
4050
|
+
} catch {
|
|
4051
|
+
return false;
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
function isExactHasnaPackageName(item) {
|
|
4055
|
+
return /^@hasna\/[a-z0-9][a-z0-9._-]*$/.test(item);
|
|
4056
|
+
}
|
|
4057
|
+
function isSafeReference(value) {
|
|
4058
|
+
const trimmed = stripQuotes(value.trim());
|
|
4059
|
+
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);
|
|
4060
|
+
}
|
|
4061
|
+
function stripQuotes(value) {
|
|
4062
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
4063
|
+
return value.slice(1, -1);
|
|
4064
|
+
}
|
|
4065
|
+
return value;
|
|
4066
|
+
}
|
|
4067
|
+
function stripInlineComment(value) {
|
|
4068
|
+
return value.replace(/\s[#;].*$/, "").trim();
|
|
4069
|
+
}
|
|
4070
|
+
function displayPath(file, root) {
|
|
4071
|
+
const home = homedir5();
|
|
4072
|
+
if (root === home && (file === home || file.startsWith(home + "/")))
|
|
4073
|
+
return "~/" + toPosix(relative4(home, file));
|
|
4074
|
+
if (isAbsolute3(root) && file.startsWith(root + "/"))
|
|
4075
|
+
return toPosix(relative4(root, file));
|
|
4076
|
+
if (file === home || file.startsWith(home + "/"))
|
|
4077
|
+
return "~/" + toPosix(relative4(home, file));
|
|
4078
|
+
return file;
|
|
4079
|
+
}
|
|
4080
|
+
function toPosix(path) {
|
|
4081
|
+
return path.split("\\").join("/");
|
|
3622
4082
|
}
|
|
3623
4083
|
export {
|
|
3624
4084
|
uuid,
|
|
3625
|
-
updateProfile,
|
|
3626
|
-
updateMachineApplied,
|
|
3627
|
-
updateConfig,
|
|
3628
4085
|
transformSkillContent,
|
|
3629
4086
|
templateizeMachineContent,
|
|
3630
4087
|
syncToDisk,
|
|
@@ -3633,58 +4090,30 @@ export {
|
|
|
3633
4090
|
syncKnown,
|
|
3634
4091
|
syncFromDir,
|
|
3635
4092
|
stripClaudeOnlySections,
|
|
3636
|
-
storageSync,
|
|
3637
|
-
storagePush,
|
|
3638
|
-
storagePull,
|
|
3639
4093
|
sourcesFromIdentityExport,
|
|
3640
4094
|
sourceFromFilePath,
|
|
3641
4095
|
sourceFromConfig,
|
|
3642
4096
|
slugify,
|
|
3643
4097
|
scanSecrets,
|
|
3644
|
-
|
|
3645
|
-
resolveTables,
|
|
4098
|
+
scanPackageManagerSecrets,
|
|
3646
4099
|
resolveSessionTargetOwnership,
|
|
3647
4100
|
resolveSessionPath,
|
|
3648
4101
|
resolveProfileVariables,
|
|
3649
|
-
|
|
3650
|
-
|
|
4102
|
+
resolveConfigStore,
|
|
4103
|
+
resolveCloudConfig,
|
|
3651
4104
|
renderTemplate,
|
|
3652
4105
|
renderMachineAwareContent,
|
|
3653
|
-
removeConfigFromProfile,
|
|
3654
|
-
registerMachine,
|
|
3655
4106
|
redactContent,
|
|
3656
|
-
pruneSnapshots,
|
|
3657
|
-
profileMatchesMachine,
|
|
3658
|
-
profileHasSelectors,
|
|
3659
4107
|
planSessionRender,
|
|
3660
4108
|
parseTemplateVars,
|
|
3661
4109
|
now,
|
|
3662
4110
|
normalizeOsFamily,
|
|
3663
4111
|
machineContextToVariables,
|
|
3664
|
-
listSnapshots,
|
|
3665
|
-
listProfiles,
|
|
3666
|
-
listMachines,
|
|
3667
|
-
listConfigs,
|
|
3668
4112
|
isTemplate,
|
|
3669
|
-
|
|
4113
|
+
isCloudMode,
|
|
3670
4114
|
importConfigs,
|
|
3671
4115
|
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
4116
|
getConfigsStatus,
|
|
3685
|
-
getConfigStats,
|
|
3686
|
-
getConfigById,
|
|
3687
|
-
getConfig,
|
|
3688
4117
|
extractTemplateVars,
|
|
3689
4118
|
exportConfigs,
|
|
3690
4119
|
expandPath,
|
|
@@ -3695,17 +4124,11 @@ export {
|
|
|
3695
4124
|
detectFormat,
|
|
3696
4125
|
detectCategory,
|
|
3697
4126
|
detectAgent,
|
|
3698
|
-
deleteProfile,
|
|
3699
|
-
deleteConfig,
|
|
3700
4127
|
currentOs,
|
|
3701
4128
|
currentHostname2 as currentHostname,
|
|
3702
4129
|
currentArch2 as currentArch,
|
|
3703
|
-
createSnapshot,
|
|
3704
|
-
createProfile,
|
|
3705
|
-
createConfig,
|
|
3706
4130
|
cleanSessionPathInput,
|
|
3707
4131
|
checkSessionRenderDrift,
|
|
3708
|
-
buildPgPoolConfig,
|
|
3709
4132
|
buildOpenCodeAgentsMd,
|
|
3710
4133
|
buildCursorMdc,
|
|
3711
4134
|
buildCodexAgentsMd,
|
|
@@ -3713,37 +4136,30 @@ export {
|
|
|
3713
4136
|
applySessionRender,
|
|
3714
4137
|
applyConfigs,
|
|
3715
4138
|
applyConfig,
|
|
3716
|
-
addConfigToProfile,
|
|
3717
4139
|
TemplateRenderError,
|
|
3718
4140
|
SessionApplyError,
|
|
3719
|
-
STORAGE_TABLES,
|
|
3720
|
-
STORAGE_MODE_ENV,
|
|
3721
|
-
STORAGE_DATABASE_ENV,
|
|
3722
4141
|
SESSION_TOOL_ADAPTERS,
|
|
3723
4142
|
SESSION_RENDER_TOOLS,
|
|
3724
4143
|
SESSION_RENDER_SCHEMA,
|
|
3725
4144
|
SESSION_RENDER_MANAGED_MARKER,
|
|
3726
4145
|
RAW_STORE_ROOT_ENV,
|
|
3727
4146
|
ProfileNotFoundError,
|
|
3728
|
-
PgAdapterAsync,
|
|
3729
4147
|
PROJECT_DASHBOARD_STANDARD_SLUG,
|
|
3730
4148
|
PROJECT_DASHBOARD_STANDARD_CONTENT,
|
|
3731
4149
|
PROJECT_DASHBOARD_PROFILE_VARIABLES,
|
|
3732
4150
|
PROJECT_CONFIG_FILES,
|
|
3733
4151
|
PLATFORM_PROFILE_PRESETS,
|
|
3734
4152
|
PG_MIGRATIONS,
|
|
4153
|
+
LocalConfigStore,
|
|
3735
4154
|
KNOWN_CONFIGS,
|
|
3736
4155
|
ConfigNotFoundError,
|
|
3737
4156
|
ConfigApplyError,
|
|
4157
|
+
CloudHttpError,
|
|
4158
|
+
CloudConfigStore,
|
|
3738
4159
|
CONFIG_TRANSFORMS,
|
|
3739
4160
|
CONFIG_KINDS,
|
|
3740
4161
|
CONFIG_FORMATS,
|
|
3741
4162
|
CONFIG_CATEGORIES,
|
|
3742
4163
|
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
4164
|
CODEWITH_NATIVE_IMPORTS_ENV
|
|
3749
4165
|
};
|