@hasna/skills 0.1.71 → 0.2.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.
- package/README.md +4 -2
- package/bin/index.js +24657 -24681
- package/bin/mcp.js +372 -243
- package/bin/migrate.js +202 -41
- package/bin/server.js +3502 -2898
- package/bin/worker.js +2187 -1806
- package/dist/cli/commands/install.d.ts +16 -0
- package/dist/cli/commands/publish.d.ts +35 -0
- package/dist/cli/commands/registry.d.ts +5 -0
- package/dist/index.js +705 -363
- package/dist/lib/app-home.d.ts +111 -0
- package/dist/lib/config.d.ts +9 -10
- package/dist/lib/feedback.d.ts +6 -0
- package/dist/lib/installer.d.ts +2 -0
- package/dist/lib/pull.d.ts +18 -1
- package/dist/lib/remote-client.d.ts +15 -1
- package/dist/lib/skill-version.d.ts +11 -0
- package/dist/lib/station-hydrate.d.ts +2 -0
- package/dist/lib/station-snapshot.d.ts +1 -1
- package/dist/sdk/index.js +5723 -5340
- package/dist/server/app.d.ts +3 -0
- package/dist/server/artifact-storage.d.ts +27 -0
- package/dist/server/config.d.ts +2 -0
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/seed-bundled.d.ts +20 -0
- package/dist/server/skills-api.d.ts +18 -1
- package/dist/server/sqlite-store.d.ts +3 -1
- package/dist/server/store.d.ts +6 -1
- package/dist/server/types.d.ts +43 -0
- package/dist/storage.js +156 -43
- package/migrations/postgres/0006_skill_versions.sql +30 -0
- package/migrations/sqlite/0006_skill_versions.sql +17 -0
- package/package.json +2 -2
package/bin/migrate.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/server/migrate.ts
|
|
5
|
-
import { existsSync as
|
|
6
|
-
import { join as
|
|
5
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync3 } from "fs";
|
|
6
|
+
import { join as join6 } from "path";
|
|
7
7
|
|
|
8
8
|
// src/lib/retired-settings.ts
|
|
9
9
|
var RETIRED_ENV_SUFFIXES = ["_STORAGE_MODE", "_DEPLOYMENT_MODE", "_CLOUD_MODE"];
|
|
@@ -56,6 +56,7 @@ function resolveServerConfig(env = process.env) {
|
|
|
56
56
|
port,
|
|
57
57
|
databaseUrl: env[DATABASE_URL_ENV] || env.DATABASE_URL || undefined,
|
|
58
58
|
bootstrapApiKey: env.HASNA_SKILLS_BOOTSTRAP_API_KEY || undefined,
|
|
59
|
+
seedBundledCorpus: (env.HASNA_SKILLS_SEED_BUNDLED_CORPUS ?? "1") !== "0",
|
|
59
60
|
artifactBucket: env.HASNA_SKILLS_S3_BUCKET || env.SKILLS_S3_BUCKET || undefined,
|
|
60
61
|
artifactPrefix: normalizePrefix(env.HASNA_SKILLS_S3_PREFIX || env.SKILLS_S3_PREFIX || "skills/artifacts"),
|
|
61
62
|
inlineWorker: env.HASNA_SKILLS_INLINE_WORKER === "1",
|
|
@@ -81,65 +82,167 @@ function normalizePrefix(value) {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
// src/server/database-url.ts
|
|
84
|
-
import { isAbsolute, join as
|
|
85
|
+
import { isAbsolute, join as join3 } from "path";
|
|
85
86
|
import { fileURLToPath } from "url";
|
|
86
87
|
|
|
87
88
|
// src/lib/config.ts
|
|
88
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
89
|
-
import { join, dirname } from "path";
|
|
89
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync, statSync } from "fs";
|
|
90
|
+
import { join as join2, dirname } from "path";
|
|
91
|
+
|
|
92
|
+
// src/lib/app-home.ts
|
|
93
|
+
import { existsSync } from "fs";
|
|
90
94
|
import { homedir } from "os";
|
|
95
|
+
import { join, resolve } from "path";
|
|
96
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
97
|
+
import { join as pathsResolverJoin } from "path";
|
|
98
|
+
var PATHS_RESOLVER_KIND_ENV = {
|
|
99
|
+
config: "HASNA_CONFIG_HOME",
|
|
100
|
+
data: "HASNA_DATA_HOME",
|
|
101
|
+
state: "HASNA_STATE_HOME",
|
|
102
|
+
cache: "HASNA_CACHE_HOME"
|
|
103
|
+
};
|
|
104
|
+
var PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
105
|
+
function pathsResolverAssertApp(app) {
|
|
106
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
107
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
108
|
+
}
|
|
109
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
110
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function pathsResolverAssertKind(kind) {
|
|
114
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
115
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function pathsResolverBaseDir(kind, options) {
|
|
119
|
+
pathsResolverAssertKind(kind);
|
|
120
|
+
const env = options.env ?? process.env;
|
|
121
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
122
|
+
if (typeof override === "string" && override.length > 0)
|
|
123
|
+
return override;
|
|
124
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
125
|
+
const platform = options.platform ?? process.platform;
|
|
126
|
+
if (platform === "darwin") {
|
|
127
|
+
switch (kind) {
|
|
128
|
+
case "config":
|
|
129
|
+
case "data":
|
|
130
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
131
|
+
case "cache":
|
|
132
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
133
|
+
case "state":
|
|
134
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
switch (kind) {
|
|
138
|
+
case "config":
|
|
139
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
140
|
+
case "data":
|
|
141
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
142
|
+
case "state":
|
|
143
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
144
|
+
case "cache":
|
|
145
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function pathsResolverResolve(kind, options) {
|
|
149
|
+
pathsResolverAssertApp(options.app);
|
|
150
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
151
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
152
|
+
}
|
|
153
|
+
function dataDir(options) {
|
|
154
|
+
return pathsResolverResolve("data", options);
|
|
155
|
+
}
|
|
156
|
+
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
157
|
+
var HASNA_SKILLS_HOME_ENV = "HASNA_SKILLS_HOME";
|
|
158
|
+
var SKILLS_HOME_ENV = "SKILLS_HOME";
|
|
159
|
+
var DEFAULT_SQLITE_FILENAME = "server.db";
|
|
160
|
+
var GLOBAL_CONFIG_FILENAME = "config.json";
|
|
161
|
+
function effectiveHome() {
|
|
162
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir() || "/tmp";
|
|
163
|
+
}
|
|
164
|
+
function legacyDataRoot() {
|
|
165
|
+
return join(effectiveHome(), ".hasna", "skills");
|
|
166
|
+
}
|
|
167
|
+
function resolverDataRoot(home = effectiveHome(), env) {
|
|
168
|
+
return dataDir({ app: "skills", home, env });
|
|
169
|
+
}
|
|
170
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
171
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
172
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
173
|
+
return true;
|
|
174
|
+
return existsSync(join(resolved, DEFAULT_SQLITE_FILENAME)) || existsSync(join(resolved, GLOBAL_CONFIG_FILENAME));
|
|
175
|
+
}
|
|
176
|
+
function exactDataRoot() {
|
|
177
|
+
for (const key of [DATA_DIR_ENV, HASNA_SKILLS_HOME_ENV, SKILLS_HOME_ENV]) {
|
|
178
|
+
const dir = process.env[key]?.trim();
|
|
179
|
+
if (dir)
|
|
180
|
+
return resolve(dir);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
function hasExactOverride(env = process.env) {
|
|
185
|
+
return Boolean(env[DATA_DIR_ENV]?.trim()) || Boolean(env[HASNA_SKILLS_HOME_ENV]?.trim()) || Boolean(env[SKILLS_HOME_ENV]?.trim());
|
|
186
|
+
}
|
|
187
|
+
function hasOperatorOverride(env = process.env) {
|
|
188
|
+
return hasExactOverride(env) || Boolean(env.HASNA_DATA_HOME?.trim());
|
|
189
|
+
}
|
|
190
|
+
function getDataRoot() {
|
|
191
|
+
const exact = exactDataRoot();
|
|
192
|
+
if (exact)
|
|
193
|
+
return exact;
|
|
194
|
+
const resolved = resolverDataRoot();
|
|
195
|
+
return adoptResolverDataRoot(resolved) ? resolve(resolved) : resolve(legacyDataRoot());
|
|
196
|
+
}
|
|
197
|
+
// src/lib/config.ts
|
|
91
198
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
92
|
-
if (!
|
|
199
|
+
if (!existsSync2(sourceDir))
|
|
93
200
|
return;
|
|
94
201
|
mkdirSync(targetDir, { recursive: true });
|
|
95
202
|
for (const entry of readdirSync(sourceDir)) {
|
|
96
|
-
const sourcePath =
|
|
97
|
-
const targetPath =
|
|
203
|
+
const sourcePath = join2(sourceDir, entry);
|
|
204
|
+
const targetPath = join2(targetDir, entry);
|
|
98
205
|
try {
|
|
99
206
|
const sourceStat = statSync(sourcePath);
|
|
100
207
|
if (sourceStat.isDirectory()) {
|
|
101
208
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
102
209
|
continue;
|
|
103
210
|
}
|
|
104
|
-
if (!
|
|
211
|
+
if (!existsSync2(targetPath))
|
|
105
212
|
copyFileSync(sourcePath, targetPath);
|
|
106
213
|
} catch {}
|
|
107
214
|
}
|
|
108
215
|
}
|
|
109
|
-
var DATA_DIR_ENV = "HASNA_SKILLS_DIR";
|
|
110
216
|
function getDataDir() {
|
|
111
|
-
const
|
|
112
|
-
if (override) {
|
|
113
|
-
try {
|
|
114
|
-
mkdirSync(override, { recursive: true });
|
|
115
|
-
} catch {}
|
|
116
|
-
return override;
|
|
117
|
-
}
|
|
118
|
-
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
119
|
-
const newDir = join(home, ".hasna", "skills");
|
|
120
|
-
const oldDir = join(home, ".skills");
|
|
121
|
-
const oldConfigFile = join(home, ".skillsrc");
|
|
122
|
-
mkdirSync(newDir, { recursive: true });
|
|
217
|
+
const root = getDataRoot();
|
|
123
218
|
try {
|
|
124
|
-
|
|
219
|
+
mkdirSync(root, { recursive: true });
|
|
125
220
|
} catch {}
|
|
126
|
-
if (
|
|
221
|
+
if (hasOperatorOverride())
|
|
222
|
+
return root;
|
|
223
|
+
const home = effectiveHome();
|
|
224
|
+
const oldDir = join2(home, ".skills");
|
|
225
|
+
const oldConfigFile = join2(home, ".skillsrc");
|
|
226
|
+
try {
|
|
227
|
+
mergeDirectoryContents(oldDir, root);
|
|
228
|
+
} catch {}
|
|
229
|
+
if (existsSync2(oldConfigFile) && !existsSync2(join2(root, "config.json"))) {
|
|
127
230
|
try {
|
|
128
|
-
copyFileSync(oldConfigFile,
|
|
231
|
+
copyFileSync(oldConfigFile, join2(root, "config.json"));
|
|
129
232
|
} catch {}
|
|
130
233
|
}
|
|
131
|
-
return
|
|
234
|
+
return root;
|
|
132
235
|
}
|
|
133
236
|
|
|
134
237
|
// src/server/database-url.ts
|
|
135
|
-
var
|
|
238
|
+
var DEFAULT_SQLITE_FILENAME2 = "server.db";
|
|
136
239
|
var SQLITE_MEMORY_PATH = ":memory:";
|
|
137
240
|
var POSTGRES_SCHEMES = new Set(["postgres", "postgresql"]);
|
|
138
241
|
var SQLITE_SCHEMES = new Set(["sqlite", "sqlite3", "file"]);
|
|
139
242
|
var MEMORY_SCHEMES = new Set(["memory"]);
|
|
140
243
|
var SQLITE_EXTENSIONS = [".db", ".sqlite", ".sqlite3", ".db3"];
|
|
141
244
|
function defaultSqlitePath() {
|
|
142
|
-
return
|
|
245
|
+
return join3(getDataDir(), DEFAULT_SQLITE_FILENAME2);
|
|
143
246
|
}
|
|
144
247
|
function resolveDatabaseTarget(raw) {
|
|
145
248
|
const value = raw?.trim();
|
|
@@ -166,7 +269,7 @@ function resolveDatabaseTarget(raw) {
|
|
|
166
269
|
throw new Error(`unsupported database scheme "${scheme}:". Supported: postgres://, postgresql://, sqlite:, file:, ` + `an absolute or relative path to a .db/.sqlite file, ":memory:", or "memory:" (non-durable, tests only). ` + `Leave the setting empty to use the default SQLite database at ${defaultSqlitePath()}.`);
|
|
167
270
|
}
|
|
168
271
|
if (looksLikeSqlitePath(value)) {
|
|
169
|
-
const path = isAbsolute(value) ? value :
|
|
272
|
+
const path = isAbsolute(value) ? value : join3(process.cwd(), value);
|
|
170
273
|
return { kind: "sqlite", path, durable: true, label: `sqlite (${path})` };
|
|
171
274
|
}
|
|
172
275
|
throw new Error(`could not resolve a database backend from "${value}". Use postgres://\u2026, sqlite:\u2026, an absolute or ` + `relative path ending in ${SQLITE_EXTENSIONS.join("/")}, ":memory:", or leave it empty for the ` + `default SQLite database at ${defaultSqlitePath()}.`);
|
|
@@ -190,7 +293,7 @@ function sqlitePathFromUrl(value, scheme) {
|
|
|
190
293
|
}
|
|
191
294
|
return rest.slice(2).replace(/^\/\/+/, "/");
|
|
192
295
|
}
|
|
193
|
-
return isAbsolute(rest) ? rest :
|
|
296
|
+
return isAbsolute(rest) ? rest : join3(process.cwd(), rest);
|
|
194
297
|
}
|
|
195
298
|
function looksLikeSqlitePath(value) {
|
|
196
299
|
if (value.includes("/"))
|
|
@@ -199,16 +302,16 @@ function looksLikeSqlitePath(value) {
|
|
|
199
302
|
}
|
|
200
303
|
|
|
201
304
|
// src/server/migrations-dir.ts
|
|
202
|
-
import { existsSync as
|
|
203
|
-
import { dirname as dirname2, join as
|
|
305
|
+
import { existsSync as existsSync3 } from "fs";
|
|
306
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
204
307
|
var MIGRATION_DIALECTS = ["postgres", "sqlite"];
|
|
205
308
|
var MAX_WALK_UP = 6;
|
|
206
309
|
function findMigrationsRoot(startDirs = defaultStartDirs()) {
|
|
207
310
|
for (const start of startDirs) {
|
|
208
311
|
let dir = start;
|
|
209
312
|
for (let level = 0;level < MAX_WALK_UP; level += 1) {
|
|
210
|
-
const candidate =
|
|
211
|
-
if (MIGRATION_DIALECTS.some((dialect) =>
|
|
313
|
+
const candidate = join4(dir, "migrations");
|
|
314
|
+
if (MIGRATION_DIALECTS.some((dialect) => existsSync3(join4(candidate, dialect))))
|
|
212
315
|
return candidate;
|
|
213
316
|
const parent = dirname2(dir);
|
|
214
317
|
if (parent === dir)
|
|
@@ -222,8 +325,8 @@ function resolveMigrationsDir(dialect, root = findMigrationsRoot()) {
|
|
|
222
325
|
if (!root) {
|
|
223
326
|
throw new Error(`could not locate the migrations directory for dialect "${dialect}". ` + `Expected a migrations/${dialect}/ folder alongside the package root.`);
|
|
224
327
|
}
|
|
225
|
-
const dir =
|
|
226
|
-
if (!
|
|
328
|
+
const dir = join4(root, dialect);
|
|
329
|
+
if (!existsSync3(dir)) {
|
|
227
330
|
throw new Error(`migrations directory not found: ${dir}`);
|
|
228
331
|
}
|
|
229
332
|
return dir;
|
|
@@ -240,7 +343,7 @@ function defaultStartDirs() {
|
|
|
240
343
|
import { Database } from "bun:sqlite";
|
|
241
344
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
242
345
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
243
|
-
import { dirname as dirname3, join as
|
|
346
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
244
347
|
|
|
245
348
|
// src/server/auth.ts
|
|
246
349
|
import { createHash } from "crypto";
|
|
@@ -532,6 +635,20 @@ function dateString(value) {
|
|
|
532
635
|
return value.toISOString();
|
|
533
636
|
return String(value);
|
|
534
637
|
}
|
|
638
|
+
function rowToSkillVersion(row) {
|
|
639
|
+
return {
|
|
640
|
+
orgId: String(row.org_id),
|
|
641
|
+
slug: String(row.slug),
|
|
642
|
+
version: String(row.version),
|
|
643
|
+
bundleSha256: String(row.bundle_sha256),
|
|
644
|
+
bundleByteSize: Number(row.bundle_byte_size),
|
|
645
|
+
storageKind: String(row.storage_kind ?? "db"),
|
|
646
|
+
...typeof row.storage_key === "string" ? { storageKey: row.storage_key } : {},
|
|
647
|
+
manifest: parseJsonObject(row.manifest_json),
|
|
648
|
+
...typeof row.published_by_user_id === "string" ? { publishedByUserId: row.published_by_user_id } : {},
|
|
649
|
+
createdAt: dateString(row.created_at)
|
|
650
|
+
};
|
|
651
|
+
}
|
|
535
652
|
|
|
536
653
|
// src/server/types.ts
|
|
537
654
|
class StaleLeaseGenerationError extends Error {
|
|
@@ -562,6 +679,21 @@ class SkillRevisionConflictError extends Error {
|
|
|
562
679
|
}
|
|
563
680
|
}
|
|
564
681
|
|
|
682
|
+
class SkillVersionExistsError extends Error {
|
|
683
|
+
slug;
|
|
684
|
+
version;
|
|
685
|
+
existingSha256;
|
|
686
|
+
attemptedSha256;
|
|
687
|
+
constructor(slug, version, existingSha256, attemptedSha256) {
|
|
688
|
+
super(`version conflict for '${slug}@${version}': already published with bundle ${existingSha256}, ` + `refusing to overwrite it with ${attemptedSha256}. Publish a new version instead.`);
|
|
689
|
+
this.slug = slug;
|
|
690
|
+
this.version = version;
|
|
691
|
+
this.existingSha256 = existingSha256;
|
|
692
|
+
this.attemptedSha256 = attemptedSha256;
|
|
693
|
+
this.name = "SkillVersionExistsError";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
565
697
|
// src/lib/revision.ts
|
|
566
698
|
import { createHash as createHash2 } from "crypto";
|
|
567
699
|
function revisionIdOf(content) {
|
|
@@ -857,6 +989,13 @@ class SqliteSkillsStore {
|
|
|
857
989
|
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, previousRevisionId);
|
|
858
990
|
}
|
|
859
991
|
const carriedSha = input.bundle?.sha256 ?? previousSha;
|
|
992
|
+
if (input.version && carriedSha) {
|
|
993
|
+
const existingVersion = this.get("SELECT bundle_sha256 FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [orgId, input.slug, input.version]);
|
|
994
|
+
const existingSha = typeof existingVersion?.bundle_sha256 === "string" ? existingVersion.bundle_sha256 : null;
|
|
995
|
+
if (existingSha && existingSha !== carriedSha) {
|
|
996
|
+
throw new SkillVersionExistsError(input.slug, input.version, existingSha, carriedSha);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
860
999
|
const carriedSize = input.bundle?.byteSize ?? (previous?.bundle_byte_size == null ? null : Number(previous.bundle_byte_size));
|
|
861
1000
|
const revisionId = revisionIdOfRecord({
|
|
862
1001
|
slug: input.slug,
|
|
@@ -944,6 +1083,21 @@ class SqliteSkillsStore {
|
|
|
944
1083
|
const currentId = typeof current?.revision_id === "string" ? current.revision_id : null;
|
|
945
1084
|
throw new SkillRevisionConflictError(input.slug, input.expectedRevisionId, currentId);
|
|
946
1085
|
}
|
|
1086
|
+
if (input.version && carriedSha) {
|
|
1087
|
+
this.db.run(`INSERT OR IGNORE INTO skills_versions (org_id, slug, version, bundle_sha256, bundle_byte_size, storage_kind, storage_key, manifest_json, published_by_user_id, created_at)
|
|
1088
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
1089
|
+
orgId,
|
|
1090
|
+
input.slug,
|
|
1091
|
+
input.version,
|
|
1092
|
+
carriedSha,
|
|
1093
|
+
carriedSize ?? 0,
|
|
1094
|
+
input.versionStorage?.storageKind ?? "db",
|
|
1095
|
+
input.versionStorage?.storageKey ?? null,
|
|
1096
|
+
JSON.stringify(input.versionManifest ?? {}),
|
|
1097
|
+
input.principal.userId ?? null,
|
|
1098
|
+
now
|
|
1099
|
+
]);
|
|
1100
|
+
}
|
|
947
1101
|
if (previousSha && input.bundle && previousSha !== input.bundle.sha256)
|
|
948
1102
|
this.collectOrphanBundle(orgId, previousSha);
|
|
949
1103
|
this.db.run("DELETE FROM skills_tags WHERE org_id = ? AND slug = ?", [orgId, input.slug]);
|
|
@@ -1051,6 +1205,13 @@ class SqliteSkillsStore {
|
|
|
1051
1205
|
const row = this.get("SELECT * FROM skills_bundles WHERE org_id = ? AND sha256 = ? LIMIT 1", [principal.orgId, sha256]);
|
|
1052
1206
|
return row ? rowToSkillBundle(row) : null;
|
|
1053
1207
|
}
|
|
1208
|
+
async listSkillVersions(principal, slug) {
|
|
1209
|
+
return this.all("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? ORDER BY created_at DESC, version DESC", [principal.orgId, slug]).map(rowToSkillVersion);
|
|
1210
|
+
}
|
|
1211
|
+
async getSkillVersion(principal, slug, version) {
|
|
1212
|
+
const row = this.get("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [principal.orgId, slug, version]);
|
|
1213
|
+
return row ? rowToSkillVersion(row) : null;
|
|
1214
|
+
}
|
|
1054
1215
|
async pinSkill(principal, slug, metadata = {}) {
|
|
1055
1216
|
const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
1056
1217
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -1090,7 +1251,7 @@ class SqliteSkillsStore {
|
|
|
1090
1251
|
return this.all("SELECT slug FROM skills_registry WHERE org_id = ? AND tombstoned_at IS NULL ORDER BY slug ASC", [principal.orgId]).map((row) => String(row.slug));
|
|
1091
1252
|
}
|
|
1092
1253
|
collectOrphanBundle(orgId, sha256) {
|
|
1093
|
-
const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
|
|
1254
|
+
const referenced = this.get("SELECT 1 AS present FROM skills_registry WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]) ?? this.get("SELECT 1 AS present FROM skills_versions WHERE org_id = ? AND bundle_sha256 = ? LIMIT 1", [orgId, sha256]);
|
|
1094
1255
|
if (referenced)
|
|
1095
1256
|
return;
|
|
1096
1257
|
this.db.run("DELETE FROM skills_bundles WHERE org_id = ? AND sha256 = ?", [orgId, sha256]);
|
|
@@ -1119,7 +1280,7 @@ function applySqliteMigrations(db, migrationsDir = resolveMigrationsDir("sqlite"
|
|
|
1119
1280
|
const appliedNow = [];
|
|
1120
1281
|
for (const file of files) {
|
|
1121
1282
|
const version = file.replace(/\.sql$/, "");
|
|
1122
|
-
const text = readFileSync2(
|
|
1283
|
+
const text = readFileSync2(join5(migrationsDir, file), "utf8");
|
|
1123
1284
|
const apply = db.transaction(() => {
|
|
1124
1285
|
const already = db.query("SELECT 1 AS present FROM schema_migrations WHERE version = ? LIMIT 1").get(version);
|
|
1125
1286
|
if (already)
|
|
@@ -1192,7 +1353,7 @@ async function runMigrations(databaseUrl, migrationsDir) {
|
|
|
1192
1353
|
return { backend: "postgres", applied: await runPostgresMigrations(target.url, migrationsDir ?? resolveMigrationsDir("postgres")) };
|
|
1193
1354
|
}
|
|
1194
1355
|
async function runPostgresMigrations(databaseUrl, migrationsDir) {
|
|
1195
|
-
if (!
|
|
1356
|
+
if (!existsSync4(migrationsDir))
|
|
1196
1357
|
throw new Error(`migrations directory not found: ${migrationsDir}`);
|
|
1197
1358
|
const bunWithSql = Bun;
|
|
1198
1359
|
const sql = new bunWithSql.SQL(databaseUrl, { max: 1 });
|
|
@@ -1208,7 +1369,7 @@ async function runPostgresMigrations(databaseUrl, migrationsDir) {
|
|
|
1208
1369
|
const version = file.replace(/\.sql$/, "");
|
|
1209
1370
|
if (applied.has(version))
|
|
1210
1371
|
continue;
|
|
1211
|
-
const sqlText = readFileSync3(
|
|
1372
|
+
const sqlText = readFileSync3(join6(migrationsDir, file), "utf8");
|
|
1212
1373
|
await sql.unsafe("BEGIN");
|
|
1213
1374
|
try {
|
|
1214
1375
|
await sql.unsafe(sqlText);
|