@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
@@ -0,0 +1,119 @@
1
+ // @bun
2
+ // src/types/index.ts
3
+ var CONFIG_AGENTS = [
4
+ "claude",
5
+ "codex",
6
+ "opencode",
7
+ "cursor",
8
+ "codewith",
9
+ "aicopilot",
10
+ "antigravity",
11
+ "qwen",
12
+ "grok",
13
+ "copilot",
14
+ "devin",
15
+ "windsurf-legacy",
16
+ "cline",
17
+ "zsh",
18
+ "git",
19
+ "npm",
20
+ "global"
21
+ ];
22
+ var PROFILE_CONFIG_BINDING_SCHEMA = "hasna.instructions.profile-config-binding/v1";
23
+ var INSTRUCTION_ACTIVATION_MODES = ["always", "glob", "model", "manual"];
24
+ var INSTRUCTION_FALLBACKS = ["fail", "flatten", "promote-always", "omit"];
25
+ var PROFILE_ASSET_BINDING_SCHEMA = "hasna.instructions.profile-asset-binding/v1";
26
+ var ASSET_KINDS = ["skill", "workflow", "plugin", "extension", "hook", "custom-agent"];
27
+ var ASSET_DESTINATION_STRATEGIES = ["emit-file", "install-local", "install-marketplace", "unsupported"];
28
+ var ASSET_SCOPES = ["global", "project", "session"];
29
+ var ASSET_UNINSTALL_POLICIES = ["remove-managed", "retain"];
30
+ var ASSET_ROLLBACK_POLICIES = ["snapshot", "installer-receipt", "none"];
31
+ var INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA = "hasna.instructions.domain-archive/v2";
32
+
33
+ class ConfigNotFoundError extends Error {
34
+ constructor(id) {
35
+ super(`Config not found: ${id}`);
36
+ this.name = "ConfigNotFoundError";
37
+ }
38
+ }
39
+
40
+ class ProfileNotFoundError extends Error {
41
+ constructor(id) {
42
+ super(`Profile not found: ${id}`);
43
+ this.name = "ProfileNotFoundError";
44
+ }
45
+ }
46
+
47
+ class ConfigApplyError extends Error {
48
+ constructor(message) {
49
+ super(message);
50
+ this.name = "ConfigApplyError";
51
+ }
52
+ }
53
+
54
+ class TemplateRenderError extends Error {
55
+ constructor(message) {
56
+ super(message);
57
+ this.name = "TemplateRenderError";
58
+ }
59
+ }
60
+
61
+ // src/lib/template.ts
62
+ var VAR_PATTERN = /\{\{([A-Z0-9_]+)(?::([^}]*))?\}\}/g;
63
+ function parseTemplateVars(content) {
64
+ const names = new Set;
65
+ let match;
66
+ VAR_PATTERN.lastIndex = 0;
67
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
68
+ names.add(match[1]);
69
+ }
70
+ return Array.from(names);
71
+ }
72
+ function extractTemplateVars(content) {
73
+ const vars = new Map;
74
+ let match;
75
+ VAR_PATTERN.lastIndex = 0;
76
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
77
+ const name = match[1];
78
+ const description = match[2] ?? null;
79
+ if (!vars.has(name)) {
80
+ vars.set(name, { name, description, required: true });
81
+ }
82
+ }
83
+ return Array.from(vars.values());
84
+ }
85
+ function renderTemplate(content, vars) {
86
+ const missing = [];
87
+ VAR_PATTERN.lastIndex = 0;
88
+ let match;
89
+ while ((match = VAR_PATTERN.exec(content)) !== null) {
90
+ const name = match[1];
91
+ if (!(name in vars))
92
+ missing.push(name);
93
+ }
94
+ if (missing.length > 0) {
95
+ throw new TemplateRenderError(`Missing required template variables: ${missing.join(", ")}`);
96
+ }
97
+ VAR_PATTERN.lastIndex = 0;
98
+ return content.replace(VAR_PATTERN, (_match, name) => vars[name] ?? "");
99
+ }
100
+ function renderTemplatePreview(content, vars) {
101
+ const unresolved = new Set;
102
+ VAR_PATTERN.lastIndex = 0;
103
+ const rendered = content.replace(VAR_PATTERN, (match, name) => {
104
+ if (name in vars)
105
+ return vars[name] ?? "";
106
+ unresolved.add(name);
107
+ return match;
108
+ });
109
+ return {
110
+ content: rendered,
111
+ unresolved: [...unresolved].sort()
112
+ };
113
+ }
114
+ function isTemplate(content) {
115
+ VAR_PATTERN.lastIndex = 0;
116
+ return VAR_PATTERN.test(content);
117
+ }
118
+
119
+ export { CONFIG_AGENTS, PROFILE_CONFIG_BINDING_SCHEMA, INSTRUCTION_ACTIVATION_MODES, INSTRUCTION_FALLBACKS, PROFILE_ASSET_BINDING_SCHEMA, ASSET_KINDS, ASSET_DESTINATION_STRATEGIES, ASSET_SCOPES, ASSET_UNINSTALL_POLICIES, ASSET_ROLLBACK_POLICIES, INSTRUCTIONS_DOMAIN_ARCHIVE_SCHEMA, ConfigNotFoundError, ProfileNotFoundError, ConfigApplyError, parseTemplateVars, extractTemplateVars, renderTemplate, renderTemplatePreview, isTemplate };
@@ -1,10 +1,11 @@
1
1
  // @bun
