@hasna/instructions 0.6.1 → 0.7.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.
Files changed (85) hide show
  1. package/README.md +141 -10
  2. package/dist/chunks/{apply-ynfn1av7.js → apply-c3sd4agz.js} +5 -3
  3. package/dist/chunks/{apply-sfzja1f3.js → apply-pnn7a3y7.js} +6 -3
  4. package/dist/chunks/database-wstmr56z.js +23 -0
  5. package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
  6. package/dist/chunks/index-5d7zdftc.js +186 -0
  7. package/dist/chunks/{index-1hpwskwd.js → index-6jncnnw7.js} +318 -329
  8. package/dist/chunks/index-8rb7ng2y.js +148 -0
  9. package/dist/chunks/{index-v7dkb384.js → index-aj66f6xr.js} +243 -100
  10. package/dist/chunks/index-fpeyzn3n.js +186 -0
  11. package/dist/chunks/{index-9p9gn7gh.js → index-jg8ptcwh.js} +5 -7
  12. package/dist/chunks/index-mvckkf15.js +363 -0
  13. package/dist/chunks/index-n3fjspr0.js +119 -0
  14. package/dist/chunks/{index-85n5wnd8.js → index-pdpg8a44.js} +5 -7
  15. package/dist/chunks/{index-e9epbsvs.js → index-r7nfef2g.js} +15 -4
  16. package/dist/chunks/{index-axhwg61p.js → index-ypcef3a2.js} +15 -4
  17. package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
  18. package/dist/chunks/{local-a9nen65z.js → local-0a9gja58.js} +142 -2
  19. package/dist/chunks/{local-0d8qrc92.js → local-969ye538.js} +153 -188
  20. package/dist/chunks/s3-backup-hxe3zpfz.js +240 -0
  21. package/dist/chunks/s3-config-hhght91k.js +14 -0
  22. package/dist/chunks/s3-object-store-n0s82swt.js +14 -0
  23. package/dist/chunks/{sync-vy1yjvs6.js → sync-q5hvwq6e.js} +4 -4
  24. package/dist/chunks/{sync-m46fc257.js → sync-xtkbm5br.js} +5 -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 +1236 -256
  28. package/dist/data/config-store.d.ts +10 -1
  29. package/dist/data/config-store.d.ts.map +1 -1
  30. package/dist/db/configs.d.ts +4 -1
  31. package/dist/db/configs.d.ts.map +1 -1
  32. package/dist/db/local.d.ts +1 -1
  33. package/dist/db/local.d.ts.map +1 -1
  34. package/dist/index.js +1086 -215
  35. package/dist/lib/apply.d.ts +7 -0
  36. package/dist/lib/apply.d.ts.map +1 -1
  37. package/dist/lib/compact-output.d.ts +3 -18
  38. package/dist/lib/compact-output.d.ts.map +1 -1
  39. package/dist/lib/export.d.ts +22 -3
  40. package/dist/lib/export.d.ts.map +1 -1
  41. package/dist/lib/import.d.ts +18 -0
  42. package/dist/lib/import.d.ts.map +1 -1
  43. package/dist/lib/legacy-store-migration.d.ts +39 -0
  44. package/dist/lib/legacy-store-migration.d.ts.map +1 -0
  45. package/dist/lib/managed-skill-runtimes.d.ts +1 -0
  46. package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
  47. package/dist/lib/sync-dir.d.ts.map +1 -1
  48. package/dist/mcp/index.js +41 -30
  49. package/dist/mcp/server.d.ts.map +1 -1
  50. package/dist/sdk/index.d.ts +2 -0
  51. package/dist/sdk/index.d.ts.map +1 -1
  52. package/dist/sdk/index.js +229 -10
  53. package/dist/sdk/resolve.d.ts +1 -1
  54. package/dist/sdk/resolve.d.ts.map +1 -1
  55. package/dist/sdk/v1-client.d.ts +58 -0
  56. package/dist/sdk/v1-client.d.ts.map +1 -0
  57. package/dist/sdk/v1.generated.d.ts +242 -21
  58. package/dist/sdk/v1.generated.d.ts.map +1 -1
  59. package/dist/server/cloud.d.ts.map +1 -1
  60. package/dist/server/index.d.ts +13 -5
  61. package/dist/server/index.d.ts.map +1 -1
  62. package/dist/server/index.js +1261 -427
  63. package/dist/server/openapi.d.ts +1379 -93
  64. package/dist/server/openapi.d.ts.map +1 -1
  65. package/dist/server/v1.d.ts +25 -1
  66. package/dist/server/v1.d.ts.map +1 -1
  67. package/dist/storage/cloud-store.d.ts +45 -2
  68. package/dist/storage/cloud-store.d.ts.map +1 -1
  69. package/dist/storage/index.d.ts +7 -0
  70. package/dist/storage/index.d.ts.map +1 -0
  71. package/dist/storage/index.js +744 -0
  72. package/dist/storage/s3-backup.d.ts +76 -0
  73. package/dist/storage/s3-backup.d.ts.map +1 -0
  74. package/dist/storage/s3-config.d.ts +46 -0
  75. package/dist/storage/s3-config.d.ts.map +1 -0
  76. package/dist/storage/s3-object-store.d.ts +70 -0
  77. package/dist/storage/s3-object-store.d.ts.map +1 -0
  78. package/dist/types/index.d.ts +98 -3
  79. package/dist/types/index.d.ts.map +1 -1
  80. package/hasna.contract.json +110 -0
  81. package/migrations/0001_instructions.sql +63 -0
  82. package/migrations/0002_api_keys.sql +20 -0
  83. package/migrations/0003_idempotency_receipts.sql +24 -0
  84. package/package.json +24 -13
  85. package/assets/skills/inbox/SKILL.md +0 -86
