@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.
- package/README.md +141 -10
- package/dist/chunks/{apply-ynfn1av7.js → apply-c3sd4agz.js} +5 -3
- package/dist/chunks/{apply-sfzja1f3.js → apply-pnn7a3y7.js} +6 -3
- package/dist/chunks/database-wstmr56z.js +23 -0
- package/dist/chunks/{index-pkscjnj5.js → index-3djthcm6.js} +0 -1
- package/dist/chunks/index-5d7zdftc.js +186 -0
- package/dist/chunks/{index-1hpwskwd.js → index-6jncnnw7.js} +318 -329
- package/dist/chunks/index-8rb7ng2y.js +148 -0
- package/dist/chunks/{index-v7dkb384.js → index-aj66f6xr.js} +243 -100
- package/dist/chunks/index-fpeyzn3n.js +186 -0
- package/dist/chunks/{index-9p9gn7gh.js → index-jg8ptcwh.js} +5 -7
- package/dist/chunks/index-mvckkf15.js +363 -0
- package/dist/chunks/index-n3fjspr0.js +119 -0
- package/dist/chunks/{index-85n5wnd8.js → index-pdpg8a44.js} +5 -7
- package/dist/chunks/{index-e9epbsvs.js → index-r7nfef2g.js} +15 -4
- package/dist/chunks/{index-axhwg61p.js → index-ypcef3a2.js} +15 -4
- package/dist/chunks/legacy-store-migration-z2m5yzyk.js +322 -0
- package/dist/chunks/{local-a9nen65z.js → local-0a9gja58.js} +142 -2
- package/dist/chunks/{local-0d8qrc92.js → local-969ye538.js} +153 -188
- package/dist/chunks/s3-backup-hxe3zpfz.js +240 -0
- package/dist/chunks/s3-config-hhght91k.js +14 -0
- package/dist/chunks/s3-object-store-n0s82swt.js +14 -0
- package/dist/chunks/{sync-vy1yjvs6.js → sync-q5hvwq6e.js} +4 -4
- package/dist/chunks/{sync-m46fc257.js → sync-xtkbm5br.js} +5 -4
- package/dist/chunks/{template-02wjvv05.js → template-fsfxpe4p.js} +1 -1
- package/dist/chunks/{template-ke972qqe.js → template-wgnzdn7h.js} +1 -1
- package/dist/cli/index.js +1236 -256
- package/dist/data/config-store.d.ts +10 -1
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/configs.d.ts +4 -1
- package/dist/db/configs.d.ts.map +1 -1
- package/dist/db/local.d.ts +1 -1
- package/dist/db/local.d.ts.map +1 -1
- package/dist/index.js +1086 -215
- package/dist/lib/apply.d.ts +7 -0
- package/dist/lib/apply.d.ts.map +1 -1
- package/dist/lib/compact-output.d.ts +3 -18
- package/dist/lib/compact-output.d.ts.map +1 -1
- package/dist/lib/export.d.ts +22 -3
- package/dist/lib/export.d.ts.map +1 -1
- package/dist/lib/import.d.ts +18 -0
- package/dist/lib/import.d.ts.map +1 -1
- package/dist/lib/legacy-store-migration.d.ts +39 -0
- package/dist/lib/legacy-store-migration.d.ts.map +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts +1 -0
- package/dist/lib/managed-skill-runtimes.d.ts.map +1 -1
- package/dist/lib/sync-dir.d.ts.map +1 -1
- package/dist/mcp/index.js +41 -30
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/sdk/index.d.ts +2 -0
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +229 -10
- package/dist/sdk/resolve.d.ts +1 -1
- package/dist/sdk/resolve.d.ts.map +1 -1
- package/dist/sdk/v1-client.d.ts +58 -0
- package/dist/sdk/v1-client.d.ts.map +1 -0
- package/dist/sdk/v1.generated.d.ts +242 -21
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.d.ts +13 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1261 -427
- package/dist/server/openapi.d.ts +1379 -93
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +25 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +45 -2
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/storage/index.d.ts +7 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +744 -0
- package/dist/storage/s3-backup.d.ts +76 -0
- package/dist/storage/s3-backup.d.ts.map +1 -0
- package/dist/storage/s3-config.d.ts +46 -0
- package/dist/storage/s3-config.d.ts.map +1 -0
- package/dist/storage/s3-object-store.d.ts +70 -0
- package/dist/storage/s3-object-store.d.ts.map +1 -0
- package/dist/types/index.d.ts +98 -3
- package/dist/types/index.d.ts.map +1 -1
- package/hasna.contract.json +110 -0
- package/migrations/0001_instructions.sql +63 -0
- package/migrations/0002_api_keys.sql +20 -0
- package/migrations/0003_idempotency_receipts.sql +24 -0
- package/package.json +24 -13
- 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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
21
|
+
} from "./index-6jncnnw7.js";
|
|
22
22
|
import {
|
|
23
23
|
ConfigApplyError,
|
|
24
24
|
parseTemplateVars
|
|
25
|
-
} from "./index-
|
|
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-
|
|
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-
|
|
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
|
+
};
|