2
2
  import {
3
3
  applyConfigsWithReport,
4
+ compactPathForConfigHome,
4
5
  expandPath,
5
6
  getConfigHome,
6
7
  normalizeTargetPath
7
- } from "./index-e9epbsvs.js";
8
+ } from "./index-r7nfef2g.js";
8
9
  import {
9
10
  applyTransform,
10
11
  detectMachineContext,
@@ -15,7 +16,7 @@ import {
15
16
  resolveConfigStore,
16
17
  retiredOrUnsupportedAgentReason,
17
18
  templateizeMachineContent
18
- } from "./index-1hpwskwd.js";
19
+ } from "./index-6jncnnw7.js";
19
20
 
20
21
  // src/lib/sync.ts
21
22
  import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
@@ -24,7 +25,6 @@ import { basename, extname, join as join2 } from "path";
24
25
  // src/lib/sync-dir.ts
25
26
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
26
27
  import { join, relative } from "path";
27
- import { homedir } from "os";
28
28
  var SKIP = [".db", ".db-shm", ".db-wal", ".log", ".lock", ".DS_Store", "node_modules", ".git"];
29
29
  function shouldSkip(p) {
30
30
  return SKIP.some((s) => p.includes(s));
@@ -36,7 +36,6 @@ async function syncFromDir(dir, opts = {}) {
36
36
  return { added: 0, updated: 0, unchanged: 0, skipped: [`Not found: ${absDir}`] };
37
37
  const files = opts.recursive !== false ? walkDir(absDir) : readdirSync(absDir).map((f) => join(absDir, f)).filter((f) => statSync(f).isFile());
38
38
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
39
- const home = homedir();
40
39
  const allConfigs = await store.listConfigs();
41
40
  for (const file of files) {
42
41
  if (shouldSkip(file)) {
@@ -49,7 +48,7 @@ async function syncFromDir(dir, opts = {}) {
49
48
  result.skipped.push(file + " (too large)");
50
49
  continue;
51
50
  }
52
- const targetPath = file.replace(home, "~");
51
+ const targetPath = compactPathForConfigHome(file);
53
52
  const redacted = redactContent(content, redactFormatForTarget(targetPath, detectFormat(file)));
54
53
  const existing = allConfigs.find((c) => c.target_path === targetPath);
55
54
  if (!existing) {
@@ -71,9 +70,8 @@ async function syncFromDir(dir, opts = {}) {
71
70
  }
72
71
  async function syncToDir(dir, opts = {}) {
73
72
  const store = opts.store ?? resolveConfigStore();
74
- const home = homedir();
75
73
  const absDir = expandPath(dir);
76
- const normalized = dir.startsWith("~/") ? dir : absDir.replace(home, "~");
74
+ const normalized = dir.startsWith("~/") ? dir : compactPathForConfigHome(absDir);
77
75
  const configs = (await store.listConfigs()).filter((c) => c.target_path && (c.target_path.startsWith(normalized) || c.target_path.startsWith(absDir)));
78
76
  const result = { added: 0, updated: 0, unchanged: 0, skipped: [] };
79
77
  for (const config of configs) {
@@ -18,15 +18,15 @@ import {
18
18
  resolveProfileVariables,
19
19
  scanSecrets,
20
20
  stampCursorGlobalAuthorityMarker
21
- } from "./index-1hpwskwd.js";
21
+ } from "./index-6jncnnw7.js";
22
22
  import {
23
23
  ConfigApplyError,
24
24
  parseTemplateVars
25
- } from "./index-pkscjnj5.js";
25
+ } from "./index-n3fjspr0.js";
26
26
 
27
27
  // src/lib/apply.ts
28
28
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "fs";
29
- import { basename, dirname as dirname2, join as join2, resolve } from "path";
29
+ import { basename, dirname as dirname2, isAbsolute, join as join2, relative as relative2, resolve, sep as sep2 } from "path";
30
30
  import { homedir } from "os";
31
31
 
32
32
  // src/lib/session-render-ownership.ts
@@ -110,6 +110,17 @@ function expandPath(p) {
110
110
  }
111
111
  return resolve(p);
112
112
  }
113
+ function compactPathForConfigHome(p) {
114
+ const absolute = resolve(p);
115
+ const configHome = resolve(getConfigHome());
116
+ const conventionalHome = resolve(process.env["HOME"] || homedir());
117
+ if (configHome !== conventionalHome)
118
+ return absolute;
119
+ const rel = relative2(configHome, absolute);
120
+ if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel))
121
+ return absolute;
122
+ return `~/${rel}`;
123
+ }
113
124
  function normalizeTargetPath(p) {
114
125
  const expanded = expandPath(p);
115
126
  try {
@@ -518,4 +529,4 @@ function sessionRendererOwnsCanonicalTarget(normalized, opts) {
518
529
  return sessionRenderOwnsPath(normalized);
519
530
  }
520
531
 
521
- export { getConfigHome, expandPath, normalizeTargetPath, applyConfig, applyConfigs, applyConfigsWithReport, previewConfigs };
532
+ export { getConfigHome, expandPath, compactPathForConfigHome, normalizeTargetPath, applyConfig, applyConfigs, applyConfigsWithReport, previewConfigs };
@@ -15,7 +15,7 @@ import {
15
15
  resolveConfigStore,
16
16
  resolveProfileVariables,
17
17
  stampCursorGlobalAuthorityMarker
18
- } from "./index-v7dkb384.js";
18
+ } from "./index-aj66f6xr.js";
19
19
  import {
20
20
  isSecretVarName,
21
21
  redactFormatForTarget,
@@ -24,11 +24,11 @@ import {
24
24
  import {
25
25
  ConfigApplyError,
26
26
  parseTemplateVars
27
- } from "./index-pkscjnj5.js";
27
+ } from "./index-3djthcm6.js";
28
28
 
29
29
  // src/lib/apply.ts
30
30
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync } from "fs";
31
- import { basename, dirname as dirname2, join as join2, resolve } from "path";
31
+ import { basename, dirname as dirname2, isAbsolute, join as join2, relative as relative2, resolve, sep as sep2 } from "path";
32
32
  import { homedir } from "os";
33
33
 
34
34
  // src/lib/session-render-ownership.ts
@@ -112,6 +112,17 @@ function expandPath(p) {
112
112
  }
113
113
  return resolve(p);
114
114
  }
115
+ function compactPathForConfigHome(p) {
116
+ const absolute = resolve(p);
117
+ const configHome = resolve(getConfigHome());
118
+ const conventionalHome = resolve(process.env["HOME"] || homedir());
119
+ if (configHome !== conventionalHome)
120
+ return absolute;
121
+ const rel = relative2(configHome, absolute);
122
+ if (!rel || rel === ".." || rel.startsWith(`..${sep2}`) || isAbsolute(rel))
123
+ return absolute;
124
+ return `~/${rel}`;
125
+ }
115
126
  function normalizeTargetPath(p) {
116
127
  const expanded = expandPath(p);
117
128
  try {
@@ -520,4 +531,4 @@ function sessionRendererOwnsCanonicalTarget(normalized, opts) {
520
531
  return sessionRenderOwnsPath(normalized);
521
532
  }
522
533
 
523
- export { getConfigHome, expandPath, normalizeTargetPath, applyConfig, applyConfigs, applyConfigsWithReport, previewConfigs };
534
+ export { getConfigHome, expandPath, compactPathForConfigHome, normalizeTargetPath, applyConfig, applyConfigs, applyConfigsWithReport, previewConfigs };
@@ -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
+ };