package/README.md CHANGED
@@ -18,9 +18,15 @@ bun add --global @hasna/instructions
18
18
 
19
19
  ```bash
20
20
  instructions --help
21
- instructions init
21
+
22
+ # Hosted-by-default: resolves the approved key and uses
23
+ # https://api.hasna.com/instructions/v1.
24
+ instructions status
22
25
  instructions list
23
- instructions profile resolve
26
+
27
+ # Explicit on-box SQLite mode; there is never an implicit fallback.
28
+ HASNA_INSTRUCTIONS_LOCAL=1 instructions init
29
+ HASNA_INSTRUCTIONS_LOCAL=1 instructions list
24
30
  ```
25
31
 
26
32
  ## Migrating from `@hasna/configs`
@@ -29,13 +35,30 @@ This package was formerly `@hasna/configs`. The `configs` CLI alias and
29
35
  `configs-mcp` alias remain available for existing integrations; new usage can
30
36
  use the `instructions` names.
31
37
 
38
+ Historical on-box rows can be migrated without overwriting the current store:
39
+
40
+ ```bash
41
+ export HASNA_INSTRUCTIONS_LOCAL=1
42
+ instructions migrate-legacy --confirm-local --json # no-write plan
43
+ instructions migrate-legacy --confirm-local --apply --json # backup + transaction
44
+ ```
45
+
46
+ The source defaults to `~/.hasna/configs/configs.db`; the destination defaults
47
+ to the current `instructions.db`. A non-empty destination is refused unless
48
+ `--merge-preserve-destination` is explicit, and that merge never overwrites an
49
+ existing row.
50
+
32
51
  ## CLI Usage
33
52
 
