@hasna/instructions 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +123 -6
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-7svah2qm.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-jp13sa1e.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-85n5wnd8.js → index-1cnyjnd7.js} +5 -7
  6. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  7. package/dist/chunks/{index-v7dkb384.js → index-4pfq42t5.js} +129 -77
  8. package/dist/chunks/index-5d7zdftc.js +186 -0
  9. package/dist/chunks/{index-1hpwskwd.js → index-7m4j8a33.js} +205 -329
  10. package/dist/chunks/index-8rb7ng2y.js +148 -0
  11. package/dist/chunks/index-arrrx0ed.js +344 -0
  12. package/dist/chunks/{index-9p9gn7gh.js → index-bzyxvwcd.js} +5 -7
  13. package/dist/chunks/index-fpeyzn3n.js +186 -0
  14. package/dist/chunks/{index-axhwg61p.js → index-fwbs6tma.js} +15 -4
  15. package/dist/chunks/index-n3fjspr0.js +119 -0
  16. package/dist/chunks/{index-e9epbsvs.js → index-z3ma4aqk.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-5wvv5xv3.js} +2 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-zxa5dpf4.js} +13 -188
  20. package/dist/chunks/s3-backup-n2z4fwyh.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-90tgbh6k.js +14 -0
  23. package/dist/chunks/{sync-m46fc257.js → sync-2075deve.js} +5 -4
  24. package/dist/chunks/{sync-vy1yjvs6.js → sync-r8t7zsp3.js} +4 -4
  25. package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
  26. package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
  27. package/dist/cli/index.js +1011 -247
  28. package/dist/data/config-store.d.ts +1 -0
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/index.js +840 -215
  31. package/dist/lib/apply.d.ts +7 -0
  32. package/dist/lib/apply.d.ts.map +1 -1
  33. package/dist/lib/compact-output.d.ts +1 -0
  34. package/dist/lib/compact-output.d.ts.map +1 -1
  35. package/dist/lib/export.d.ts +22 -3
  36. package/dist/lib/export.d.ts.map +1 -1
  37. package/dist/lib/import.d.ts +18 -0
  38. package/dist/lib/import.d.ts.map +1 -1
  39. package/dist/lib/legacy-store-migration.d.ts +39 -0
  40. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  41. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  42. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  43. package/dist/lib/sync-dir.d.ts.map +1 -1
  44. package/dist/mcp/index.js +31 -20
  45. package/dist/sdk/index.d.ts +2 -0
  46. package/dist/sdk/index.d.ts.map +1 -1
  47. package/dist/sdk/index.js +205 -10
  48. package/dist/sdk/resolve.d.ts +1 -1
  49. package/dist/sdk/resolve.d.ts.map +1 -1
  50. package/dist/sdk/v1-client.d.ts +57 -0
  51. package/dist/sdk/v1-client.d.ts.map +1 -0
  52. package/dist/sdk/v1.generated.d.ts +212 -21
  53. package/dist/sdk/v1.generated.d.ts.map +1 -1
  54. package/dist/server/cloud.d.ts.map +1 -1
  55. package/dist/server/index.d.ts +13 -5
  56. package/dist/server/index.d.ts.map +1 -1
  57. package/dist/server/index.js +1180 -432
  58. package/dist/server/openapi.d.ts +1264 -92
  59. package/dist/server/openapi.d.ts.map +1 -1
  60. package/dist/server/v1.d.ts +25 -1
  61. package/dist/server/v1.d.ts.map +1 -1
  62. package/dist/storage/cloud-store.d.ts +57 -1
  63. package/dist/storage/cloud-store.d.ts.map +1 -1
  64. package/dist/storage/index.d.ts +7 -0
  65. package/dist/storage/index.d.ts.map +1 -0
  66. package/dist/storage/index.js +725 -0
  67. package/dist/storage/s3-backup.d.ts +76 -0
  68. package/dist/storage/s3-backup.d.ts.map +1 -0
  69. package/dist/storage/s3-config.d.ts +46 -0
  70. package/dist/storage/s3-config.d.ts.map +1 -0
  71. package/dist/storage/s3-object-store.d.ts +70 -0
  72. package/dist/storage/s3-object-store.d.ts.map +1 -0
  73. package/dist/types/index.d.ts +65 -3
  74. package/dist/types/index.d.ts.map +1 -1
  75. package/hasna.contract.json +110 -0
  76. package/migrations/0001_instructions.sql +63 -0
  77. package/migrations/0002_api_keys.sql +20 -0
  78. package/migrations/0003_idempotency_receipts.sql +24 -0
  79. package/package.json +22 -11
  80. package/assets/skills/inbox/SKILL.md +0 -86
