@hivemagnus/hive-skills 1.0.3 → 1.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 CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  The registry is intentionally small and local-first. SQLite records skills, releases, sessions, audit events, and deployment targets; archives live under the configured data directory. The initial catalog entry is **HiveMem**, based on the existing HiveMem Hermes plugin/skill source supplied at seed time.
6
6
 
7
+ ## Install with the Hive launcher
8
+
9
+ Run `npx @hivemagnus/hive@^1.1.0 init --services all` to configure all four services,
10
+ or select `--services skills` for this registry alone. The launcher generates private
11
+ configuration, a strong session secret and TLS, and wires the Router's Skills panel.
12
+ Skills OS/PAM administration remains separate from IAM sign-in. See the
13
+ [launcher guide](../../packages/hive/README.md#include-hive-skills).
14
+
7
15
  ## Install from npm
8
16
 
9
17
  Requires Linux, Node.js 24.22.1+ or 26.13.0+, TLS certificates, and `pamtester` for
@@ -11,15 +19,18 @@ OS-backed sign-in. SQLite is provided by Node; there are no native npm add-ons.
11
19
  Agent deployment also needs Bash, OpenSSH, and permission to enable the target's user timer.
12
20
 
13
21
  ```bash
14
- npm install --prefix /opt/hive-skills/runtime @hivemagnus/hive-skills@1.0.3
22
+ npm install --prefix /opt/hive-skills/runtime @hivemagnus/hive-skills@1.2.0
15
23
  /opt/hive-skills/runtime/node_modules/.bin/hive-skills --env-file /opt/hive-skills/skills.env
16
24
  ```
17
25
 
18
26
  Or run `npx @hivemagnus/hive-skills --env-file skills.env`. The CLI accepts `start`
19
- (the default), `seed`, `--help` and `--version`. Environment-file paths may be absolute;
27
+ (the default), `seed`, `check`, `env-schema`, `migrate`, `upgrade`, `rollback`, `--help` and `--version`. `check` validates TLS,
28
+ configuration and an existing database without creating or migrating state. `env-schema` prints
29
+ configuration metadata without loading runtime configuration. Environment-file paths may be absolute;
20
30
  existing environment variables win. Without `--env-file`, the historical `./.env` lookup
21
31
  is retained. Help/version do not initialize storage. A fresh install starts with an empty
22
- catalog; startup never seeds or replaces existing records.
32
+ catalog; startup never seeds or replaces existing records. Run `hive-skills migrate`
33
+ explicitly before first startup, or enable `HIVE_AUTO_MIGRATE=true` with backup access.
23
34
 
24
35
  Configure these values in a private environment file (mode 0600):
25
36
 
@@ -145,3 +156,13 @@ version in `package.json`. CI also checks the package allowlist and npm 12 insta
145
156
  The package publishes from `.github/workflows/publish.yml` alongside the other packages;
146
157
  configure its npm trusted publisher for that workflow and the `npm-publish` environment after
147
158
  the initial authenticated publication.
159
+
160
+ ## Managed upgrades
161
+
162
+ Use `hive upgrade --dry-run`, then `hive upgrade` for the enrolled release bundle. This server's
163
+ `upgrade` and `rollback` commands delegate to the same owner-run launcher. `migrate --plan --json`
164
+ is read-only; migration requires a verified backup. Set `HIVE_AUTO_MIGRATE=true` explicitly for
165
+ startup migrations outside a managed install. Slow or one-way automatic migrations are refused.
166
+ `check --json` reports configuration without initializing state. No upgrade rotates signing keys
167
+ or reseeds existing accounts. See [UPGRADING.md](https://github.com/ChefTaco/hive/blob/main/UPGRADING.md)
168
+ for adoption, compatibility, recovery, update notices and container controls.
package/dist/app.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { maintenanceActive } from './release/runtime.js';
2
+ import { metadata } from './release/runtime.js';
3
+ import { packageRoot as releasePackageRoot } from './paths.js';
4
+ import { home as upgradeHome, updateStatus } from './release/runtime.js';
1
5
  import { bodyLimit } from 'hono/body-limit';
2
6
  import { existsSync, readFileSync } from 'node:fs';
3
7
  import { Hono } from 'hono';
@@ -50,6 +54,8 @@ const archiveUrl = (skillSlug, release, channel) => {
50
54
  const base = `${config.publicUrl}/v1/skills/${skillSlug}/releases/${release.version}/archive`;
51
55
  return channel === 'stable' ? base : `${base}?channel=${channel}`;
52
56
  };
57
+ app.use('*', async (c, next) => { if (maintenanceActive(new URL(c.req.url).pathname))
58
+ return c.json({ error: 'Hive is undergoing maintenance' }, 503); await next(); });
53
59
  app.use('*', bodyLimit({ maxSize: 70 * 1024 * 1024 }));
54
60
  for (const target of config.targets)
55
61
  store.seedTarget(target);
@@ -60,11 +66,11 @@ app.use('*', async (c, next) => {
60
66
  await next();
61
67
  });
62
68
  app.get('/', (c) => c.html(page));
63
- app.get('/health/live', (c) => c.json({ status: 'ok', service: 'hive-skills' }));
69
+ app.get('/health/live', (c) => c.json({ ...metadata(releasePackageRoot), status: 'ok', service: 'hive-skills' }));
64
70
  app.get('/healthz', (c) => {
65
71
  try {
66
72
  const health = store.readiness();
67
- return c.json({ status: health.ready ? 'ok' : 'degraded', service: 'hive-skills', ...health }, health.ready ? 200 : 503);
73
+ return c.json({ ...metadata(releasePackageRoot), status: health.ready ? 'ok' : 'degraded', service: 'hive-skills', ...health }, health.ready ? 200 : 503);
68
74
  }
69
75
  catch {
70
76
  return c.json({ status: 'degraded', service: 'hive-skills', reason: 'database_unavailable' }, 503);
@@ -179,6 +185,7 @@ app.post('/auth/logout', (c) => {
179
185
  });
180
186
  app.get('/auth/me', requireAuth(), (c) => c.json(c.get('auth')));
181
187
  app.get('/admin/skills', requireAuth(true), (c) => c.json(store.listAllSkills()));
188
+ app.get('/admin/updates', requireAuth(true), async (c) => c.json(await updateStatus(metadata(releasePackageRoot).release, upgradeHome())));
182
189
  app.post('/admin/skills', requireAuth(true), async (c) => {
183
190
  const body = await requestJson(c);
184
191
  const skill = store.createSkill({
package/dist/check.js ADDED
@@ -0,0 +1,67 @@
1
+ import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { X509Certificate, createPrivateKey } from 'node:crypto';
3
+ import { isIP } from 'node:net';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ /** Read-only preflight: never imports config/app/db, seeds, or creates state. */
7
+ export function checkConfiguration(env) {
8
+ let failures = 0;
9
+ const check = (ok, message) => { console.log(` ${ok ? 'ok ' : 'FAIL'} ${message}`); if (!ok)
10
+ failures++; };
11
+ const port = Number(env.HIVE_SKILLS_PORT ?? '4030');
12
+ check(Number.isInteger(port) && port > 0 && port <= 65535, 'HIVE_SKILLS_PORT is valid');
13
+ check((env.HIVE_SKILLS_SESSION_SECRET?.trim().length ?? 0) >= 32, 'HIVE_SKILLS_SESSION_SECRET has at least 32 characters');
14
+ let hostname;
15
+ try {
16
+ const url = new URL(env.HIVE_SKILLS_PUBLIC_URL ?? `https://localhost:${port}`);
17
+ hostname = url.hostname.replace(/^\[|\]$/g, '');
18
+ check(url.protocol === 'https:' && !url.username && !url.password && !url.search && !url.hash, 'HIVE_SKILLS_PUBLIC_URL is an HTTPS URL without credentials');
19
+ }
20
+ catch {
21
+ check(false, 'HIVE_SKILLS_PUBLIC_URL must be a valid HTTPS URL');
22
+ }
23
+ try {
24
+ const cert = new X509Certificate(readFileSync(env.HIVE_SKILLS_TLS_CERT_FILE ?? ''));
25
+ const key = createPrivateKey(readFileSync(env.HIVE_SKILLS_TLS_KEY_FILE ?? ''));
26
+ check(cert.checkPrivateKey(key), 'TLS certificate and key match');
27
+ check(Date.parse(cert.validTo) > Date.now() && Date.parse(cert.validFrom) <= Date.now(), 'TLS certificate is currently valid');
28
+ if (hostname)
29
+ check(Boolean(isIP(hostname) ? cert.checkIP(hostname) : cert.checkHost(hostname)), 'TLS certificate covers the public URL hostname');
30
+ }
31
+ catch {
32
+ check(false, 'HIVE_SKILLS_TLS_CERT_FILE and HIVE_SKILLS_TLS_KEY_FILE must be readable valid PEM files');
33
+ }
34
+ const data = resolve(env.HIVE_SKILLS_DATA_DIR ?? './data');
35
+ try {
36
+ let ancestor = data;
37
+ while (!existsSync(ancestor) && dirname(ancestor) !== ancestor)
38
+ ancestor = dirname(ancestor);
39
+ accessSync(ancestor, constants.W_OK | constants.X_OK);
40
+ check(statSync(ancestor).isDirectory(), 'Skills data directory exists and is writable, or can be created');
41
+ }
42
+ catch {
43
+ check(false, 'Skills data directory must be writable');
44
+ }
45
+ const database = resolve(data, 'registry.sqlite');
46
+ if (existsSync(database)) {
47
+ let db;
48
+ try {
49
+ db = new DatabaseSync(database, { readOnly: true });
50
+ check(db.prepare('PRAGMA quick_check').get()?.quick_check === 'ok', 'SQLite integrity');
51
+ check(db.prepare('PRAGMA foreign_key_check').all().length === 0, 'SQLite foreign keys');
52
+ db.prepare('SELECT slug FROM skills LIMIT 1').get();
53
+ }
54
+ catch {
55
+ check(false, 'Existing database must be a readable Skills registry');
56
+ }
57
+ finally {
58
+ db?.close();
59
+ }
60
+ }
61
+ else
62
+ console.log(' info Registry database will be created on first start; catalog starts empty');
63
+ if (!env.HIVE_SKILLS_ADMIN_USERS?.trim())
64
+ console.log(' ! HIVE_SKILLS_ADMIN_USERS is empty; OS administration is disabled');
65
+ console.log(failures ? `Result: ${failures} problem(s)` : 'Result: ready to start');
66
+ return failures ? 1 : 0;
67
+ }
package/dist/cli.js CHANGED
@@ -4,10 +4,16 @@ import { resolve } from 'node:path';
4
4
  import { packageRoot } from './paths.js';
5
5
  const usage = `Hive Skills: HTTPS skill registry and Hermes distribution bridge.
6
6
 
7
- Usage: hive-skills [start|seed] [--env-file FILE]
7
+ Usage: hive-skills [start|seed|check|env-schema|migrate|upgrade|rollback] [--env-file FILE]
8
8
  hive-skills --help | --version
9
9
 
10
10
  start Start the registry (default); requires TLS key and certificate.
11
+ check Validate configuration, TLS and existing storage without writing.
12
+ --json prints a JSON report.
13
+ migrate --plan [--json] inspects; otherwise back up and migrate (--backup-dir DIR).
14
+ upgrade Delegate a complete managed bundle upgrade to the Hive launcher.
15
+ rollback Delegate managed code rollback or explicitly confirmed data restore.
16
+ env-schema Print configuration metadata as JSON without loading configuration.
11
17
  seed Explicitly seed the bundled HiveMem skill; never runs on start.
12
18
  --env-file Load a dotenv file; existing environment values win.
13
19
 
@@ -16,6 +22,27 @@ HIVE_SKILLS_ADMIN_USERS, HIVE_SKILLS_TLS_KEY_FILE and HIVE_SKILLS_TLS_CERT_FILE.
16
22
  See the package README for the complete configuration and migration guide.`;
17
23
  export async function main(args) {
18
24
  try {
25
+ const special = args.find(arg => ['upgrade', 'rollback', 'migrate'].includes(arg));
26
+ if (special) {
27
+ const rest = [...args];
28
+ rest.splice(rest.indexOf(special), 1);
29
+ for (let i = 0; i < rest.length; i++) {
30
+ if (rest[i] === '--env-file' || rest[i]?.startsWith('--env-file=')) {
31
+ const inline = rest[i].startsWith('--env-file=');
32
+ const file = inline ? rest[i].slice(11) : rest[i + 1];
33
+ if (!file)
34
+ throw new Error('--env-file requires a file');
35
+ process.loadEnvFile(resolve(file));
36
+ rest.splice(i, inline ? 1 : 2);
37
+ i--;
38
+ }
39
+ }
40
+ if (special === 'migrate') {
41
+ await (await import('./upgrade.js')).skillsMigration(rest);
42
+ return 0;
43
+ }
44
+ return (await import('./release/runtime.js')).delegate([special, ...rest]);
45
+ }
19
46
  if (args.length === 1 && ['--help', '-h'].includes(args[0])) {
20
47
  console.log(usage);
21
48
  return 0;
@@ -25,6 +52,11 @@ export async function main(args) {
25
52
  console.log(manifest.version);
26
53
  return 0;
27
54
  }
55
+ if (args.length === 1 && args[0] === 'env-schema') {
56
+ const { envSchemaDocument } = await import('./env-schema.js');
57
+ console.log(JSON.stringify(envSchemaDocument()));
58
+ return 0;
59
+ }
28
60
  let command;
29
61
  const files = [];
30
62
  for (let index = 0; index < args.length; index++) {
@@ -35,15 +67,33 @@ export async function main(args) {
35
67
  throw new Error('--env-file requires a file path');
36
68
  files.push(file);
37
69
  }
38
- else if (!command && (arg === 'start' || arg === 'seed'))
70
+ else if (!command && (arg === 'start' || arg === 'seed' || arg === 'check'))
39
71
  command = arg;
72
+ else if (arg === '--json' && command === 'check')
73
+ continue;
40
74
  else
41
75
  throw new Error('Invalid arguments. Run hive-skills --help.');
42
76
  }
43
77
  for (const file of files)
44
78
  process.loadEnvFile(resolve(file));
45
- if (command === 'seed')
79
+ if (command === 'check') {
80
+ const { checkConfiguration } = await import('./check.js');
81
+ // Preserve the historical cwd env lookup without importing the writable config module.
82
+ if (files.length === 0) {
83
+ const { existsSync } = await import('node:fs');
84
+ if (existsSync('.env'))
85
+ process.loadEnvFile('.env');
86
+ }
87
+ if (args.includes('--json'))
88
+ return (await import('./release/runtime.js')).captureCheck(() => checkConfiguration(process.env));
89
+ const code = checkConfiguration(process.env);
90
+ await (await import('./release/runtime.js')).printUpdateNotice(JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8')).version);
91
+ return code;
92
+ }
93
+ if (command === 'seed') {
94
+ await (await import('./upgrade.js')).skillsMigration([]);
46
95
  await import('./seed.js');
96
+ }
47
97
  else
48
98
  await import('./server.js');
49
99
  return 0;
package/dist/db.js CHANGED
@@ -3,59 +3,9 @@ import { existsSync } from 'node:fs';
3
3
  import { DatabaseSync } from 'node:sqlite';
4
4
  import { config } from './config.js';
5
5
  import { compatibleVersion } from './compatibility.js';
6
+ if (!existsSync(config.databasePath))
7
+ throw new Error('Skills database is missing; run hive-skills migrate before opening storage');
6
8
  const db = new DatabaseSync(config.databasePath, { enableForeignKeyConstraints: true });
7
- db.exec(`
8
- PRAGMA journal_mode = WAL;
9
- CREATE TABLE IF NOT EXISTS skills (
10
- slug TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, homepage TEXT,
11
- enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL
12
- ) STRICT;
13
- CREATE TABLE IF NOT EXISTS releases (
14
- skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE, version TEXT NOT NULL,
15
- sha256 TEXT NOT NULL, archive_path TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL,
16
- min_hermes_version TEXT, max_hermes_version TEXT,
17
- PRIMARY KEY(skill_slug, version)
18
- ) STRICT;
19
- CREATE TABLE IF NOT EXISTS sessions (
20
- id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL
21
- ) STRICT;
22
- CREATE TABLE IF NOT EXISTS deployment_targets (
23
- name TEXT PRIMARY KEY, host TEXT NOT NULL, username TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1))
24
- ) STRICT;
25
- CREATE TABLE IF NOT EXISTS channel_releases (
26
- channel TEXT NOT NULL CHECK(channel IN ('stable','canary')), skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE,
27
- version TEXT NOT NULL, PRIMARY KEY(channel,skill_slug), FOREIGN KEY(skill_slug,version) REFERENCES releases(skill_slug,version)
28
- ) STRICT;
29
- CREATE TABLE IF NOT EXISTS target_policies (
30
- target TEXT PRIMARY KEY REFERENCES deployment_targets(name), channel TEXT NOT NULL CHECK(channel IN ('stable','canary')),
31
- token_hash TEXT NOT NULL, skills_json TEXT, receipt_json TEXT, updated_at TEXT NOT NULL
32
- ) STRICT;
33
- CREATE TABLE IF NOT EXISTS registry_migrations (id TEXT PRIMARY KEY) STRICT;
34
- CREATE TABLE IF NOT EXISTS deployment_jobs (
35
- id TEXT PRIMARY KEY, target TEXT NOT NULL, actor TEXT NOT NULL, status TEXT NOT NULL,
36
- result TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
37
- ) STRICT;
38
- CREATE TABLE IF NOT EXISTS audit_log (
39
- id INTEGER PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, subject TEXT NOT NULL, details TEXT NOT NULL, created_at TEXT NOT NULL
40
- ) STRICT;
41
- `);
42
- const releaseColumns = new Set(db.prepare('PRAGMA table_info(releases)').all().map((column) => column.name));
43
- if (!releaseColumns.has('min_hermes_version'))
44
- db.exec('ALTER TABLE releases ADD COLUMN min_hermes_version TEXT');
45
- if (!releaseColumns.has('max_hermes_version'))
46
- db.exec('ALTER TABLE releases ADD COLUMN max_hermes_version TEXT');
47
- // Preserve the currently published version during the first channel migration.
48
- if (!db.prepare("SELECT id FROM registry_migrations WHERE id='release-channels'").get()) {
49
- db.exec("BEGIN IMMEDIATE");
50
- try {
51
- db.exec("INSERT OR IGNORE INTO channel_releases SELECT 'stable', skill_slug, version FROM releases r WHERE created_at=(SELECT MAX(created_at) FROM releases WHERE skill_slug=r.skill_slug)");
52
- db.exec("INSERT INTO registry_migrations VALUES ('release-channels'); COMMIT");
53
- }
54
- catch (error) {
55
- db.exec('ROLLBACK');
56
- throw error;
57
- }
58
- }
59
9
  const tokenHash = (token) => createHash('sha256').update(token).digest('hex');
60
10
  const now = () => new Date().toISOString();
61
11
  const mapSkill = (row) => ({ slug: row.slug, name: row.name, description: row.description, homepage: row.homepage, enabled: row.enabled === 1, createdAt: row.created_at, updatedAt: row.updated_at });
@@ -1,12 +1,14 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { config } from './config.js';
3
3
  import { store } from './db.js';
4
+ import { maintenanceActive } from './release/runtime.js';
4
5
  /** One local deployment at a time. Output may contain credentials: retain only exit codes. */
5
6
  export function startDeploymentWorker() {
6
7
  let busy = false;
8
+ let active = Promise.resolve();
7
9
  store.recoverDeployments();
8
10
  const timer = setInterval(() => {
9
- if (busy)
11
+ if (busy || maintenanceActive('/worker'))
10
12
  return;
11
13
  const job = store.claimDeployment();
12
14
  if (!job)
@@ -26,8 +28,10 @@ export function startDeploymentWorker() {
26
28
  HIVE_SKILLS_TARGET_USER: target.username, HIVE_SKILLS_KEY_ONLY: 'true' },
27
29
  });
28
30
  let finished = false;
31
+ let complete = () => { };
32
+ active = new Promise((done) => { complete = done; });
29
33
  const finish = (ok, code) => { if (finished)
30
- return; finished = true; store.finishDeployment(job.id, ok, code); busy = false; };
34
+ return; finished = true; store.finishDeployment(job.id, ok, code); busy = false; complete(); };
31
35
  child.once('error', () => finish(false, 'launch_failed'));
32
36
  child.once('close', (code) => finish(code === 0, code === 0 ? 'timer_installed' : `exit_${code ?? 'signal'}`));
33
37
  }
@@ -37,5 +41,5 @@ export function startDeploymentWorker() {
37
41
  }
38
42
  }, 1000);
39
43
  timer.unref();
40
- return () => clearInterval(timer);
44
+ return async () => { clearInterval(timer); await active; };
41
45
  }
@@ -0,0 +1,26 @@
1
+ import { commonEnv } from './release/runtime.js';
2
+ import { readFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { packageRoot } from './paths.js';
5
+ /** Safe to call before configuration is loaded; contains no runtime values. */
6
+ export function envSchemaDocument() {
7
+ const { version } = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
8
+ return {
9
+ service: 'skills', package: '@hivemagnus/hive-skills', version, defaultPort: 4030,
10
+ healthPath: '/healthz', consolePath: '/', dependencies: [],
11
+ env: [...commonEnv(),
12
+ { name: 'NODE_ENV', default: 'production', description: 'Runtime environment.' },
13
+ { name: 'HIVE_SKILLS_HOST', default: '127.0.0.1', description: 'Listener bind address.' },
14
+ { name: 'HIVE_SKILLS_PORT', default: '4030', description: 'HTTPS listener port.' },
15
+ { name: 'HIVE_SKILLS_PUBLIC_URL', default: 'https://localhost:4030', description: 'Public HTTPS registry URL used in archive links.' },
16
+ { name: 'HIVE_SKILLS_DATA_DIR', default: './data', description: 'SQLite database and immutable skill archives.' },
17
+ { name: 'HIVE_SKILLS_SESSION_SECRET', required: true, secret: true, generate: 'hex32', description: 'Session signing secret; at least 32 characters.' },
18
+ { name: 'HIVE_SKILLS_ADMIN_USERS', description: 'Comma-separated OS accounts allowed to administer the registry; independent of IAM.' },
19
+ { name: 'HIVE_SKILLS_PAM_SERVICE', default: 'hive-skills', description: 'Installed PAM policy for OS authentication; use login for a standard Linux policy.' },
20
+ { name: 'HIVE_SKILLS_TLS_KEY_FILE', required: true, description: 'TLS private-key PEM file.' },
21
+ { name: 'HIVE_SKILLS_TLS_CERT_FILE', required: true, description: 'TLS certificate PEM file.' },
22
+ { name: 'HIVE_SKILLS_DEPLOY_SCRIPT', description: 'Optional override for the bundled agent deployment helper.' },
23
+ { name: 'HIVE_SKILLS_TARGETS', description: 'Optional initial deployment targets: name=user@host, comma-separated.' },
24
+ ].map((entry) => ({ required: false, secret: false, ...entry })),
25
+ };
26
+ }
@@ -0,0 +1,54 @@
1
+ export function initializeSchema(db) {
2
+ db.exec(`
3
+ PRAGMA journal_mode = WAL;
4
+ CREATE TABLE IF NOT EXISTS skills (
5
+ slug TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, homepage TEXT,
6
+ enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL
7
+ ) STRICT;
8
+ CREATE TABLE IF NOT EXISTS releases (
9
+ skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE, version TEXT NOT NULL,
10
+ sha256 TEXT NOT NULL, archive_path TEXT NOT NULL, notes TEXT, created_at TEXT NOT NULL,
11
+ min_hermes_version TEXT, max_hermes_version TEXT,
12
+ PRIMARY KEY(skill_slug, version)
13
+ ) STRICT;
14
+ CREATE TABLE IF NOT EXISTS sessions (
15
+ id TEXT PRIMARY KEY, username TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL
16
+ ) STRICT;
17
+ CREATE TABLE IF NOT EXISTS deployment_targets (
18
+ name TEXT PRIMARY KEY, host TEXT NOT NULL, username TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1))
19
+ ) STRICT;
20
+ CREATE TABLE IF NOT EXISTS channel_releases (
21
+ channel TEXT NOT NULL CHECK(channel IN ('stable','canary')), skill_slug TEXT NOT NULL REFERENCES skills(slug) ON DELETE CASCADE,
22
+ version TEXT NOT NULL, PRIMARY KEY(channel,skill_slug), FOREIGN KEY(skill_slug,version) REFERENCES releases(skill_slug,version)
23
+ ) STRICT;
24
+ CREATE TABLE IF NOT EXISTS target_policies (
25
+ target TEXT PRIMARY KEY REFERENCES deployment_targets(name), channel TEXT NOT NULL CHECK(channel IN ('stable','canary')),
26
+ token_hash TEXT NOT NULL, skills_json TEXT, receipt_json TEXT, updated_at TEXT NOT NULL
27
+ ) STRICT;
28
+ CREATE TABLE IF NOT EXISTS registry_migrations (id TEXT PRIMARY KEY) STRICT;
29
+ CREATE TABLE IF NOT EXISTS deployment_jobs (
30
+ id TEXT PRIMARY KEY, target TEXT NOT NULL, actor TEXT NOT NULL, status TEXT NOT NULL,
31
+ result TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL
32
+ ) STRICT;
33
+ CREATE TABLE IF NOT EXISTS audit_log (
34
+ id INTEGER PRIMARY KEY, actor TEXT NOT NULL, action TEXT NOT NULL, subject TEXT NOT NULL, details TEXT NOT NULL, created_at TEXT NOT NULL
35
+ ) STRICT;
36
+ `);
37
+ const releaseColumns = new Set(db.prepare('PRAGMA table_info(releases)').all().map((column) => column.name));
38
+ if (!releaseColumns.has('min_hermes_version'))
39
+ db.exec('ALTER TABLE releases ADD COLUMN min_hermes_version TEXT');
40
+ if (!releaseColumns.has('max_hermes_version'))
41
+ db.exec('ALTER TABLE releases ADD COLUMN max_hermes_version TEXT');
42
+ // Preserve the currently published version during the first channel migration.
43
+ if (!db.prepare("SELECT id FROM registry_migrations WHERE id='release-channels'").get()) {
44
+ db.exec('BEGIN IMMEDIATE');
45
+ try {
46
+ db.exec("INSERT OR IGNORE INTO channel_releases SELECT 'stable', skill_slug, version FROM releases r WHERE created_at=(SELECT MAX(created_at) FROM releases WHERE skill_slug=r.skill_slug)");
47
+ db.exec("INSERT INTO registry_migrations VALUES ('release-channels'); COMMIT");
48
+ }
49
+ catch (error) {
50
+ db.exec('ROLLBACK');
51
+ throw error;
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Colin McDonnell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.