34
53
  ```bash
35
54
  instructions --help
36
- instructions list # compact, paged summary
37
- instructions list --verbose # expanded metadata, still paged
38
- instructions list --json # full machine-readable records
55
+ instructions list # compact, paged summary
56
+ instructions list --verbose # expanded metadata, still paged
57
+ instructions list --json # legacy complete full-record array
58
+ instructions list --json --detail compact --limit 20 # bounded identity envelope
59
+ instructions list --json --detail compact --fields id,slug,name,version
60
+ instructions list --json --detail full --limit 20 # explicit bounded content read
61
+ instructions list --json --detail compact --all # explicit complete metadata read
39
62
  instructions show <slug> # full metadata + content
40
63
  instructions inspect <slug> # alias for show
41
64
  instructions profile resolve
@@ -49,7 +72,9 @@ small. Human output is capped at 20 rows unless you pass `--limit`; use
49
72
  `--cursor` to continue from the next page. Detail is explicit:
50
73
 
51
74
  - `--verbose` expands list rows with descriptions, tags, and paths.
52
- - `--json` preserves full machine-readable records for automation.
75
+ - Legacy `--json` with no modern detail flags preserves the complete full-record array for automation. Explicit `--limit`/`--cursor` now bound that legacy array.
76
+ - `--detail compact` returns a content-free `{ configs, _meta }` identity envelope through the already-deployed `/v1/configs?view=identity` projection. Add `--fields` (including immutable `id`), `--all`, or `--pretty` as needed.
77
+ - `--detail full` is the explicit content-bearing envelope; it is bounded unless `--all` is supplied.
53
78
  - `show`/`inspect` and `snapshot show` print full config or snapshot content.
54
79
 
55
80
  `instructions report --json` emits the stable `schema_version: 1` report
@@ -109,8 +134,9 @@ instructions-mcp --http # http://127.0.0.1:8807/mcp
109
134
  MCP_HTTP=1 instructions-mcp
110
135
  ```
111
136
 
112
- Health: `GET http://127.0.0.1:8807/health`. MCP is also mounted on
113
- `instructions-serve` at `/mcp`.
137
+ Health: `GET http://127.0.0.1:8807/health`. The production
138
+ `instructions-serve` process deliberately does **not** mount MCP; local MCP and
139
+ the authenticated `/v1` service remain separate authorities.
114
140
 
115
141
  ## HTTP API server (`instructions-serve`)
116
142
 
@@ -120,14 +146,20 @@ instructions-serve
120
146
 
121
147
  Surfaces:
122
148
 
123
- - `GET /health`, `GET /ready`, `GET /version` → `{ status, version, mode }`
149
+ - `GET /health`, `GET /ready`, `GET /version` → `{ status, version, backend }`
124
150
  - `GET /openapi.json`, `GET /v1/openapi.json` → the OpenAPI 3.1 document the SDK
125
151
  is generated from.
126
152
  - `/v1/*` — versioned cloud API (configs, profiles, snapshots, stats).
127
153
  - No `/api/*` — the former local REST surface is not mounted (the removed
128
154
  bundled dashboard was its only consumer).
129
155
 
130
- ### Server data backend (postgresql)
156
+ ### Server data backend (PostgreSQL, fail closed)
157
+
158
+ `instructions-serve` requires `HASNA_INSTRUCTIONS_DATABASE_URL` (or its documented
159
+ alias) for a usable `/v1` service. Without a DSN, `/health` remains a liveness
160
+ probe but reports `backend: "unconfigured"`; `/ready` returns 503 and `/v1`
161
+ returns a stable unavailable error. It never exposes local SQLite over the
162
+ service boundary.
131
163
 
132
164
  When `HASNA_INSTRUCTIONS_DATABASE_URL` is set the `/v1` API reads/writes the
133
165
  shared Postgres **directly** (no local sync/cache in the service) and every
@@ -150,6 +182,16 @@ they resolve `HASNA_INSTRUCTIONS_API_KEY` (or the Keychain / credentials-file
150
182
  tiers) through the one `@hasna/contracts` client resolver, and the authority
151
183
  defaults to the fleet gateway `https://api.hasna.com/instructions`.
152
184
 
185
+ Collection responses are producer-bounded with `limit`/`cursor` envelopes.
186
+ Current clients follow every page; `view=identity` returns an allowlisted
187
+ metadata-only projection for configs, profiles, and machines, without loading
188
+ instruction content or private profile fields. Retryable create and binding
189
+ writes may send `Idempotency-Key`; PostgreSQL stores the authenticated
190
+ principal, operation, canonical request digest, and first committed response in
191
+ the same transaction as the domain mutation. A same-body retry replays that
192
+ response, while key reuse with different bytes returns
193
+ `409 IDEMPOTENCY_KEY_REUSED`.
194
+
153
195
  ## SDK
154
196
 
155
197
  The importable module ships INSIDE `@hasna/instructions` at the `./sdk` export
@@ -215,6 +257,95 @@ transport is decided by what resolves, never by a mode word.
215
257
  Clients never hold a database DSN. The raw Postgres connection is a server-only
216
258
  concern (`instructions-serve`), selected by `HASNA_INSTRUCTIONS_DATABASE_URL`.
217
259
 
260
+
261
+
262
+ ## Domain export and import
263
+
264
+ `instructions export` writes a restorable Instructions domain archive v2. The
265
+ archive contains config content and every retained config snapshot, profiles,
266
+ ordered profile membership and config-binding metadata, profile asset bindings,
267
+ and registered machines. Relationships are recorded by stable config/profile
268
+ slugs and machine hostnames, so import can map them safely onto destination IDs.
269
+
270
+ ```bash
271
+ instructions export --output ./instructions-domain.tar.gz
272
+ instructions import ./instructions-domain.tar.gz # exact recovery into an empty destination
273
+ ```
274
+
275
+ V2 is deliberately a recovery format, not a merge format. Import validates the
276
+ complete archive and requires a destination with zero configs, profiles, and
277
+ machines before the first mutation. `--overwrite` is rejected for v2 before
278
+ destination inspection or mutation. Any validation, mutation, or readback
279
+ failure throws and makes the CLI exit nonzero; discard that attempted
280
+ destination and retry from a newly empty database.
281
+
282
+ The manifest contains deterministic per-collection counts and SHA-256 logical
283
+ hashes for pre/post deployment comparison without placing instruction content
284
+ in logs. Exact deployment integrity includes config `created_at`, `updated_at`,
285
+ and `synced_at`; snapshot `created_at`; profile `created_at` and `updated_at`;
286
+ and machine `created_at` plus the exact `last_applied_at` value. After recovery,
287
+ import reads the complete domain back through `ConfigStore` and verifies every
288
+ field that interface can reproduce: config data and versions, retained snapshot
289
+ contents, profiles, ordered bindings, asset mappings, machines, and whether a
290
+ machine was ever applied.
291
+
292
+ `ConfigStore` cannot assign archived config/profile/snapshot/machine creation or
293
+ update timestamps, nor an exact machine `last_applied_at`; generated destination
294
+ IDs are also intentionally remapped by stable slugs and hostnames. These values
295
+ remain protected in the archive's exact integrity hashes but are not recreated
296
+ by recovery. ConfigStore also has no cross-entity transaction, so an operational
297
+ failure may leave a partial destination; the nonzero result is terminal and that
298
+ destination must not be reused. Legacy v1 config-only archives remain importable,
299
+ including their historical skip/overwrite conflict behavior.
300
+
301
+ API keys and idempotency receipts are intentionally excluded: they are security
302
+ and transport state that must be provisioned independently. Feedback is also
303
+ excluded because it is product telemetry, not part of the Instructions
304
+ configuration domain.
305
+
306
+ ## Native S3 backup storage
307
+
308
+ S3 is an **adjunct immutable backup plane**, never a database selector. SQLite
309
+ remains the explicit local authority and PostgreSQL remains the hosted `/v1`
310
+ authority. Setting S3 variables alone never opens SQLite, never selects HTTP,
311
+ and never changes CRUD routing.
312
+
313
+ ```bash
314
+ export HASNA_INSTRUCTIONS_S3_BUCKET=your-private-bucket
315
+ export HASNA_INSTRUCTIONS_AWS_REGION=us-east-1
316
+ # Optional: HASNA_INSTRUCTIONS_S3_PREFIX (default: instructions/)
317
+ # Optional local/S3-compatible endpoint and explicit static credentials.
318
+
319
+ instructions storage status --json
320
+ instructions export --output ./instructions-backup.tar.gz
321
+ instructions storage backup push ./instructions-backup.tar.gz --id 2026-09-15-pre-deploy --dry-run --json
322
+ instructions storage backup push ./instructions-backup.tar.gz --id 2026-09-15-pre-deploy --json
323
+ instructions storage backup verify 2026-09-15-pre-deploy --json
324
+ instructions storage backup pull 2026-09-15-pre-deploy --output ./restored-instructions.tar.gz --json
325
+ ```
326
+
327
+ Each backup uses traversal-safe deterministic keys, an atomically created
328
+ payload, and an atomically created manifest containing SHA-256, byte size,
329
+ content type, and creation time. Native S3 creation uses `If-None-Match: *`;
330
+ concurrent different-byte writers cannot replace the winner, identical replay
331
+ is idempotent, and an injected store without conditional-create support fails
332
+ closed. Pulls verify the payload before an owner-only local file is written.
333
+ Production backup buckets must enable versioning, encryption, public-access
334
+ blocking, and S3 Object Lock with a default retention period. AWS runtime
335
+ credentials may come from Bun's standard AWS chain (including an ECS task role);
336
+ explicit static credentials are optional and must be a complete pair. Status
337
+ never prints credential values or the bucket name.
338
+
339
+ The production deployment runs the immutable predeploy backup push and verify
340
+ as direct Bun commands in the exact candidate image. The GitHub runner alone
341
+ parses the push receipt, then supplies the recorded payload and manifest version
342
+ IDs to verification; it also checks the receipt digest and size, COMPLIANCE
343
+ Object Lock retention for both versions, and an exact versioned-payload
344
+ readback. Every backup gate completes before any ECR push, database migration,
345
+ or ECS update.
346
+
347
+ The public importable surface is available at `@hasna/instructions/storage`.
348
+
218
349
  ## Data Directory
219
350
 
220
351
  Local data (the SQLite store and backups) lives under the configs store home,
@@ -3,20 +3,22 @@ import {
3
3
  applyConfig,
4
4
  applyConfigs,
5
5
  applyConfigsWithReport,
6
+ compactPathForConfigHome,
6
7
  expandPath,
7
8
  getConfigHome,
8
9
  normalizeTargetPath,
9
10
  previewConfigs
10
- } from "./index-axhwg61p.js";
11
- import"./index-v7dkb384.js";
11
+ } from "./index-ypcef3a2.js";
12
+ import"./index-aj66f6xr.js";
12
13
  import"./index-35z65jsa.js";
13
- import"./index-pkscjnj5.js";
14
+ import"./index-3djthcm6.js";
14
15
  import"./index-nnwp92tk.js";
15
16
  export {
16
17
  previewConfigs,
17
18
  normalizeTargetPath,
18
19
  getConfigHome,
19
20
  expandPath,
21
+ compactPathForConfigHome,
20
22
  applyConfigsWithReport,
21
23
  applyConfigs,
22
24
  applyConfig
@@ -3,19 +3,22 @@ import {
3
3
  applyConfig,
4
4
  applyConfigs,
5
5
  applyConfigsWithReport,
6
+ compactPathForConfigHome,
6
7
  expandPath,
7
8
  getConfigHome,
8
9
  normalizeTargetPath,
9
10
  previewConfigs
10
- } from "./index-e9epbsvs.js";
11
- import"./index-1hpwskwd.js";
12
- import"./index-pkscjnj5.js";
11
+ } from "./index-r7nfef2g.js";
12
+ import"./index-6jncnnw7.js";
13
+ import"./index-n3fjspr0.js";
14
+ import"./index-fpeyzn3n.js";
13
15
  import"./index-bsan4c16.js";
14
16
  export {
15
17
  previewConfigs,
16
18
  normalizeTargetPath,
17
19
  getConfigHome,
18
20
  expandPath,
21
+ compactPathForConfigHome,
19
22
  applyConfigsWithReport,
20
23
  applyConfigs,
21
24
  applyConfig
@@ -0,0 +1,23 @@
1
+ // @bun
2
+ import {
3
+ getDatabase,
4
+ insertFeedback,
5
+ resetDatabase,
6
+ resetLocalDatabase
7
+ } from "./index-5d7zdftc.js";
8
+ import {
9
+ now,
10
+ slugify,
11
+ uuid
12
+ } from "./index-ma2xmp6c.js";
13
+ import"./index-fpeyzn3n.js";
14
+ import"./index-bsan4c16.js";
15
+ export {
16
+ uuid,
17
+ slugify,
18
+ resetLocalDatabase,
19
+ resetDatabase,
20
+ now,
21
+ insertFeedback,
22
+ getDatabase
23
+ };
@@ -28,7 +28,6 @@ var ASSET_DESTINATION_STRATEGIES = ["emit-file", "install-local", "install-marke
28
28
  var ASSET_SCOPES = ["global", "project", "session"];
29
29
  var ASSET_UNINSTALL_POLICIES = ["remove-managed", "retain"];
30
30
  var ASSET_ROLLBACK_POLICIES = ["snapshot", "installer-receipt", "none"];
31
-
32
31
  class ConfigNotFoundError extends Error {
33
32
  constructor(id) {
34
33
  super(`Config not found: ${id}`);
@@ -0,0 +1,186 @@
1
+ // @bun
2
+ import {
3
+ getRawStoreRoot,
4
+ hasInstructionsEnvAuthorityIntent
5
+ } from "./index-fpeyzn3n.js";
6
+
7
+ // src/db/database.ts
8
+ import { Database } from "bun:sqlite";
9
+ import { existsSync, mkdirSync, rmSync } from "fs";
10
+ import { join } from "path";
11
+ function getDbPath() {
12
+ if (process.env["HASNA_INSTRUCTIONS_DB_PATH"]) {
13
+ return process.env["HASNA_INSTRUCTIONS_DB_PATH"];
14
+ }
15
+ const dir = getRawStoreRoot();
16
+ mkdirSync(dir, { recursive: true });
17
+ return join(dir, "instructions.db");
18
+ }
19
+ var MIGRATIONS = [
20
+ `
21
+ CREATE TABLE IF NOT EXISTS configs (
22
+ id TEXT PRIMARY KEY,
23
+ name TEXT NOT NULL,
24
+ slug TEXT NOT NULL UNIQUE,
25
+ kind TEXT NOT NULL DEFAULT 'file',
26
+ category TEXT NOT NULL,
27
+ agent TEXT NOT NULL DEFAULT 'global',
28
+ target_path TEXT,
29
+ format TEXT NOT NULL DEFAULT 'text',
30
+ content TEXT NOT NULL DEFAULT '',
31
+ description TEXT,
32
+ tags TEXT NOT NULL DEFAULT '[]',
33
+ is_template INTEGER NOT NULL DEFAULT 0,
34
+ version INTEGER NOT NULL DEFAULT 1,
35
+ created_at TEXT NOT NULL,
36
+ updated_at TEXT NOT NULL,
37
+ synced_at TEXT
38
+ );
39
+
40
+ CREATE TABLE IF NOT EXISTS config_snapshots (
41
+ id TEXT PRIMARY KEY,
42
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
43
+ content TEXT NOT NULL,
44
+ version INTEGER NOT NULL,
45
+ created_at TEXT NOT NULL
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS profiles (
49
+ id TEXT PRIMARY KEY,
50
+ name TEXT NOT NULL,
51
+ slug TEXT NOT NULL UNIQUE,
52
+ description TEXT,
53
+ created_at TEXT NOT NULL,
54
+ updated_at TEXT NOT NULL
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS profile_configs (
58
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
59
+ config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
60
+ sort_order INTEGER NOT NULL DEFAULT 0,
61
+ PRIMARY KEY (profile_id, config_id)
62
+ );
63
+
64
+ CREATE TABLE IF NOT EXISTS machines (
65
+ id TEXT PRIMARY KEY,
66
+ hostname TEXT NOT NULL UNIQUE,
67
+ os TEXT,
68
+ last_applied_at TEXT,
69
+ created_at TEXT NOT NULL
70
+ );
71
+
72
+ CREATE TABLE IF NOT EXISTS schema_version (
73
+ version INTEGER PRIMARY KEY
74
+ );
75
+
76
+ INSERT OR IGNORE INTO schema_version (version) VALUES (1);
77
+ `,
78
+ `
79
+ ALTER TABLE profiles ADD COLUMN selectors TEXT NOT NULL DEFAULT '{}';
80
+ ALTER TABLE profiles ADD COLUMN variables TEXT NOT NULL DEFAULT '{}';
81
+ ALTER TABLE machines ADD COLUMN arch TEXT;
82
+ `,
83
+ `
84
+ ALTER TABLE configs ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]';
85
+ `,
86
+ `
87
+ 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"}';
88
+ `,
89
+ `
90
+ CREATE TABLE IF NOT EXISTS profile_assets (
91
+ profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
92
+ source_config_id TEXT NOT NULL REFERENCES configs(id) ON DELETE CASCADE,
93
+ asset_key TEXT NOT NULL,
94
+ sort_order INTEGER NOT NULL DEFAULT 0,
95
+ binding TEXT NOT NULL,
96
+ PRIMARY KEY (profile_id, asset_key)
97
+ );
98
+ CREATE INDEX IF NOT EXISTS profile_assets_source_config_idx ON profile_assets (source_config_id);
99
+ `
100
+ ];
101
+ var _db = null;
102
+ function getDatabase(path) {
103
+ if (_db)
104
+ return _db;
105
+ if (!path && hasInstructionsEnvAuthorityIntent(process.env)) {
106
+ 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.");
107
+ }
108
+ const dbPath = path || getDbPath();
109
+ const db = new Database(dbPath);
110
+ db.run("PRAGMA journal_mode = WAL");
111
+ db.run("PRAGMA foreign_keys = ON");
112
+ applyMigrations(db);
113
+ ensureFeedbackTable(db);
114
+ _db = db;
115
+ return db;
116
+ }
117
+ function resetDatabase() {
118
+ if (_db) {
119
+ try {
120
+ _db.close();
121
+ } catch {}
122
+ }
123
+ _db = null;
124
+ }
125
+ function resetLocalDatabase() {
126
+ resetDatabase();
127
+ const dbPath = getDbPath();
128
+ if (dbPath === ":memory:")
129
+ return;
130
+ for (const p of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
131
+ if (existsSync(p))
132
+ rmSync(p);
133
+ }
134
+ }
135
+ function applyMigrations(db) {
136
+ let currentVersion = 0;
137
+ try {
138
+ const row = db.query("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1").get();
139
+ currentVersion = row?.version ?? 0;
140
+ } catch {
141
+ currentVersion = 0;
142
+ }
143
+ const applyOne = db.transaction((index) => {
144
+ if (!migrationEffectAlreadyPresent(db, index))
145
+ db.exec(MIGRATIONS[index]);
146
+ db.run(`INSERT OR REPLACE INTO schema_version (version) VALUES (${index + 1})`);
147
+ });
148
+ for (let i = currentVersion;i < MIGRATIONS.length; i++)
149
+ applyOne(i);
150
+ }
151
+ function migrationEffectAlreadyPresent(db, index) {
152
+ if (index !== 3)
153
+ return false;
154
+ return db.query("PRAGMA table_info(profile_configs)").all().some((column) => column.name === "binding");
155
+ }
156
+ function ensureFeedbackTable(db) {
157
+ db.exec(`
158
+ CREATE TABLE IF NOT EXISTS feedback (
159
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
160
+ message TEXT NOT NULL,
161
+ email TEXT,
162
+ category TEXT DEFAULT 'general',
163
+ version TEXT,
164
+ machine_id TEXT,
165
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
166
+ )
167
+ `);
168
+ const existing = new Set(db.query("PRAGMA table_info(feedback)").all().map((r) => r.name));
169
+ const required = [
170
+ ["email", "TEXT"],
171
+ ["category", "TEXT DEFAULT 'general'"],
172
+ ["version", "TEXT"],
173
+ ["machine_id", "TEXT"],
174
+ ["created_at", "TEXT"]
175
+ ];
176
+ for (const [name, def] of required) {
177
+ if (!existing.has(name))
178
+ db.exec(`ALTER TABLE feedback ADD COLUMN ${name} ${def}`);
179
+ }
180
+ }
181
+ function insertFeedback(input, db) {
182
+ const d = db || getDatabase();
183
+ d.run("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)", [input.message, input.email ?? null, input.category ?? "general", input.version ?? null]);
184
+ }
185
+
186
+ export { getDatabase, resetDatabase, resetLocalDatabase, insertFeedback };