@@ -0,0 +1,322 @@
1
+ // @bun
2
+ import"./index-bsan4c16.js";
3
+
4
+ // src/lib/legacy-store-migration.ts
5
+ import { Database } from "bun:sqlite";
6
+ import { chmodSync, constants, copyFileSync, existsSync, mkdirSync, statSync } from "fs";
7
+ import { createHash } from "crypto";
8
+ import { dirname, join, resolve } from "path";
9
+ import { homedir } from "os";
10
+
11
+ class LegacyStoreMigrationError extends Error {
12
+ code;
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.name = "LegacyStoreMigrationError";
16
+ this.code = code;
17
+ }
18
+ }
19
+ function emptyCounts() {
20
+ return { configs: 0, snapshots: 0, profiles: 0, profileConfigs: 0, machines: 0 };
21
+ }
22
+ function legacyInstructionsStorePaths(home = process.env.HOME || homedir()) {
23
+ return {
24
+ sourcePath: join(home, ".hasna", "configs", "configs.db"),
25
+ destinationPath: join(home, ".hasna", "instructions", "instructions.db")
26
+ };
27
+ }
28
+ function tableExists(db, table) {
29
+ return db.query("SELECT 1 AS present FROM sqlite_master WHERE type = 'table' AND name = ?").get(table)?.present === 1;
30
+ }
31
+ function columns(db, table) {
32
+ if (!tableExists(db, table))
33
+ return new Set;
34
+ return new Set(db.query(`PRAGMA table_info(${table})`).all().map((row) => row.name));
35
+ }
36
+ function rows(db, table) {
37
+ if (!tableExists(db, table))
38
+ return [];
39
+ return db.query(`SELECT * FROM ${table} ORDER BY rowid`).all();
40
+ }
41
+ function value(row, name, fallback) {
42
+ return Object.prototype.hasOwnProperty.call(row, name) && row[name] !== undefined ? row[name] : fallback;
43
+ }
44
+ function normalizeJson(valueToNormalize, fallback) {
45
+ if (typeof valueToNormalize !== "string")
46
+ return fallback;
47
+ try {
48
+ JSON.parse(valueToNormalize);
49
+ return valueToNormalize;
50
+ } catch {
51
+ return fallback;
52
+ }
53
+ }
54
+ function compatibleConfig(row) {
55
+ return {
56
+ id: value(row, "id", ""),
57
+ name: value(row, "name", ""),
58
+ slug: value(row, "slug", ""),
59
+ kind: value(row, "kind", "file"),
60
+ category: value(row, "category", "rules"),
61
+ agent: value(row, "agent", "global"),
62
+ target_path: value(row, "target_path", null),
63
+ outputs: normalizeJson(value(row, "outputs", "[]"), "[]"),
64
+ format: value(row, "format", "text"),
65
+ content: value(row, "content", ""),
66
+ description: value(row, "description", null),
67
+ tags: normalizeJson(value(row, "tags", "[]"), "[]"),
68
+ is_template: value(row, "is_template", 0),
69
+ version: value(row, "version", 1),
70
+ created_at: value(row, "created_at", "1970-01-01T00:00:00.000Z"),
71
+ updated_at: value(row, "updated_at", value(row, "created_at", "1970-01-01T00:00:00.000Z")),
72
+ synced_at: value(row, "synced_at", null)
73
+ };
74
+ }
75
+ function compatibleProfile(row) {
76
+ return {
77
+ id: value(row, "id", ""),
78
+ name: value(row, "name", ""),
79
+ slug: value(row, "slug", ""),
80
+ description: value(row, "description", null),
81
+ selectors: normalizeJson(value(row, "selectors", "{}"), "{}"),
82
+ variables: normalizeJson(value(row, "variables", "{}"), "{}"),
83
+ created_at: value(row, "created_at", "1970-01-01T00:00:00.000Z"),
84
+ updated_at: value(row, "updated_at", value(row, "created_at", "1970-01-01T00:00:00.000Z"))
85
+ };
86
+ }
87
+ function compatibleMachine(row) {
88
+ return {
89
+ id: value(row, "id", ""),
90
+ hostname: value(row, "hostname", ""),
91
+ os: value(row, "os", null),
92
+ arch: value(row, "arch", null),
93
+ last_applied_at: value(row, "last_applied_at", null),
94
+ created_at: value(row, "created_at", "1970-01-01T00:00:00.000Z")
95
+ };
96
+ }
97
+ function stableSnapshotId(config) {
98
+ const digest = createHash("sha256").update(String(config.id)).update("\x00").update(String(config.version)).update("\x00").update(String(config.content)).digest("hex").slice(0, 32);
99
+ return `legacy-current-${digest}`;
100
+ }
101
+ function countDestinationRows(db) {
102
+ return ["configs", "config_snapshots", "profiles", "profile_configs", "machines"].reduce((total, table) => {
103
+ if (!tableExists(db, table))
104
+ return total;
105
+ return total + (db.query(`SELECT COUNT(*) AS count FROM ${table}`).get()?.count ?? 0);
106
+ }, 0);
107
+ }
108
+ function requireCurrentSchema(db) {
109
+ const required = {
110
+ configs: ["id", "slug", "kind", "outputs", "content", "version"],
111
+ config_snapshots: ["id", "config_id", "content", "version"],
112
+ profiles: ["id", "slug", "selectors", "variables"],
113
+ profile_configs: ["profile_id", "config_id", "binding"],
114
+ machines: ["id", "hostname", "arch"]
115
+ };
116
+ for (const [table, names] of Object.entries(required)) {
117
+ const actual = columns(db, table);
118
+ if (names.some((name) => !actual.has(name))) {
119
+ throw new LegacyStoreMigrationError("DESTINATION_SCHEMA_INCOMPATIBLE", `instructions legacy migration: destination is not a current Instructions SQLite database (incompatible ${table} table)`);
120
+ }
121
+ }
122
+ }
123
+ function sameConfig(a, b) {
124
+ const keys = ["id", "name", "slug", "kind", "category", "agent", "target_path", "outputs", "format", "content", "description", "tags", "is_template", "version", "created_at", "updated_at", "synced_at"];
125
+ return keys.every((key) => String(a[key] ?? "") === String(b[key] ?? ""));
126
+ }
127
+ function buildPlan(source, destination, mergePolicy) {
128
+ const sourceCounts = emptyCounts();
129
+ const migrated = emptyCounts();
130
+ const skipped = emptyCounts();
131
+ const planned = [];
132
+ let conflicts = 0;
133
+ let snapshotsCreated = 0;
134
+ const sourceConfigs = rows(source, "configs").map(compatibleConfig);
135
+ const sourceSnapshots = rows(source, "config_snapshots");
136
+ const sourceProfiles = rows(source, "profiles").map(compatibleProfile);
137
+ const sourceProfileConfigs = rows(source, "profile_configs");
138
+ const sourceMachines = rows(source, "machines").map(compatibleMachine);
139
+ sourceCounts.configs = sourceConfigs.length;
140
+ sourceCounts.snapshots = sourceSnapshots.length;
141
+ sourceCounts.profiles = sourceProfiles.length;
142
+ sourceCounts.profileConfigs = sourceProfileConfigs.length;
143
+ sourceCounts.machines = sourceMachines.length;
144
+ const acceptedConfigIds = new Set;
145
+ for (const config of sourceConfigs) {
146
+ const id = String(config.id);
147
+ const existing = destination.query("SELECT * FROM configs WHERE id = ? OR slug = ? LIMIT 1").get(id, String(config.slug));
148
+ if (existing) {
149
+ skipped.configs++;
150
+ if (!sameConfig(existing, config))
151
+ conflicts++;
152
+ if (mergePolicy === "preserve-destination" && String(existing.id) === id && sameConfig(existing, config)) {
153
+ acceptedConfigIds.add(id);
154
+ }
155
+ continue;
156
+ }
157
+ acceptedConfigIds.add(id);
158
+ planned.push({ table: "configs", values: config });
159
+ migrated.configs++;
160
+ }
161
+ const snapshotIdentity = new Set;
162
+ for (const snapshot of sourceSnapshots) {
163
+ const configId = String(value(snapshot, "config_id", ""));
164
+ if (!acceptedConfigIds.has(configId)) {
165
+ skipped.snapshots++;
166
+ continue;
167
+ }
168
+ const normalized = {
169
+ id: value(snapshot, "id", ""),
170
+ config_id: configId,
171
+ content: value(snapshot, "content", ""),
172
+ version: value(snapshot, "version", 1),
173
+ created_at: value(snapshot, "created_at", "1970-01-01T00:00:00.000Z")
174
+ };
175
+ const identity = `${configId}\x00${normalized.version}\x00${normalized.content}`;
176
+ snapshotIdentity.add(identity);
177
+ const existing = destination.query("SELECT id FROM config_snapshots WHERE id = ?").get(String(normalized.id));
178
+ if (existing)
179
+ skipped.snapshots++;
180
+ else {
181
+ planned.push({ table: "snapshots", values: normalized });
182
+ migrated.snapshots++;
183
+ }
184
+ }
185
+ for (const config of sourceConfigs) {
186
+ const configId = String(config.id);
187
+ if (!acceptedConfigIds.has(configId))
188
+ continue;
189
+ const identity = `${configId}\x00${config.version}\x00${config.content}`;
190
+ const destinationHas = destination.query("SELECT 1 AS present FROM config_snapshots WHERE config_id = ? AND version = ? AND content = ? LIMIT 1").get(configId, Number(config.version), String(config.content));
191
+ if (!snapshotIdentity.has(identity) && !destinationHas) {
192
+ planned.push({ table: "snapshots", values: {
193
+ id: stableSnapshotId(config),
194
+ config_id: configId,
195
+ content: config.content,
196
+ version: config.version,
197
+ created_at: config.updated_at
198
+ } });
199
+ migrated.snapshots++;
200
+ snapshotsCreated++;
201
+ }
202
+ }
203
+ const acceptedProfileIds = new Set;
204
+ for (const profile of sourceProfiles) {
205
+ const existing = destination.query("SELECT id FROM profiles WHERE id = ? OR slug = ? LIMIT 1").get(String(profile.id), String(profile.slug));
206
+ if (existing) {
207
+ skipped.profiles++;
208
+ if (String(existing.id) !== String(profile.id))
209
+ conflicts++;
210
+ } else {
211
+ acceptedProfileIds.add(String(profile.id));
212
+ planned.push({ table: "profiles", values: profile });
213
+ migrated.profiles++;
214
+ }
215
+ }
216
+ for (const link of sourceProfileConfigs) {
217
+ const profileId = String(value(link, "profile_id", ""));
218
+ const configId = String(value(link, "config_id", ""));
219
+ if (!acceptedProfileIds.has(profileId) || !acceptedConfigIds.has(configId)) {
220
+ skipped.profileConfigs++;
221
+ continue;
222
+ }
223
+ planned.push({ table: "profileConfigs", values: {
224
+ profile_id: profileId,
225
+ config_id: configId,
226
+ sort_order: value(link, "sort_order", 0),
227
+ binding: normalizeJson(value(link, "binding", '{"schema":"hasna.instructions.profile-config-binding/v1","activation":{"mode":"always"},"required":true,"fallback":"fail"}'), "{}")
228
+ } });
229
+ migrated.profileConfigs++;
230
+ }
231
+ for (const machine of sourceMachines) {
232
+ const existing = destination.query("SELECT id FROM machines WHERE id = ? OR hostname = ? LIMIT 1").get(String(machine.id), String(machine.hostname));
233
+ if (existing) {
234
+ skipped.machines++;
235
+ if (String(existing.id) !== String(machine.id))
236
+ conflicts++;
237
+ } else {
238
+ planned.push({ table: "machines", values: machine });
239
+ migrated.machines++;
240
+ }
241
+ }
242
+ return { source: sourceCounts, migrated, skipped, conflicts, snapshotsCreated, rows: planned };
243
+ }
244
+ function insertRow(db, row) {
245
+ const table = row.table === "snapshots" ? "config_snapshots" : row.table === "profileConfigs" ? "profile_configs" : row.table;
246
+ const names = Object.keys(row.values);
247
+ const placeholders = names.map(() => "?").join(", ");
248
+ db.run(`INSERT INTO ${table} (${names.join(", ")}) VALUES (${placeholders})`, names.map((name) => row.values[name]));
249
+ }
250
+ function migrateLegacyInstructionsStore(options) {
251
+ if (!options.operatorConfirmed) {
252
+ throw new LegacyStoreMigrationError("LOCAL_OPERATOR_CONFIRMATION_REQUIRED", "instructions legacy migration requires explicit local operator confirmation");
253
+ }
254
+ const defaults = legacyInstructionsStorePaths();
255
+ const sourcePath = resolve(options.sourcePath ?? defaults.sourcePath);
256
+ const destinationPath = resolve(options.destinationPath ?? defaults.destinationPath);
257
+ const dryRun = options.dryRun ?? true;
258
+ if (sourcePath === destinationPath) {
259
+ throw new LegacyStoreMigrationError("SOURCE_DESTINATION_SAME", "instructions legacy migration source and destination must differ");
260
+ }
261
+ if (!existsSync(sourcePath) || !statSync(sourcePath).isFile()) {
262
+ throw new LegacyStoreMigrationError("SOURCE_NOT_FOUND", "instructions legacy migration source database was not found");
263
+ }
264
+ if (!existsSync(destinationPath) || !statSync(destinationPath).isFile()) {
265
+ throw new LegacyStoreMigrationError("DESTINATION_NOT_FOUND", "instructions legacy migration destination must be an initialized current Instructions SQLite database");
266
+ }
267
+ const source = new Database(sourcePath, { readonly: true, strict: true });
268
+ const destination = new Database(destinationPath, { strict: true });
269
+ let backupPath = null;
270
+ try {
271
+ source.run("PRAGMA query_only = ON");
272
+ destination.run("PRAGMA foreign_keys = ON");
273
+ requireCurrentSchema(destination);
274
+ const destinationRows = countDestinationRows(destination);
275
+ if (destinationRows > 0 && options.mergePolicy === undefined) {
276
+ throw new LegacyStoreMigrationError("DESTINATION_NOT_EMPTY", "instructions legacy migration refuses a non-empty destination without an explicit merge policy");
277
+ }
278
+ const plan = buildPlan(source, destination, options.mergePolicy);
279
+ if (!dryRun) {
280
+ backupPath = resolve(options.backupPath ?? `${destinationPath}.before-legacy-migration.bak`);
281
+ if (backupPath === sourcePath || backupPath === destinationPath) {
282
+ throw new LegacyStoreMigrationError("INVALID_BACKUP_PATH", "instructions legacy migration backup path must be separate");
283
+ }
284
+ if (existsSync(backupPath)) {
285
+ throw new LegacyStoreMigrationError("BACKUP_ALREADY_EXISTS", "instructions legacy migration refuses to overwrite an existing destination backup");
286
+ }
287
+ mkdirSync(dirname(backupPath), { recursive: true, mode: 448 });
288
+ destination.run("PRAGMA wal_checkpoint(FULL)");
289
+ copyFileSync(destinationPath, backupPath, constants.COPYFILE_EXCL);
290
+ chmodSync(backupPath, 384);
291
+ const apply = destination.transaction(() => {
292
+ for (const row of plan.rows)
293
+ insertRow(destination, row);
294
+ });
295
+ apply();
296
+ }
297
+ return {
298
+ dryRun,
299
+ source: plan.source,
300
+ migrated: plan.migrated,
301
+ skipped: plan.skipped,
302
+ conflicts: plan.conflicts,
303
+ snapshotsCreated: plan.snapshotsCreated,
304
+ backupCreated: !dryRun,
305
+ sourcePath,
306
+ destinationPath,
307
+ backupPath
308
+ };
309
+ } catch (error) {
310
+ if (error instanceof LegacyStoreMigrationError)
311
+ throw error;
312
+ throw new LegacyStoreMigrationError("MIGRATION_FAILED", `instructions legacy migration failed safely: ${error instanceof Error ? error.name : "unknown error"}`);
313
+ } finally {
314
+ source.close();
315
+ destination.close();
316
+ }
317
+ }
318
+ export {
319
+ migrateLegacyInstructionsStore,
320
+ legacyInstructionsStorePaths,
321
+ LegacyStoreMigrationError
322
+ };
@@ -14,12 +14,12 @@ import {
14
14
  normalizeOsFamily,
15
15
  normalizeProfileAssetBinding,
16
16
  normalizeProfileConfigBinding
17
- } from "./index-v7dkb384.js";
17
+ } from "./index-4pfq42t5.js";
18
18
  import"./index-35z65jsa.js";
19
19
  import {
20
20
  ConfigNotFoundError,
21
21
  ProfileNotFoundError
22
- } from "./index-pkscjnj5.js";
22
+ } from "./index-3djthcm6.js";
23
23
  import"./index-nnwp92tk.js";
24
24
 
25
25
  // src/db/database.ts
@@ -1,205 +1,30 @@
1
1
  // @bun
2
- import {
3
- now,
4
- slugify,
5
- uuid
6
- } from "./index-ma2xmp6c.js";
7
2
  import {
8
3
  boundedReadPage,
9
4
  detectMachineContext,
10
- getRawStoreRoot,
11
- hasInstructionsEnvAuthorityIntent,
12
5
  legacyProfileConfigBinding,
13
6
  normalizeBoundedReadOptions,
14
7
  normalizeOsFamily,
15
8
  normalizeProfileAssetBinding,
16
9
  normalizeProfileConfigBinding
17
- } from "./index-1hpwskwd.js";
10
+ } from "./index-7m4j8a33.js";
18
11
  import {
19
12
  ConfigNotFoundError,
20
13
  ProfileNotFoundError
21
- } from "./index-pkscjnj5.js";
14
+ } from "./index-n3fjspr0.js";
15
+ import {
16
+ getDatabase,
17
+ insertFeedback,
18
+ resetLocalDatabase
19
+ } from "./index-5d7zdftc.js";
20
+ import {
21
+ now,
22
+ slugify,
23
+ uuid
24
+ } from "./index-ma2xmp6c.js";
25
+ import"./index-fpeyzn3n.js";
22
26
  import"./index-bsan4c16.js";
23
27
 
24
- // src/db/database.ts
25
- import { Database } from "bun:sqlite";
26
- import { existsSync, mkdirSync, rmSync } from "fs";
27
- import { join } from "path";
28
- function getDbPath() {
29
- if (process.env["HASNA_INSTRUCTIONS_DB_PATH"]) {
30
- return process.env["HASNA_INSTRUCTIONS_DB_PATH"];
31
- }
32
- const dir = getRawStoreRoot();
33
- mkdirSync(dir, { recursive: true });
34
- return join(dir, "instructions.db");
35
- }
36
- var MIGRATIONS = [
37
- `
38
- CREATE TABLE IF NOT EXISTS configs (
39
- id TEXT PRIMARY KEY,
40
- name TEXT NOT NULL,
41
- slug TEXT NOT NULL UNIQUE,
42
- kind TEXT NOT NULL DEFAULT 'file',
43
- category TEXT NOT NULL,
44
- agent TEXT NOT NULL DEFAULT 'global',
45
- target_path TEXT,
46
- format TEXT NOT NULL DEFAULT 'text',
47
- content TEXT NOT NULL DEFAULT '',
48
- description TEXT,
49
- tags TEXT NOT NULL DEFAULT '[]',
50
- is_template INTEGER NOT NULL DEFAULT 0,
51
- version INTEGER NOT NULL DEFAULT 1,
52
- created_at TEXT NOT NULL,
53
- updated_at TEXT NOT NULL,
54
- synced_at TEXT
55
- );
56
-
57
- CREATE TABLE IF NOT EXISTS config_snapshots (
58
- id TEXT PRIMARY KEY,
59
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
60
- content TEXT NOT NULL,
61
- version INTEGER NOT NULL,
62
- created_at TEXT NOT NULL
63
- );
64
-
65
- CREATE TABLE IF NOT EXISTS profiles (
66
- id TEXT PRIMARY KEY,
67
- name TEXT NOT NULL,
68
- slug TEXT NOT NULL UNIQUE,
69
- description TEXT,
70
- created_at TEXT NOT NULL,
71
- updated_at TEXT NOT NULL
72
- );
73
-
74
- CREATE TABLE IF NOT EXISTS profile_configs (
75
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
76
- config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
77
- sort_order INTEGER NOT NULL DEFAULT 0,
78
- PRIMARY KEY (profile_id, config_id)
79
- );
80
-
81
- CREATE TABLE IF NOT EXISTS machines (
82
- id TEXT PRIMARY KEY,
83
- hostname TEXT NOT NULL UNIQUE,
84
- os TEXT,
85
- last_applied_at TEXT,
86
- created_at TEXT NOT NULL
87
- );
88
-
89
- CREATE TABLE IF NOT EXISTS schema_version (
90
- version INTEGER PRIMARY KEY
91
- );
92
-
93
- INSERT OR IGNORE INTO schema_version (version) VALUES (1);
94
- `,
95
- `
96
- ALTER TABLE profiles ADD COLUMN selectors TEXT NOT NULL DEFAULT '{}';
97
- ALTER TABLE profiles ADD COLUMN variables TEXT NOT NULL DEFAULT '{}';
98
- ALTER TABLE machines ADD COLUMN arch TEXT;
99
- `,
100
- `
101
- ALTER TABLE configs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]';
102
- `,
103
- `
104
- 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"}';
105
- `,
106
- `
107
- CREATE TABLE IF NOT EXISTS profile_assets (
108
- profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
109
- source_config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
110
- asset_key TEXT NOT NULL,
111
- sort_order INTEGER NOT NULL DEFAULT 0,
112
- binding TEXT NOT NULL,
113
- PRIMARY KEY (profile_id, asset_key)
114
- );
115
- CREATE INDEX IF NOT EXISTS profile_assets_source_config_idx ON profile_assets (source_config_id);
116
- `
117
- ];
118
- var _db = null;
119
- function getDatabase(path) {
120
- if (_db)
121
- return _db;
122
- if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
123
- 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.");
124
- }
125
- const dbPath = path || getDbPath();
126
- const db = new Database(dbPath);
127
- db.run("PRAGMA journal_mode = WAL");
128
- db.run("PRAGMA foreign_keys = ON");
129
- applyMigrations(db);
130
- ensureFeedbackTable(db);
131
- _db = db;
132
- return db;
133
- }
134
- function resetDatabase() {
135
- if (_db) {
136
- try {
137
- _db.close();
138
- } catch {}
139
- }
140
- _db = null;
141
- }
142
- function resetLocalDatabase() {
143
- resetDatabase();
144
- const dbPath = getDbPath();
145
- if (dbPath === ":memory:")
146
- return;
147
- for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
148
- if (existsSync(p))
149
- rmSync(p);
150
- }
151
- }
152
- function applyMigrations(db) {
153
- let currentVersion = 0;
154
- try {
155
- const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
156
- currentVersion = row?.version ?? 0;
157
- } catch {
158
- currentVersion = 0;
159
- }
160
- const applyOne = db.transaction((index) => {
161
- if (!migrationEffectAlreadyPresent(db, index))
162
- db.exec(MIGRATIONS[index]);
163
- db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${index + 1})`);
164
- });
165
- for (let i = currentVersion;i < MIGRATIONS.length; i++)
166
- applyOne(i);
167
- }
168
- function migrationEffectAlreadyPresent(db, index) {
169
- if (index !== 3)
170
- return false;
171
- return db.query("PRAGMA table_info(profile_configs)").all().some((column) => column.name === "binding");
172
- }
173
- function ensureFeedbackTable(db) {
174
- db.exec(`
175
- CREATE TABLE IF NOT EXISTS feedback (
176
- id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
177
- message TEXT NOT NULL,
178
- email TEXT,
179
- category TEXT DEFAULT 'general',
180
- version TEXT,
181
- machine_id TEXT,
182
- created_at TEXT NOT NULL DEFAULT (datetime('now'))
183
- )
184
- `);
185
- const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
186
- const required = [
187
- ["email", "TEXT"],
188
- ["category", "TEXT DEFAULT 'general'"],
189
- ["version", "TEXT"],
190
- ["machine_id", "TEXT"],
191
- ["created_at", "TEXT"]
192
- ];
193
- for (const [name, def] of required) {
194
- if (!existing.has(name))
195
- db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
196
- }
197
- }
198
- function insertFeedback(input, db) {
199
- const d = db || getDatabase();
200
- d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
201
- }
202
-
203
28
  // src/db/snapshots.ts
204
29
  function createSnapshot(configId, content, version, db) {
205
30
  const d = db || getDatabase();