@hasna/instructions 0.6.0 → 0.6.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 +26 -2
- package/dist/chunks/apply-sfzja1f3.js +22 -0
- package/dist/chunks/apply-ynfn1av7.js +23 -0
- package/dist/chunks/index-1hpwskwd.js +12702 -0
- package/dist/chunks/index-35z65jsa.js +198 -0
- package/dist/chunks/index-85n5wnd8.js +625 -0
- package/dist/chunks/index-9p9gn7gh.js +627 -0
- package/dist/chunks/index-axhwg61p.js +523 -0
- package/dist/chunks/index-bsan4c16.js +48 -0
- package/dist/chunks/index-e9epbsvs.js +521 -0
- package/dist/chunks/index-ma2xmp6c.js +14 -0
- package/dist/chunks/index-nnwp92tk.js +19 -0
- package/dist/chunks/index-pkscjnj5.js +118 -0
- package/dist/chunks/index-v7dkb384.js +7248 -0
- package/dist/chunks/local-0d8qrc92.js +693 -0
- package/dist/chunks/local-a9nen65z.js +694 -0
- package/dist/chunks/package-manager-guard-x70xrwk2.js +391 -0
- package/dist/chunks/redact-hnq4sc4r.js +16 -0
- package/dist/chunks/sync-m46fc257.js +33 -0
- package/dist/chunks/sync-vy1yjvs6.js +34 -0
- package/dist/chunks/template-02wjvv05.js +16 -0
- package/dist/chunks/template-ke972qqe.js +16 -0
- package/dist/cli/index.js +722 -15448
- package/dist/data/config-store.d.ts +1 -1
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/database.d.ts +3 -5
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/local.d.ts +38 -0
- package/dist/db/local.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10684 -10498
- package/dist/lib/ids.d.ts +4 -0
- package/dist/lib/ids.d.ts.map +1 -0
- package/dist/lib/instruction-graph.d.ts.map +1 -1
- package/dist/lib/session-render.d.ts +3 -0
- package/dist/lib/session-render.d.ts.map +1 -1
- package/dist/mcp/index.js +55 -9455
- package/dist/server/index.js +7329 -7235
- package/dist/status.d.ts +12 -2
- package/dist/status.d.ts.map +1 -1
- package/package.json +12 -8
- package/dist/cli/add-duplicate-target.test.d.ts +0 -2
- package/dist/cli/add-duplicate-target.test.d.ts.map +0 -1
- package/dist/cli/add-reference-update.test.d.ts +0 -2
- package/dist/cli/add-reference-update.test.d.ts.map +0 -1
- package/dist/cli/apply-unchanged-snapshot-bloat.test.d.ts +0 -2
- package/dist/cli/apply-unchanged-snapshot-bloat.test.d.ts.map +0 -1
- package/dist/cli/doctor-reference-duplicates.test.d.ts +0 -2
- package/dist/cli/doctor-reference-duplicates.test.d.ts.map +0 -1
- package/dist/cli/fail-closed-no-env.test.d.ts +0 -2
- package/dist/cli/fail-closed-no-env.test.d.ts.map +0 -1
- package/dist/cli/output.test.d.ts +0 -2
- package/dist/cli/output.test.d.ts.map +0 -1
- package/dist/cli/profile-binding.test.d.ts +0 -2
- package/dist/cli/profile-binding.test.d.ts.map +0 -1
- package/dist/cli/profile-reads.test.d.ts +0 -2
- package/dist/cli/profile-reads.test.d.ts.map +0 -1
- package/dist/cli/profile-update.test.d.ts +0 -2
- package/dist/cli/profile-update.test.d.ts.map +0 -1
- package/dist/cli/project-context.test.d.ts +0 -2
- package/dist/cli/project-context.test.d.ts.map +0 -1
- package/dist/cli/raw-store-root.test.d.ts +0 -2
- package/dist/cli/raw-store-root.test.d.ts.map +0 -1
- package/dist/cli/session-apply-managed-input-bound.test.d.ts +0 -2
- package/dist/cli/session-apply-managed-input-bound.test.d.ts.map +0 -1
- package/dist/cli/session.test.d.ts +0 -2
- package/dist/cli/session.test.d.ts.map +0 -1
- package/dist/cli/station-profile.test.d.ts +0 -2
- package/dist/cli/station-profile.test.d.ts.map +0 -1
- package/dist/data/config-store.test.d.ts +0 -2
- package/dist/data/config-store.test.d.ts.map +0 -1
- package/dist/db/configs.test.d.ts +0 -2
- package/dist/db/configs.test.d.ts.map +0 -1
- package/dist/db/database.test.d.ts +0 -2
- package/dist/db/database.test.d.ts.map +0 -1
- package/dist/db/machines.test.d.ts +0 -2
- package/dist/db/machines.test.d.ts.map +0 -1
- package/dist/db/profile-assets.test.d.ts +0 -2
- package/dist/db/profile-assets.test.d.ts.map +0 -1
- package/dist/db/profile-config-bindings.test.d.ts +0 -2
- package/dist/db/profile-config-bindings.test.d.ts.map +0 -1
- package/dist/db/profiles.test.d.ts +0 -2
- package/dist/db/profiles.test.d.ts.map +0 -1
- package/dist/db/snapshots.test.d.ts +0 -2
- package/dist/db/snapshots.test.d.ts.map +0 -1
- package/dist/lib/app-home.test.d.ts +0 -2
- package/dist/lib/app-home.test.d.ts.map +0 -1
- package/dist/lib/apply-batch.test.d.ts +0 -2
- package/dist/lib/apply-batch.test.d.ts.map +0 -1
- package/dist/lib/apply-dry-run-verdict.test.d.ts +0 -2
- package/dist/lib/apply-dry-run-verdict.test.d.ts.map +0 -1
- package/dist/lib/apply-secret-placeholder-guard.test.d.ts +0 -2
- package/dist/lib/apply-secret-placeholder-guard.test.d.ts.map +0 -1
- package/dist/lib/apply-text-format-credential-guard.test.d.ts +0 -2
- package/dist/lib/apply-text-format-credential-guard.test.d.ts.map +0 -1
- package/dist/lib/apply.test.d.ts +0 -2
- package/dist/lib/apply.test.d.ts.map +0 -1
- package/dist/lib/asset-plan.test.d.ts +0 -2
- package/dist/lib/asset-plan.test.d.ts.map +0 -1
- package/dist/lib/client-types.test.d.ts +0 -2
- package/dist/lib/client-types.test.d.ts.map +0 -1
- package/dist/lib/codewith-shared-todos-storage-standard.test.d.ts +0 -2
- package/dist/lib/codewith-shared-todos-storage-standard.test.d.ts.map +0 -1
- package/dist/lib/compact-output.test.d.ts +0 -2
- package/dist/lib/compact-output.test.d.ts.map +0 -1
- package/dist/lib/config-target-identity.test.d.ts +0 -2
- package/dist/lib/config-target-identity.test.d.ts.map +0 -1
- package/dist/lib/cursor-authority.test.d.ts +0 -2
- package/dist/lib/cursor-authority.test.d.ts.map +0 -1
- package/dist/lib/dangerous-operation-guard-standard.test.d.ts +0 -2
- package/dist/lib/dangerous-operation-guard-standard.test.d.ts.map +0 -1
- package/dist/lib/diff-redaction.test.d.ts +0 -2
- package/dist/lib/diff-redaction.test.d.ts.map +0 -1
- package/dist/lib/export-import.test.d.ts +0 -2
- package/dist/lib/export-import.test.d.ts.map +0 -1
- package/dist/lib/global-agent-rules-render-integration.test.d.ts +0 -2
- package/dist/lib/global-agent-rules-render-integration.test.d.ts.map +0 -1
- package/dist/lib/global-agent-rules-standard.test.d.ts +0 -2
- package/dist/lib/global-agent-rules-standard.test.d.ts.map +0 -1
- package/dist/lib/global-agent-rules-stored-content.test.d.ts +0 -2
- package/dist/lib/global-agent-rules-stored-content.test.d.ts.map +0 -1
- package/dist/lib/global-source-coverage.test.d.ts +0 -2
- package/dist/lib/global-source-coverage.test.d.ts.map +0 -1
- package/dist/lib/instruction-graph-apply.test.d.ts +0 -2
- package/dist/lib/instruction-graph-apply.test.d.ts.map +0 -1
- package/dist/lib/instruction-graph.test.d.ts +0 -2
- package/dist/lib/instruction-graph.test.d.ts.map +0 -1
- package/dist/lib/machine.test.d.ts +0 -2
- package/dist/lib/machine.test.d.ts.map +0 -1
- package/dist/lib/managed-skill-runtimes.test.d.ts +0 -2
- package/dist/lib/managed-skill-runtimes.test.d.ts.map +0 -1
- package/dist/lib/package-manager-guard.test.d.ts +0 -2
- package/dist/lib/package-manager-guard.test.d.ts.map +0 -1
- package/dist/lib/project-context-prepared-file.test.d.ts +0 -2
- package/dist/lib/project-context-prepared-file.test.d.ts.map +0 -1
- package/dist/lib/project-context.test.d.ts +0 -2
- package/dist/lib/project-context.test.d.ts.map +0 -1
- package/dist/lib/project-dashboard-standard.test.d.ts +0 -2
- package/dist/lib/project-dashboard-standard.test.d.ts.map +0 -1
- package/dist/lib/provider-assets-adapters.test.d.ts +0 -2
- package/dist/lib/provider-assets-adapters.test.d.ts.map +0 -1
- package/dist/lib/provider-context.test.d.ts +0 -2
- package/dist/lib/provider-context.test.d.ts.map +0 -1
- package/dist/lib/provider-version-adapters.test.d.ts +0 -2
- package/dist/lib/provider-version-adapters.test.d.ts.map +0 -1
- package/dist/lib/redact.test.d.ts +0 -2
- package/dist/lib/redact.test.d.ts.map +0 -1
- package/dist/lib/session-apply.test.d.ts +0 -2
- package/dist/lib/session-apply.test.d.ts.map +0 -1
- package/dist/lib/session-authority.test.d.ts +0 -2
- package/dist/lib/session-authority.test.d.ts.map +0 -1
- package/dist/lib/session-render-managed-output-bound.test.d.ts +0 -2
- package/dist/lib/session-render-managed-output-bound.test.d.ts.map +0 -1
- package/dist/lib/session-render-ownership.test.d.ts +0 -2
- package/dist/lib/session-render-ownership.test.d.ts.map +0 -1
- package/dist/lib/session-render-rule-currency-floor.test.d.ts +0 -2
- package/dist/lib/session-render-rule-currency-floor.test.d.ts.map +0 -1
- package/dist/lib/session-render-silent-source-drop.test.d.ts +0 -2
- package/dist/lib/session-render-silent-source-drop.test.d.ts.map +0 -1
- package/dist/lib/session-render-state-hermeticity.test.d.ts +0 -2
- package/dist/lib/session-render-state-hermeticity.test.d.ts.map +0 -1
- package/dist/lib/session-render-state.test.d.ts +0 -2
- package/dist/lib/session-render-state.test.d.ts.map +0 -1
- package/dist/lib/session-render.test.d.ts +0 -2
- package/dist/lib/session-render.test.d.ts.map +0 -1
- package/dist/lib/station-profile.test.d.ts +0 -2
- package/dist/lib/station-profile.test.d.ts.map +0 -1
- package/dist/lib/sync-dir.test.d.ts +0 -2
- package/dist/lib/sync-dir.test.d.ts.map +0 -1
- package/dist/lib/sync-dry-run-unchanged.test.d.ts +0 -2
- package/dist/lib/sync-dry-run-unchanged.test.d.ts.map +0 -1
- package/dist/lib/sync-known.test.d.ts +0 -2
- package/dist/lib/sync-known.test.d.ts.map +0 -1
- package/dist/lib/sync.test.d.ts +0 -2
- package/dist/lib/sync.test.d.ts.map +0 -1
- package/dist/lib/template.test.d.ts +0 -2
- package/dist/lib/template.test.d.ts.map +0 -1
- package/dist/lib/transforms.test.d.ts +0 -2
- package/dist/lib/transforms.test.d.ts.map +0 -1
- package/dist/lib/transport-resolver.test.d.ts +0 -2
- package/dist/lib/transport-resolver.test.d.ts.map +0 -1
- package/dist/mcp/create-config-target-guard.test.d.ts +0 -2
- package/dist/mcp/create-config-target-guard.test.d.ts.map +0 -1
- package/dist/mcp/early-args.test.d.ts +0 -2
- package/dist/mcp/early-args.test.d.ts.map +0 -1
- package/dist/mcp/http.test.d.ts +0 -2
- package/dist/mcp/http.test.d.ts.map +0 -1
- package/dist/mcp/mcp.test.d.ts +0 -2
- package/dist/mcp/mcp.test.d.ts.map +0 -1
- package/dist/sdk/resolve.test.d.ts +0 -2
- package/dist/sdk/resolve.test.d.ts.map +0 -1
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts +0 -2
- package/dist/sdk/sdk-bundle-self-contained.test.d.ts.map +0 -1
- package/dist/server/cloud-auth.test.d.ts +0 -2
- package/dist/server/cloud-auth.test.d.ts.map +0 -1
- package/dist/server/cloud.test.d.ts +0 -2
- package/dist/server/cloud.test.d.ts.map +0 -1
- package/dist/server/early-args.test.d.ts +0 -2
- package/dist/server/early-args.test.d.ts.map +0 -1
- package/dist/server/profile-contract.test.d.ts +0 -2
- package/dist/server/profile-contract.test.d.ts.map +0 -1
- package/dist/status.test.d.ts +0 -2
- package/dist/status.test.d.ts.map +0 -1
- package/dist/storage/cloud-store.test.d.ts +0 -2
- package/dist/storage/cloud-store.test.d.ts.map +0 -1
- package/dist/test-support/preload-state-home.d.ts +0 -2
- package/dist/test-support/preload-state-home.d.ts.map +0 -1
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
now,
|
|
4
|
+
slugify,
|
|
5
|
+
uuid
|
|
6
|
+
} from "./index-ma2xmp6c.js";
|
|
7
|
+
import {
|
|
8
|
+
boundedReadPage,
|
|
9
|
+
detectMachineContext,
|
|
10
|
+
getRawStoreRoot,
|
|
11
|
+
hasInstructionsEnvAuthorityIntent,
|
|
12
|
+
legacyProfileConfigBinding,
|
|
13
|
+
normalizeBoundedReadOptions,
|
|
14
|
+
normalizeOsFamily,
|
|
15
|
+
normalizeProfileAssetBinding,
|
|
16
|
+
normalizeProfileConfigBinding
|
|
17
|
+
} from "./index-v7dkb384.js";
|
|
18
|
+
import"./index-35z65jsa.js";
|
|
19
|
+
import {
|
|
20
|
+
ConfigNotFoundError,
|
|
21
|
+
ProfileNotFoundError
|
|
22
|
+
} from "./index-pkscjnj5.js";
|
|
23
|
+
import"./index-nnwp92tk.js";
|
|
24
|
+
|
|
25
|
+
// src/db/database.ts
|
|
26
|
+
import { Database } from "bun:sqlite";
|
|
27
|
+
import { existsSync, mkdirSync, rmSync } from "fs";
|
|
28
|
+
import { join } from "path";
|
|
29
|
+
function getDbPath() {
|
|
30
|
+
if (process.env["HASNA_INSTRUCTIONS_DB_PATH"]) {
|
|
31
|
+
return process.env["HASNA_INSTRUCTIONS_DB_PATH"];
|
|
32
|
+
}
|
|
33
|
+
const dir = getRawStoreRoot();
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
return join(dir, "instructions.db");
|
|
36
|
+
}
|
|
37
|
+
var MIGRATIONS = [
|
|
38
|
+
`
|
|
39
|
+
CREATE TABLE IF NOT EXISTS configs (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
slug TEXT NOT NULL UNIQUE,
|
|
43
|
+
kind TEXT NOT NULL DEFAULT 'file',
|
|
44
|
+
category TEXT NOT NULL,
|
|
45
|
+
agent TEXT NOT NULL DEFAULT 'global',
|
|
46
|
+
target_path TEXT,
|
|
47
|
+
format TEXT NOT NULL DEFAULT 'text',
|
|
48
|
+
content TEXT NOT NULL DEFAULT '',
|
|
49
|
+
description TEXT,
|
|
50
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
51
|
+
is_template INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
53
|
+
created_at TEXT NOT NULL,
|
|
54
|
+
updated_at TEXT NOT NULL,
|
|
55
|
+
synced_at TEXT
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS config_snapshots (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
61
|
+
content TEXT NOT NULL,
|
|
62
|
+
version INTEGER NOT NULL,
|
|
63
|
+
created_at TEXT NOT NULL
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS profiles (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
name TEXT NOT NULL,
|
|
69
|
+
slug TEXT NOT NULL UNIQUE,
|
|
70
|
+
description TEXT,
|
|
71
|
+
created_at TEXT NOT NULL,
|
|
72
|
+
updated_at TEXT NOT NULL
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS profile_configs (
|
|
76
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
77
|
+
config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
78
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
PRIMARY KEY (profile_id, config_id)
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS machines (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
hostname TEXT NOT NULL UNIQUE,
|
|
85
|
+
os TEXT,
|
|
86
|
+
last_applied_at TEXT,
|
|
87
|
+
created_at TEXT NOT NULL
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
91
|
+
version INTEGER PRIMARY KEY
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES (1);
|
|
95
|
+
`,
|
|
96
|
+
`
|
|
97
|
+
ALTER TABLE profiles ADD COLUMN selectors TEXT NOT NULL DEFAULT '{}';
|
|
98
|
+
ALTER TABLE profiles ADD COLUMN variables TEXT NOT NULL DEFAULT '{}';
|
|
99
|
+
ALTER TABLE machines ADD COLUMN arch TEXT;
|
|
100
|
+
`,
|
|
101
|
+
`
|
|
102
|
+
ALTER TABLE configs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]';
|
|
103
|
+
`,
|
|
104
|
+
`
|
|
105
|
+
ALTER TABLE profile_configs ADD COLUMN binding TEXT NOT NULL DEFAULT '{"schema":"hasna.instructions.profile-config-binding/v1","activation":{"mode":"always"},"required":true,"fallback":"fail"}';
|
|
106
|
+
`,
|
|
107
|
+
`
|
|
108
|
+
CREATE TABLE IF NOT EXISTS profile_assets (
|
|
109
|
+
profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
110
|
+
source_config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
|
|
111
|
+
asset_key TEXT NOT NULL,
|
|
112
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
113
|
+
binding TEXT NOT NULL,
|
|
114
|
+
PRIMARY KEY (profile_id, asset_key)
|
|
115
|
+
);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS profile_assets_source_config_idx ON profile_assets (source_config_id);
|
|
117
|
+
`
|
|
118
|
+
];
|
|
119
|
+
var _db = null;
|
|
120
|
+
function getDatabase(path) {
|
|
121
|
+
if (_db)
|
|
122
|
+
return _db;
|
|
123
|
+
if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
|
|
124
|
+
throw new Error("instructions: refusing to open the on-box SQLite store \u2014 the environment configures a hosted " + "Instructions authority or credential (HASNA_INSTRUCTIONS_*), and reading or writing the local store " + "here would silently drift from the shared dataset. Pass an explicit database path (or an injected " + "Database) to work against the on-box store deliberately.");
|
|
125
|
+
}
|
|
126
|
+
const dbPath = path || getDbPath();
|
|
127
|
+
const db = new Database(dbPath);
|
|
128
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
129
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
130
|
+
applyMigrations(db);
|
|
131
|
+
ensureFeedbackTable(db);
|
|
132
|
+
_db = db;
|
|
133
|
+
return db;
|
|
134
|
+
}
|
|
135
|
+
function resetDatabase() {
|
|
136
|
+
if (_db) {
|
|
137
|
+
try {
|
|
138
|
+
_db.close();
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
_db = null;
|
|
142
|
+
}
|
|
143
|
+
function resetLocalDatabase() {
|
|
144
|
+
resetDatabase();
|
|
145
|
+
const dbPath = getDbPath();
|
|
146
|
+
if (dbPath === ":memory:")
|
|
147
|
+
return;
|
|
148
|
+
for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
149
|
+
if (existsSync(p))
|
|
150
|
+
rmSync(p);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function applyMigrations(db) {
|
|
154
|
+
let currentVersion = 0;
|
|
155
|
+
try {
|
|
156
|
+
const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
|
|
157
|
+
currentVersion = row?.version ?? 0;
|
|
158
|
+
} catch {
|
|
159
|
+
currentVersion = 0;
|
|
160
|
+
}
|
|
161
|
+
const applyOne = db.transaction((index) => {
|
|
162
|
+
if (!migrationEffectAlreadyPresent(db, index))
|
|
163
|
+
db.exec(MIGRATIONS[index]);
|
|
164
|
+
db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${index + 1})`);
|
|
165
|
+
});
|
|
166
|
+
for (let i = currentVersion;i < MIGRATIONS.length; i++)
|
|
167
|
+
applyOne(i);
|
|
168
|
+
}
|
|
169
|
+
function migrationEffectAlreadyPresent(db, index) {
|
|
170
|
+
if (index !== 3)
|
|
171
|
+
return false;
|
|
172
|
+
return db.query("PRAGMA table_info(profile_configs)").all().some((column) => column.name === "binding");
|
|
173
|
+
}
|
|
174
|
+
function ensureFeedbackTable(db) {
|
|
175
|
+
db.exec(`
|
|
176
|
+
CREATE TABLE IF NOT EXISTS feedback (
|
|
177
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
178
|
+
message TEXT NOT NULL,
|
|
179
|
+
email TEXT,
|
|
180
|
+
category TEXT DEFAULT 'general',
|
|
181
|
+
version TEXT,
|
|
182
|
+
machine_id TEXT,
|
|
183
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
184
|
+
)
|
|
185
|
+
`);
|
|
186
|
+
const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
|
|
187
|
+
const required = [
|
|
188
|
+
["email", "TEXT"],
|
|
189
|
+
["category", "TEXT DEFAULT 'general'"],
|
|
190
|
+
["version", "TEXT"],
|
|
191
|
+
["machine_id", "TEXT"],
|
|
192
|
+
["created_at", "TEXT"]
|
|
193
|
+
];
|
|
194
|
+
for (const [name, def] of required) {
|
|
195
|
+
if (!existing.has(name))
|
|
196
|
+
db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function insertFeedback(input, db) {
|
|
200
|
+
const d = db || getDatabase();
|
|
201
|
+
d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/db/snapshots.ts
|
|
205
|
+
function createSnapshot(configId, content, version, db) {
|
|
206
|
+
const d = db || getDatabase();
|
|
207
|
+
const id = uuid();
|
|
208
|
+
const ts = now();
|
|
209
|
+
d.run("INSERT INTO config_snapshots (id, config_id, content, version, created_at) VALUES (?, ?, ?, ?, ?)", [id, configId, content, version, ts]);
|
|
210
|
+
return { id, config_id: configId, content, version, created_at: ts };
|
|
211
|
+
}
|
|
212
|
+
function listSnapshots(configId, db) {
|
|
213
|
+
const d = db || getDatabase();
|
|
214
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? ORDER BY version DESC").all(configId);
|
|
215
|
+
}
|
|
216
|
+
function getSnapshot(id, db) {
|
|
217
|
+
const d = db || getDatabase();
|
|
218
|
+
return d.query("SELECT * FROM config_snapshots WHERE id = ?").get(id);
|
|
219
|
+
}
|
|
220
|
+
function getSnapshotByVersion(configId, version, db) {
|
|
221
|
+
const d = db || getDatabase();
|
|
222
|
+
return d.query("SELECT * FROM config_snapshots WHERE config_id = ? AND version = ?").get(configId, version);
|
|
223
|
+
}
|
|
224
|
+
function pruneSnapshots(configId, keep = 10, db) {
|
|
225
|
+
const d = db || getDatabase();
|
|
226
|
+
const result = d.run(`DELETE FROM config_snapshots WHERE config_id = ? AND id NOT IN (
|
|
227
|
+
SELECT id FROM config_snapshots WHERE config_id = ? ORDER BY version DESC LIMIT ?
|
|
228
|
+
)`, [configId, configId, keep]);
|
|
229
|
+
return result.changes;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/db/configs.ts
|
|
233
|
+
function rowToConfig(row) {
|
|
234
|
+
let outputs = [];
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(row.outputs || "[]");
|
|
237
|
+
outputs = Array.isArray(parsed) ? parsed : [];
|
|
238
|
+
} catch {
|
|
239
|
+
outputs = [];
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
...row,
|
|
243
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
244
|
+
outputs,
|
|
245
|
+
is_template: !!row.is_template,
|
|
246
|
+
kind: row.kind,
|
|
247
|
+
category: row.category,
|
|
248
|
+
agent: row.agent,
|
|
249
|
+
format: row.format
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function uniqueSlug(name, db, excludeId) {
|
|
253
|
+
const base = slugify(name);
|
|
254
|
+
let slug = base;
|
|
255
|
+
let i = 1;
|
|
256
|
+
while (true) {
|
|
257
|
+
const existing = db.query("SELECT id FROM configs WHERE slug = ?").get(slug);
|
|
258
|
+
if (!existing || existing.id === excludeId)
|
|
259
|
+
return slug;
|
|
260
|
+
slug = `${base}-${i++}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function createConfig(input, db) {
|
|
264
|
+
const d = db || getDatabase();
|
|
265
|
+
const id = uuid();
|
|
266
|
+
const ts = now();
|
|
267
|
+
const slug = uniqueSlug(input.name, d);
|
|
268
|
+
const tags = JSON.stringify(input.tags || []);
|
|
269
|
+
const outputs = JSON.stringify(input.outputs || []);
|
|
270
|
+
return d.transaction(() => {
|
|
271
|
+
d.run(`INSERT INTO configs (id, name, slug, kind, category, agent, target_path, outputs, format, content, description, tags, is_template, version, created_at, updated_at, synced_at)
|
|
272
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, NULL)`, [
|
|
273
|
+
id,
|
|
274
|
+
input.name,
|
|
275
|
+
slug,
|
|
276
|
+
input.kind ?? "file",
|
|
277
|
+
input.category,
|
|
278
|
+
input.agent ?? "global",
|
|
279
|
+
input.target_path ?? null,
|
|
280
|
+
outputs,
|
|
281
|
+
input.format ?? "text",
|
|
282
|
+
input.content,
|
|
283
|
+
input.description ?? null,
|
|
284
|
+
tags,
|
|
285
|
+
input.is_template ? 1 : 0,
|
|
286
|
+
ts,
|
|
287
|
+
ts
|
|
288
|
+
]);
|
|
289
|
+
createSnapshot(id, input.content, 1, d);
|
|
290
|
+
return getConfig(id, d);
|
|
291
|
+
})();
|
|
292
|
+
}
|
|
293
|
+
function getConfig(idOrSlug, db) {
|
|
294
|
+
const d = db || getDatabase();
|
|
295
|
+
const row = d.query("SELECT * FROM configs WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
296
|
+
if (!row)
|
|
297
|
+
throw new ConfigNotFoundError(idOrSlug);
|
|
298
|
+
return rowToConfig(row);
|
|
299
|
+
}
|
|
300
|
+
function getConfigById(id, db) {
|
|
301
|
+
const d = db || getDatabase();
|
|
302
|
+
const row = d.query("SELECT * FROM configs WHERE id = ?").get(id);
|
|
303
|
+
if (!row)
|
|
304
|
+
throw new ConfigNotFoundError(id);
|
|
305
|
+
return rowToConfig(row);
|
|
306
|
+
}
|
|
307
|
+
function listConfigs(filter, db) {
|
|
308
|
+
const d = db || getDatabase();
|
|
309
|
+
const conditions = [];
|
|
310
|
+
const params = [];
|
|
311
|
+
if (filter?.category) {
|
|
312
|
+
conditions.push("category = ?");
|
|
313
|
+
params.push(filter.category);
|
|
314
|
+
}
|
|
315
|
+
if (filter?.agent) {
|
|
316
|
+
conditions.push("agent = ?");
|
|
317
|
+
params.push(filter.agent);
|
|
318
|
+
}
|
|
319
|
+
if (filter?.kind) {
|
|
320
|
+
conditions.push("kind = ?");
|
|
321
|
+
params.push(filter.kind);
|
|
322
|
+
}
|
|
323
|
+
if (filter?.is_template !== undefined) {
|
|
324
|
+
conditions.push("is_template = ?");
|
|
325
|
+
params.push(filter.is_template ? 1 : 0);
|
|
326
|
+
}
|
|
327
|
+
if (filter?.search) {
|
|
328
|
+
conditions.push("(name LIKE ? OR description LIKE ? OR content LIKE ?)");
|
|
329
|
+
const q = `%${filter.search}%`;
|
|
330
|
+
params.push(q, q, q);
|
|
331
|
+
}
|
|
332
|
+
if (filter?.tags && filter.tags.length > 0) {
|
|
333
|
+
const tagConditions = filter.tags.map(() => "tags LIKE ?").join(" OR ");
|
|
334
|
+
conditions.push(`(${tagConditions})`);
|
|
335
|
+
for (const tag of filter.tags)
|
|
336
|
+
params.push(`%"${tag}"%`);
|
|
337
|
+
}
|
|
338
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
339
|
+
const rows = d.query(`SELECT * FROM configs ${where} ORDER BY category, name`).all(...params);
|
|
340
|
+
return rows.map(rowToConfig);
|
|
341
|
+
}
|
|
342
|
+
function updateConfig(idOrSlug, input, db) {
|
|
343
|
+
const d = db || getDatabase();
|
|
344
|
+
const existing = getConfig(idOrSlug, d);
|
|
345
|
+
const ts = now();
|
|
346
|
+
const updates = ["updated_at = ?", "version = version + 1"];
|
|
347
|
+
const params = [ts];
|
|
348
|
+
if (input.name !== undefined) {
|
|
349
|
+
updates.push("name = ?", "slug = ?");
|
|
350
|
+
params.push(input.name, uniqueSlug(input.name, d, existing.id));
|
|
351
|
+
}
|
|
352
|
+
if (input.kind !== undefined) {
|
|
353
|
+
updates.push("kind = ?");
|
|
354
|
+
params.push(input.kind);
|
|
355
|
+
}
|
|
356
|
+
if (input.category !== undefined) {
|
|
357
|
+
updates.push("category = ?");
|
|
358
|
+
params.push(input.category);
|
|
359
|
+
}
|
|
360
|
+
if (input.agent !== undefined) {
|
|
361
|
+
updates.push("agent = ?");
|
|
362
|
+
params.push(input.agent);
|
|
363
|
+
}
|
|
364
|
+
if (input.target_path !== undefined) {
|
|
365
|
+
updates.push("target_path = ?");
|
|
366
|
+
params.push(input.target_path);
|
|
367
|
+
}
|
|
368
|
+
if (input.outputs !== undefined) {
|
|
369
|
+
updates.push("outputs = ?");
|
|
370
|
+
params.push(JSON.stringify(input.outputs));
|
|
371
|
+
}
|
|
372
|
+
if (input.format !== undefined) {
|
|
373
|
+
updates.push("format = ?");
|
|
374
|
+
params.push(input.format);
|
|
375
|
+
}
|
|
376
|
+
if (input.content !== undefined) {
|
|
377
|
+
updates.push("content = ?");
|
|
378
|
+
params.push(input.content);
|
|
379
|
+
}
|
|
380
|
+
if (input.description !== undefined) {
|
|
381
|
+
updates.push("description = ?");
|
|
382
|
+
params.push(input.description);
|
|
383
|
+
}
|
|
384
|
+
if (input.tags !== undefined) {
|
|
385
|
+
updates.push("tags = ?");
|
|
386
|
+
params.push(JSON.stringify(input.tags));
|
|
387
|
+
}
|
|
388
|
+
if (input.is_template !== undefined) {
|
|
389
|
+
updates.push("is_template = ?");
|
|
390
|
+
params.push(input.is_template ? 1 : 0);
|
|
391
|
+
}
|
|
392
|
+
if (input.synced_at !== undefined) {
|
|
393
|
+
updates.push("synced_at = ?");
|
|
394
|
+
params.push(input.synced_at);
|
|
395
|
+
}
|
|
396
|
+
return d.transaction(() => {
|
|
397
|
+
params.push(existing.id);
|
|
398
|
+
d.run(`UPDATE configs SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
399
|
+
const updated = getConfigById(existing.id, d);
|
|
400
|
+
createSnapshot(updated.id, updated.content, updated.version, d);
|
|
401
|
+
return updated;
|
|
402
|
+
})();
|
|
403
|
+
}
|
|
404
|
+
function deleteConfig(idOrSlug, db) {
|
|
405
|
+
const d = db || getDatabase();
|
|
406
|
+
const existing = getConfig(idOrSlug, d);
|
|
407
|
+
d.run("DELETE FROM configs WHERE id = ?", [existing.id]);
|
|
408
|
+
}
|
|
409
|
+
function getConfigStats(db) {
|
|
410
|
+
const d = db || getDatabase();
|
|
411
|
+
const rows = d.query("SELECT category, COUNT(*) as count FROM configs GROUP BY category").all();
|
|
412
|
+
const stats = { total: 0 };
|
|
413
|
+
for (const row of rows) {
|
|
414
|
+
stats[row.category] = row.count;
|
|
415
|
+
stats["total"] = (stats["total"] || 0) + row.count;
|
|
416
|
+
}
|
|
417
|
+
return stats;
|
|
418
|
+
}
|
|
419
|
+
// src/db/profiles.ts
|
|
420
|
+
function rowToProfile(row) {
|
|
421
|
+
return {
|
|
422
|
+
...row,
|
|
423
|
+
selectors: JSON.parse(row.selectors || "{}"),
|
|
424
|
+
variables: JSON.parse(row.variables || "{}")
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function uniqueProfileSlug(name, db, excludeId) {
|
|
428
|
+
const base = slugify(name);
|
|
429
|
+
let slug = base;
|
|
430
|
+
let i = 1;
|
|
431
|
+
while (true) {
|
|
432
|
+
const existing = db.query("SELECT id FROM profiles WHERE slug = ?").get(slug);
|
|
433
|
+
if (!existing || existing.id === excludeId)
|
|
434
|
+
return slug;
|
|
435
|
+
slug = `${base}-${i++}`;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function createProfile(input, db) {
|
|
439
|
+
const d = db || getDatabase();
|
|
440
|
+
const id = uuid();
|
|
441
|
+
const ts = now();
|
|
442
|
+
const slug = uniqueProfileSlug(input.name, d);
|
|
443
|
+
d.run("INSERT INTO profiles (id, name, slug, description, selectors, variables, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
444
|
+
id,
|
|
445
|
+
input.name,
|
|
446
|
+
slug,
|
|
447
|
+
input.description ?? null,
|
|
448
|
+
JSON.stringify(input.selectors ?? {}),
|
|
449
|
+
JSON.stringify(input.variables ?? {}),
|
|
450
|
+
ts,
|
|
451
|
+
ts
|
|
452
|
+
]);
|
|
453
|
+
return getProfile(id, d);
|
|
454
|
+
}
|
|
455
|
+
function getProfile(idOrSlug, db) {
|
|
456
|
+
const d = db || getDatabase();
|
|
457
|
+
const row = d.query("SELECT * FROM profiles WHERE id = ? OR slug = ?").get(idOrSlug, idOrSlug);
|
|
458
|
+
if (!row)
|
|
459
|
+
throw new ProfileNotFoundError(idOrSlug);
|
|
460
|
+
return rowToProfile(row);
|
|
461
|
+
}
|
|
462
|
+
function listProfilesPage(options = {}, db) {
|
|
463
|
+
const d = db || getDatabase();
|
|
464
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
465
|
+
const total = d.query("SELECT COUNT(*) AS total FROM profiles").get()?.total ?? 0;
|
|
466
|
+
const rows = d.query("SELECT * FROM profiles ORDER BY name LIMIT ? OFFSET ?").all(normalized.limit, normalized.cursor).map(rowToProfile);
|
|
467
|
+
return boundedReadPage(rows, total, normalized);
|
|
468
|
+
}
|
|
469
|
+
function updateProfile(idOrSlug, input, db) {
|
|
470
|
+
const d = db || getDatabase();
|
|
471
|
+
const existing = getProfile(idOrSlug, d);
|
|
472
|
+
const ts = now();
|
|
473
|
+
const updates = ["updated_at = ?"];
|
|
474
|
+
const params = [ts];
|
|
475
|
+
if (input.name !== undefined) {
|
|
476
|
+
updates.push("name = ?", "slug = ?");
|
|
477
|
+
params.push(input.name, uniqueProfileSlug(input.name, d, existing.id));
|
|
478
|
+
}
|
|
479
|
+
if (input.description !== undefined) {
|
|
480
|
+
updates.push("description = ?");
|
|
481
|
+
params.push(input.description);
|
|
482
|
+
}
|
|
483
|
+
if (input.selectors !== undefined) {
|
|
484
|
+
updates.push("selectors = ?");
|
|
485
|
+
params.push(JSON.stringify(input.selectors));
|
|
486
|
+
}
|
|
487
|
+
if (input.variables !== undefined) {
|
|
488
|
+
updates.push("variables = ?");
|
|
489
|
+
params.push(JSON.stringify(input.variables));
|
|
490
|
+
}
|
|
491
|
+
params.push(existing.id);
|
|
492
|
+
d.run(`UPDATE profiles SET ${updates.join(", ")} WHERE id = ?`, params);
|
|
493
|
+
return getProfile(existing.id, d);
|
|
494
|
+
}
|
|
495
|
+
function deleteProfile(idOrSlug, db) {
|
|
496
|
+
const d = db || getDatabase();
|
|
497
|
+
const existing = getProfile(idOrSlug, d);
|
|
498
|
+
d.run("DELETE FROM profiles WHERE id = ?", [existing.id]);
|
|
499
|
+
}
|
|
500
|
+
function addConfigToProfile(profileIdOrSlug, configId, db, binding) {
|
|
501
|
+
const d = db || getDatabase();
|
|
502
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
503
|
+
const maxRow = d.query("SELECT MAX(sort_order) as max_order FROM profile_configs WHERE profile_id = ?").get(profile.id);
|
|
504
|
+
const order = (maxRow?.max_order ?? -1) + 1;
|
|
505
|
+
d.run("INSERT OR IGNORE INTO profile_configs (profile_id, config_id, sort_order, binding) VALUES (?, ?, ?, ?)", [profile.id, configId, order, JSON.stringify(normalizeProfileConfigBinding(binding))]);
|
|
506
|
+
}
|
|
507
|
+
function setProfileConfigBinding(profileIdOrSlug, configId, binding, db) {
|
|
508
|
+
const d = db || getDatabase();
|
|
509
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
510
|
+
const normalized = normalizeProfileConfigBinding(binding);
|
|
511
|
+
const result = d.run("UPDATE profile_configs SET binding = ? WHERE profile_id = ? AND config_id = ?", [JSON.stringify(normalized), profile.id, configId]);
|
|
512
|
+
if (result.changes !== 1)
|
|
513
|
+
throw new Error(`Config ${configId} is not a member of profile ${profile.slug}.`);
|
|
514
|
+
return getProfileConfigBindings(profile.id, d).find((row) => row.config_id === configId);
|
|
515
|
+
}
|
|
516
|
+
function getProfileConfigBindings(profileIdOrSlug, db) {
|
|
517
|
+
const d = db || getDatabase();
|
|
518
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
519
|
+
return d.query("SELECT profile_id, config_id, sort_order, binding FROM profile_configs WHERE profile_id = ? ORDER BY sort_order, config_id").all(profile.id).map((row) => ({
|
|
520
|
+
profile_id: row.profile_id,
|
|
521
|
+
config_id: row.config_id,
|
|
522
|
+
sort_order: row.sort_order,
|
|
523
|
+
binding: row.binding ? normalizeProfileConfigBinding(row.binding) : legacyProfileConfigBinding()
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
function removeConfigFromProfile(profileIdOrSlug, configId, db) {
|
|
527
|
+
const d = db || getDatabase();
|
|
528
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
529
|
+
d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
|
|
530
|
+
}
|
|
531
|
+
function addAssetToProfile(profileIdOrSlug, sourceConfigId, binding, db) {
|
|
532
|
+
const d = db || getDatabase();
|
|
533
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
534
|
+
getConfigById(sourceConfigId, d);
|
|
535
|
+
const normalized = normalizeProfileAssetBinding(binding);
|
|
536
|
+
const maxRow = d.query("SELECT MAX(sort_order) AS max_order FROM profile_assets WHERE profile_id = ?").get(profile.id);
|
|
537
|
+
const order = (maxRow?.max_order ?? -1) + 1;
|
|
538
|
+
d.run("INSERT INTO profile_assets (profile_id, source_config_id, asset_key, sort_order, binding) VALUES (?, ?, ?, ?, ?)", [profile.id, sourceConfigId, normalized.assetKey, order, JSON.stringify(normalized)]);
|
|
539
|
+
return getProfileAssetBindings(profile.id, d).find((row) => row.binding.assetKey === normalized.assetKey);
|
|
540
|
+
}
|
|
541
|
+
function setProfileAssetBinding(profileIdOrSlug, assetKey, binding, db) {
|
|
542
|
+
const d = db || getDatabase();
|
|
543
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
544
|
+
const normalized = normalizeProfileAssetBinding(binding);
|
|
545
|
+
if (normalized.assetKey !== assetKey)
|
|
546
|
+
throw new Error(`Asset binding key ${normalized.assetKey} does not match route key ${assetKey}.`);
|
|
547
|
+
const result = d.run("UPDATE profile_assets SET binding = ? WHERE profile_id = ? AND asset_key = ?", [JSON.stringify(normalized), profile.id, assetKey]);
|
|
548
|
+
if (result.changes !== 1)
|
|
549
|
+
throw new Error(`Asset ${assetKey} is not a member of profile ${profile.slug}.`);
|
|
550
|
+
return getProfileAssetBindings(profile.id, d).find((row) => row.binding.assetKey === assetKey);
|
|
551
|
+
}
|
|
552
|
+
function getProfileAssetBindings(profileIdOrSlug, db) {
|
|
553
|
+
const d = db || getDatabase();
|
|
554
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
555
|
+
return d.query("SELECT profile_id, source_config_id, sort_order, binding FROM profile_assets WHERE profile_id = ? ORDER BY sort_order, asset_key").all(profile.id).map((row) => ({
|
|
556
|
+
profile_id: row.profile_id,
|
|
557
|
+
source_config_id: row.source_config_id,
|
|
558
|
+
sort_order: row.sort_order,
|
|
559
|
+
binding: normalizeProfileAssetBinding(row.binding)
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
function removeAssetFromProfile(profileIdOrSlug, assetKey, db) {
|
|
563
|
+
const d = db || getDatabase();
|
|
564
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
565
|
+
d.run("DELETE FROM profile_assets WHERE profile_id = ? AND asset_key = ?", [profile.id, assetKey]);
|
|
566
|
+
}
|
|
567
|
+
function getProfileConfigsPage(profileIdOrSlug, options = {}, db) {
|
|
568
|
+
const d = db || getDatabase();
|
|
569
|
+
const profile = getProfile(profileIdOrSlug, d);
|
|
570
|
+
const normalized = normalizeBoundedReadOptions(options);
|
|
571
|
+
const total = d.query("SELECT COUNT(*) AS total FROM profile_configs WHERE profile_id = ?").get(profile.id)?.total ?? 0;
|
|
572
|
+
const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order LIMIT ? OFFSET ?").all(profile.id, normalized.limit, normalized.cursor);
|
|
573
|
+
return boundedReadPage(rows.map((row) => getConfigById(row.config_id, d)), total, normalized);
|
|
574
|
+
}
|
|
575
|
+
function profileHasSelectors(profile) {
|
|
576
|
+
const selectors = profile.selectors ?? {};
|
|
577
|
+
return (selectors.os?.length ?? 0) > 0 || (selectors.arch?.length ?? 0) > 0 || (selectors.hostnames?.length ?? 0) > 0;
|
|
578
|
+
}
|
|
579
|
+
function profileMatchesMachine(profile, machine) {
|
|
580
|
+
const selectors = profile.selectors ?? {};
|
|
581
|
+
const osMatches = !selectors.os?.length || selectors.os.some((candidate) => {
|
|
582
|
+
const value = candidate.trim().toLowerCase();
|
|
583
|
+
return value === machine.os_family || value === (machine.os ?? "").trim().toLowerCase() || normalizeOsFamily(candidate) === machine.os_family;
|
|
584
|
+
});
|
|
585
|
+
const archMatches = !selectors.arch?.length || selectors.arch.some((candidate) => candidate.trim().toLowerCase() === (machine.arch ?? "").trim().toLowerCase());
|
|
586
|
+
const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
|
|
587
|
+
return osMatches && archMatches && hostnameMatches;
|
|
588
|
+
}
|
|
589
|
+
function resolveProfileForMachineRead(machine = detectMachineContext(), options = {}, db) {
|
|
590
|
+
const d = db || getDatabase();
|
|
591
|
+
const { limit } = normalizeBoundedReadOptions(options);
|
|
592
|
+
let cursor = 0;
|
|
593
|
+
let scanned = 0;
|
|
594
|
+
let total = 0;
|
|
595
|
+
let selected = null;
|
|
596
|
+
while (true) {
|
|
597
|
+
const page = listProfilesPage({ limit, cursor }, d);
|
|
598
|
+
total = page.total;
|
|
599
|
+
scanned += page.items.length;
|
|
600
|
+
for (const profile of page.items) {
|
|
601
|
+
if (!profileHasSelectors(profile) || !profileMatchesMachine(profile, machine))
|
|
602
|
+
continue;
|
|
603
|
+
const selectors = profile.selectors;
|
|
604
|
+
const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
|
|
605
|
+
if (!selected || score > selected.score || score === selected.score && profile.name.localeCompare(selected.profile.name) < 0) {
|
|
606
|
+
selected = { profile, score };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (page.complete)
|
|
610
|
+
break;
|
|
611
|
+
cursor = page.next_cursor;
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
profile: selected?.profile ?? null,
|
|
615
|
+
scanned,
|
|
616
|
+
total,
|
|
617
|
+
batch_limit: limit,
|
|
618
|
+
source_bounded: true,
|
|
619
|
+
complete: true,
|
|
620
|
+
truncated: false
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
// src/db/machines.ts
|
|
624
|
+
import { arch, hostname, type } from "os";
|
|
625
|
+
function currentHostname() {
|
|
626
|
+
return hostname();
|
|
627
|
+
}
|
|
628
|
+
function currentOs() {
|
|
629
|
+
return type();
|
|
630
|
+
}
|
|
631
|
+
function currentArch() {
|
|
632
|
+
return arch();
|
|
633
|
+
}
|
|
634
|
+
function registerMachine(hostnameStr, os, archStr, db) {
|
|
635
|
+
const d = db || getDatabase();
|
|
636
|
+
const h = hostnameStr ?? currentHostname();
|
|
637
|
+
const o = os ?? currentOs();
|
|
638
|
+
const a = archStr ?? currentArch();
|
|
639
|
+
const existing = d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
|
|
640
|
+
if (existing) {
|
|
641
|
+
if (existing.os !== o || existing.arch !== a) {
|
|
642
|
+
d.run("UPDATE machines SET os = ?, arch = ? WHERE hostname = ?", [o, a, h]);
|
|
643
|
+
return d.query("SELECT * FROM machines WHERE hostname = ?").get(h);
|
|
644
|
+
}
|
|
645
|
+
return existing;
|
|
646
|
+
}
|
|
647
|
+
const id = uuid();
|
|
648
|
+
const ts = now();
|
|
649
|
+
d.run("INSERT INTO machines (id, hostname, os, arch, last_applied_at, created_at) VALUES (?, ?, ?, ?, NULL, ?)", [id, h, o, a, ts]);
|
|
650
|
+
return d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
651
|
+
}
|
|
652
|
+
function updateMachineApplied(hostnameStr, db) {
|
|
653
|
+
const d = db || getDatabase();
|
|
654
|
+
const h = hostnameStr ?? currentHostname();
|
|
655
|
+
d.run("UPDATE machines SET last_applied_at = ? WHERE hostname = ?", [now(), h]);
|
|
656
|
+
}
|
|
657
|
+
function listMachines(db) {
|
|
658
|
+
const d = db || getDatabase();
|
|
659
|
+
return d.query("SELECT * FROM machines ORDER BY last_applied_at DESC NULLS LAST").all();
|
|
660
|
+
}
|
|
661
|
+
export {
|
|
662
|
+
updateProfile,
|
|
663
|
+
updateMachineApplied,
|
|
664
|
+
updateConfig,
|
|
665
|
+
setProfileConfigBinding,
|
|
666
|
+
setProfileAssetBinding,
|
|
667
|
+
resolveProfileForMachineRead,
|
|
668
|
+
resetLocalDatabase,
|
|
669
|
+
removeConfigFromProfile,
|
|
670
|
+
removeAssetFromProfile,
|
|
671
|
+
registerMachine,
|
|
672
|
+
pruneSnapshots,
|
|
673
|
+
listSnapshots,
|
|
674
|
+
listProfilesPage,
|
|
675
|
+
listMachines,
|
|
676
|
+
listConfigs,
|
|
677
|
+
insertFeedback,
|
|
678
|
+
getSnapshotByVersion,
|
|
679
|
+
getSnapshot,
|
|
680
|
+
getProfileConfigsPage,
|
|
681
|
+
getProfileConfigBindings,
|
|
682
|
+
getProfileAssetBindings,
|
|
683
|
+
getProfile,
|
|
684
|
+
getConfigStats,
|
|
685
|
+
getConfigById,
|
|
686
|
+
getConfig,
|
|
687
|
+
deleteProfile,
|
|
688
|
+
deleteConfig,
|
|
689
|
+
createSnapshot,
|
|
690
|
+
createProfile,
|
|
691
|
+
createConfig,
|
|
692
|
+
addConfigToProfile,
|
|
693
|
+
addAssetToProfile
|
|
694
|
+
};
|