Skip to content

schema delete --force never sends force to the API, so a collection with content cannot be deleted from the CLI #2994

Description

@jakevis

Summary

emdash schema delete --force cannot delete a collection that has content. The flag only skips the CLI's own confirmation prompt — it is never sent to the API — so the request is rejected every time:

$ npx emdash schema delete rules --force
 ERROR  Collection "rules" has content. Use force: true to delete.

There is no other CLI path to it. --force is the only force-shaped option the command has, so a collection with rows cannot be deleted from the CLI at all.

Affected version: emdash@0.32.0, verified unchanged in 0.36.0 (current latest).

Mechanism

The command consumes --force locally and then calls the client without it:

// packages/core/src/cli/commands/schema.ts  (0.36.0)
if (!args.force) {
  const confirmed = await consola.prompt(`Delete collection "${args.collection}"?`, { type: "confirm" });
  if (!confirmed) { consola.info("Cancelled"); return; }
}
const client = createClientFromArgs(args);
await client.deleteCollection(args.collection);   // ← force dropped here

The client has no parameter to carry it:

// packages/core/src/client/index.ts  (0.36.0:548)
async deleteCollection(slug: string): Promise<void> {
  await this.request<unknown>("DELETE", `/schema/collections/${encodeURIComponent(slug)}`);
}

The route does support it, as a query parameter:

// packages/core/src/astro/routes/api/schema/collections/[slug]/index.ts  (0.36.0:63)
const force = url.searchParams.get("force") === "true";

So the capability exists end-to-end except for the two layers in between. The MCP server passes it correctly, which is why the same operation works there:

// packages/core/src/mcp/server.ts
await registry.deleteCollection(args.slug, { force: args.force });

Reproduction

  1. Create a collection and add one entry.
  2. npx emdash schema delete <slug> --force
  3. Fails with Collection "<slug>" has content. Use force: true to delete.

DELETE /_emdash/api/schema/collections/<slug>?force=true succeeds against the same instance.

Impact

Any scripted cleanup has to bypass the CLI and call the REST endpoint directly. It bit us migrating a site away from four collections whose content had moved into a pages entry — the collections were unreferenced and safe to drop, but --force refused, so the migration script had to hand-roll the HTTP call and its own auth.

The failure is also confusing rather than merely unhelpful: the error names the exact option the user just passed.

Suggested fix

Thread the flag through both layers:

// client
async deleteCollection(slug: string, options?: { force?: boolean }): Promise<void> {
  const query = options?.force ? "?force=true" : "";
  await this.request<unknown>("DELETE", `/schema/collections/${encodeURIComponent(slug)}${query}`);
}

// command
await client.deleteCollection(args.collection, { force: args.force });

That does conflate two meanings of --force — "don't ask me" and "delete even though it has content" — which may be intended, since the prompt already exists to guard the destructive case. If they should stay separate, a distinct flag (--force-content, say) would keep the confirmation prompt meaningful for empty collections while giving scripts a way through. Happy to open a PR for whichever shape you prefer.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions