Skip to content

feat(cli): add pbkit schema subcommand for schema management - #60

Merged
Karnak19 merged 3 commits into
mainfrom
feat/schema-cli
Jun 9, 2026
Merged

feat(cli): add pbkit schema subcommand for schema management#60
Karnak19 merged 3 commits into
mainfrom
feat/schema-cli

Conversation

@Karnak19

@Karnak19 Karnak19 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

Closes #59

What

Adds a pbkit schema CLI subcommand that manages PocketBase collection definitions directly against the admin API (/api/collections), replacing the ad-hoc curl + python flow. Schema-only — record CRUD is intentionally out of scope.

pbkit schema list                       # list collections (name + type)
pbkit schema get <collection>           # dump one collection as JSON
pbkit schema pull [--out pb-schema.json]# download the snapshot the generator reads
pbkit schema apply <file.json>          # import definitions (non-destructive)
pbkit schema add-field <collection> <json>
pbkit schema add-index <collection> "<sql>"
pbkit schema set-rule <collection> --list/--view/--create/--update/--delete <rule>
pbkit schema create-view <name> --query "<sql>"

Auth

Superuser auth via _superusers/auth-with-password. Credentials resolve from env (preferred) with config fallback, never inline:
POCKETBASE_URL, POCKETBASE_ADMIN_EMAIL, POCKETBASE_ADMIN_PASSWORD, or POCKETBASE_ADMIN_TOKEN.

Acceptance criteria

  • pull produces the same shape the generator reads (raw /api/collections array → parseJson).
  • apply is idempotent and non-destructive by default (deleteMissing: false); opt-in --delete-missing.
  • Schema-only — no record create/update/delete exposed.
  • Partial updates (add-field, add-index, set-rule) fetch current state and patch — never clobber unrelated fields/indexes/rules.
  • Docs updated (README, docs site CLI reference + new how-to, and the pbkit skill).

Notes

  • add-field/add-index send the full merged array because PocketBase replaces array fields wholesale on PATCH.
  • For set-rule, a value of "null" makes a rule superuser-only and "" makes it public.

Verification

  • tsc --noEmit clean
  • bun test — 138 pass / 0 fail (8 new tests covering merge + auth resolution)
  • Astro docs site builds (22 pages)

🤖 Generated with Claude Code

Karnak19 and others added 2 commits June 9, 2026 10:23
Adds list/get/pull/apply/add-field/add-index/set-rule/create-view commands
operating over PocketBase's /api/collections admin API with superuser auth.
Credentials resolve from env (POCKETBASE_URL/ADMIN_EMAIL/ADMIN_PASSWORD/
ADMIN_TOKEN) with config fallback, never inline. pull matches the generator's
snapshot shape, apply is non-destructive by default, and partial ops
fetch-and-patch to avoid clobbering unrelated fields/indexes/rules.

Closes #59

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a CLI reference section, a "Manage schema from the CLI" how-to guide,
and a schema-management section in the pbkit skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two functional issues found in the new schema CLI:

  • -c short flag is silently ignoredsplitArgs only handles -- prefixed flags, so -c ./config.ts is treated as two positional args. The help text advertises -c as a working alias.
  • Incomplete email/password pair silently falls through — setting POCKETBASE_ADMIN_EMAIL without POCKETBASE_ADMIN_PASSWORD (or vice versa) skips email/password auth without warning, leading to a misleading "No superuser credentials found" error.

Comment thread packages/pbkit/src/cli/schema.ts Outdated
const flags = new Map<string, string | true>()
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith("--")) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faefb37splitArgs now recognizes single-dash short flags (-c <path>), and the look-ahead treats a following -x/--flag as a boolean rather than swallowing it as a value. Added tests for short/long/boolean parsing.

const auth = resolveAuthSettings(config)

let token = auth.token
if (auth.email && auth.password) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faefb37createSchemaClient now throws a specific error when only one of POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD is set, before the generic token fallback. Added a test asserting the partial-pair rejection.

