Skip to content

VSKI v0.15.1

The vski CLI and declarative IaC (vski-sdk 0.9.0)

Section titled “The vski CLI and declarative IaC (vski-sdk 0.9.0)”

v0.15.0 made pipelines, secrets and schedules first-class server citizens — all of it managed so far through the dashboard and ad-hoc curl. v0.15.1 adds the dev and devops standard on top: a Python CLI that covers the full management API, and a versioned YAML format that converges a deployment from files in git — collections, pipelines, cron jobs, webhooks, secrets, plugin instances and graphs — with plan/apply/pull semantics and CI-friendly exit codes.

Terminal window
pip install "vski-sdk[cli]" \
--extra-index-url https://vski.sh/api/packages/x/pypi/simple/
export VSKI_URL=https://vski.example.com
vski keys create dev --roles admin # raw key shown once
export VSKI_API_KEY=vski_...
vski status
vski plan # diff ./vski against the deployment
vski apply --yes # converge
vski pull # export live state back to YAML

Every command resolves its target the same way — flags > VSKI_URL/VSKI_API_KEY/VSKI_TOKEN/VSKI_DB env > a .env file > ~/.vski/config.json (written by vski login; same file the old Deno CLI used, so workstations carry over). Admin-role API keys are full admins; a workflows-role key scopes to pipelines and secrets. --json makes every command scriptable.

Command surface: login/logout/whoami/status, db, collections, records (CRUD + search), sql/query, migrations (list/status/generate/apply/rollback/upload/export), pipelines (list/show/validate/run/tokens/trigger), runs (list/show/events/cancel over workflow and pipeline runs alike), cron, webhooks, graphs, secrets, plugins + plugins instances (start/stop/restart/invoke/logs), keys, replicas — and the IaC verbs plan/apply/pull, both as top-level commands and under vski iac.

A deployment’s desired state lives in a vski/ directory; every YAML file under it is merged (recursively, in path order). The format is strict: each file declares version: 1, and unknown keys at any level are errors, never warnings — a typo never deploys half a resource.

version: 1
database: default # scopes collections/graphs/plugins
collections:
notes:
fields:
- {name: title, type: text, required: true}
rules: {list: "*", create: null} # null ≡ "" = admin-only
indexes: [{name: idx_created, sql: "CREATE INDEX idx_created ON notes(created)"}]
fts: {fields: [title, body]}
pipelines: # global; the map key is the name
note-intake:
triggers: [{type: collection, collection: notes, actions: [create]}]
steps: [{id: enrich, type: plugin, target: hello/echo, params: {message: "{{trigger.record.title}}"}}]
cron: {nightly: {schedule: "0 2 * * *", type: http, url: "pipeline://note-intake"}}
secrets: {openai_key: {fromEnv: OPENAI_API_KEY}} # values never in YAML
plugins: {hello: {plugin: hello, config: {greeting: hi}}}
graphs: {knowledge: {rules: {list: "*"}}}
databases: {analytics: {description: Analytics}} # ensure-exists only

Semantics worth knowing:

  • Sub-blocks are opt-in per resource. An omitted rules: block is left unmanaged; a present one is authoritative. fields is required when creating a collection; modifying an existing field (type, required, …) is rejected with an explanation — migrations support add/drop only.
  • Collection changes always go through the migration system. The CLI diffs desired vs live, renders an ordinary migration file (the same format the dashboard’s generator emits), uploads it via POST /api/migrations/save and applies it — schema changes keep their audit trail, pre-migration snapshots and rollback. `—save-migrations ` also writes the generated YAML to disk for review artifacts.
  • Secrets are references, never values. Apply reads the env var at execution time; a new secret whose env var is unset is a warning, not drift, so plan can gate CI on convergence in environments that legitimately don’t hold the value.
  • Deletion is deliberate. state: absent removes one resource (collections through a soft drop_table migration); --prune opts into deleting undeclared resources of the kinds your files manage. Databases are never dropped by IaC.
  • Pipelines are validated server-side before anything is applied — an invalid definition aborts the apply with the per-step issue list.
  • plan exits 0 clean, 1 on error, 2 on drift — a drop-in CI gate.
  • pull exports live state (rules, indexes, views, FTS fields, pipeline definitions, plugin configs) in the exact format apply consumes; the round trip is a no-op by construction and covered by tests. Secret values come out as fromEnv: SECRET_<NAME> placeholders.

The app template is the reference IaC repository: vski/ declares the notes collection, a hello plugin instance, a note-intake pipeline and a nightly pipeline:// kick; make iac-plan/iac-apply/iac-pull wrap the CLI, and the e2e suite proves the loop end-to-end — a new note triggers the pipeline through the plugin’s warm function. New wiki pages: CLI and IaC.

Fix: FTS sync triggers broke all record writes on indexed collections

Section titled “Fix: FTS sync triggers broke all record writes on indexed collections”

CreateFtsTableInDB generated its sync triggers with bare column references (VALUES (new.id, title, body) — unresolvable inside the trigger) and keyed the index on the TEXT id column while FTS5 rowids are INTEGER. Net effect: inserting or updating a record in any collection with an FTS index failed (no such column: <field> / datatype mismatch). Triggers now read new.*/old.* qualified columns and key on the table’s implicit integer rowid (content_rowid='rowid'), matching the search path (rowid IN (SELECT rowid FROM <fts> ...)).

Existing FTS indexes created by older builds still carry the broken triggers — recreate them once (drop and re-add the FTS index in the dashboard, or declare fts: in IaC and let apply drop/create it).

  • vski.api.secrets was unimportable since v0.15.0 (a broken merge left a wrong base-class import and dead duplicated methods) — the secrets namespace now loads and is exercised by tests.
  • SyncVskiClient was missing the pipelines, secrets and plugins namespaces (the sync mirror had not kept up with the async client).
  • settings.migrations.save(migration) added — posts a MigrationFile JSON body to POST /api/migrations/save (the upload endpoint round-trips a file; save is what IaC apply uses).
  • _version.py resynced with pyproject.toml (was two versions behind).

The minimal Deno CLI in @vski/sdk (login/db/migrate) is deprecated in favor of the Python CLI; its README says so.

  • Python SDK: 30 new unit tests (config precedence, strict loader, the diff engine including the pull→apply round-trip no-op property) and ten new e2e tests across two suites — the CLI surface (login/keys/db/secrets/sql) and the IaC lifecycle (plan drift, apply, convergence, field add/drop + rule/pipeline mutations, state: absent, pull round-trip against a fresh deployment, field-modification rejection). The full e2e suite — 117 tests — passes.
  • Server: the repositories suite passes over the FTS trigger change; the template’s e2e covers an FTS-indexed collection receiving writes through a pipeline — the case the old triggers broke.
  • Additive: no API, storage or wire changes beyond the new SDK version and the corrected FTS trigger DDL. vski-sdk 0.9.0 keeps the 0.8 API; the CLI arrives as an optional extra (pip install vski-sdk[cli]).
  • The FTS fix changes generated trigger DDL only; indexes created by older binaries must be recreated once to pick up working triggers (see above).
  • IaC manages what its files declare: adopting it against an existing deployment starts as a no-op — vski pull first, review, commit, then let plan gate your CI.