- splitArgs now recognizes single-dash short flags (e.g. `-c <path>`), which
  were previously dropped into positionals despite being advertised in help.
- createSchemaClient throws a specific error when only one of
  POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD is set, instead of
  silently falling through to a misleading "No superuser credentials" error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread packages/pbkit/src/cli/schema.ts Outdated
const flags = new Map<string, string | true>()
for (let i = 0; i < args.length; i++) {
const arg = args[i]
if (arg.startsWith("--")) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faefb37splitArgs now recognizes single-dash short flags (-c <path>), and the look-ahead treats a following -x/--flag as a boolean rather than swallowing it as a value. Added tests for short/long/boolean parsing.

const auth = resolveAuthSettings(config)

let token = auth.token
if (auth.email && auth.password) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in faefb37createSchemaClient now throws a specific error when only one of POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD is set, before the generic token fallback. Added a test asserting the partial-pair rejection.

@Karnak19
Karnak19 merged commit bbc31a9 into main Jun 9, 2026
7 checks passed
@Karnak19
Karnak19 deleted the feat/schema-cli branch June 9, 2026 09:01

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One test reliability issue found in the new schema command tests:

  • withEnv helper doesn't support async callbacks — the partial-credential test passes an async function whose assertions become fire-and-forget, so failures could be silently swallowed as unhandled rejections.

expect(() => resolveAuthSettings(undefined)).toThrow("No PocketBase URL")
})
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: withEnv is typed as fn: () => void and calls fn() synchronously without awaiting the result. This test passes an async callback, so the returned promise is fire-and-forget — withEnv's finally block restores env vars immediately, and await withEnv(...) resolves before the assertions run.

Impact: The test currently passes because resolveAuthSettings reads env vars synchronously before the first await, so the values are correct at read time. However, if createSchemaClient were ever refactored to read env vars asynchronously, or if the assertion itself failed, the failure would surface as an unhandled promise rejection rather than a test failure — the test runner might mark the test as passed before the rejection is reported.

Fix: Make withEnv async-aware so the env scope covers the full callback lifetime:

async function withEnv(
  env: Record<string, string | undefined>,
  fn: () => void | Promise<void>,
) {
  const saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]))
  for (const k of ENV_KEYS) delete process.env[k]
  Object.assign(process.env, env)
  try {
    await fn()
  } finally {
    for (const k of ENV_KEYS) delete process.env[k]
    for (const [k, v] of Object.entries(saved)) if (v !== undefined) process.env[k] = v
  }
}

The existing synchronous callers work unchanged since await on a non-promise is a no-op.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding in the test helper that could lead to silently passing tests:

  • withEnv doesn't await async callbacks, making the partial credential test's assertions fire-and-forget promises

for (const k of ENV_KEYS) delete process.env[k]
Object.assign(process.env, env)
try {
fn()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Problem: withEnv calls fn() synchronously without awaiting it. The "rejects a partial email/password pair" test passes an async callback, but withEnv returns void — so await withEnv(...) is a no-op and the inner expect(...).rejects.toThrow(...) assertions are fire-and-forget promises.

Impact: The test currently works because resolveAuthSettings runs synchronously before the first await in createSchemaClient, so env vars are read before the finally block restores them. But if the assertion fails, the failure surfaces as an unhandled rejection rather than a proper test failure. More importantly, if createSchemaClient is ever refactored to do async work before calling resolveAuthSettings, the env vars would be restored before they're read, and the test would fail with a misleading error.

Fix: Make withEnv async and await the callback:

async function withEnv(env: Record<string, string | undefined>, fn: () => Promise<void> | void) {
  const saved = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]))
  for (const k of ENV_KEYS) delete process.env[k]
  Object.assign(process.env, env)
  try {
    await fn()
  } finally {
    for (const k of ENV_KEYS) delete process.env[k]
    for (const [k, v] of Object.entries(saved)) if (v !== undefined) process.env[k] = v
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cli): add a pbkit schema subcommand for PocketBase schema management

1 participant