diff --git a/.changeset/six-pugs-juggle.md b/.changeset/six-pugs-juggle.md new file mode 100644 index 0000000..7ece385 --- /dev/null +++ b/.changeset/six-pugs-juggle.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fix CLI `--json` flag so JSON output is clean. Previously, `consola.success()` and other log messages leaked into stdout alongside the JSON data, making it unparseable by scripts. Log messages now go to stderr when `--json` is set. diff --git a/packages/core/package.json b/packages/core/package.json index df54e63..20748c3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -149,7 +149,8 @@ "prepublishOnly": "node --run build", "typecheck": "tsgo --noEmit", "check": "publint && attw --pack --ignore-rules=cjs-resolves-to-esm --ignore-rules=no-resolution --ignore-rules=internal-resolution-error", - "test": "vitest" + "test": "vitest", + "test:smoke": "vitest run --config vitest.smoke.config.ts" }, "dependencies": { "@emdash-cms/admin": "workspace:*", diff --git a/packages/core/src/cli/commands/content.ts b/packages/core/src/cli/commands/content.ts index da2d4cb..0a8bfab 100644 --- a/packages/core/src/cli/commands/content.ts +++ b/packages/core/src/cli/commands/content.ts @@ -10,7 +10,7 @@ import { defineCommand } from "citty"; import { consola } from "consola"; import { connectionArgs, createClientFromArgs } from "../client-factory.js"; -import { output } from "../output.js"; +import { configureOutputMode, output } from "../output.js"; // --------------------------------------------------------------------------- // Helpers @@ -77,6 +77,7 @@ const listCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const result = await client.list(args.collection, { @@ -130,6 +131,7 @@ const getCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const item = await client.get(args.collection, args.id, { @@ -177,6 +179,7 @@ const createCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const data = await readInputData(args); const client = createClientFromArgs(args); @@ -229,6 +232,7 @@ const updateCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const data = await readInputData(args); const client = createClientFromArgs(args); @@ -270,6 +274,7 @@ const deleteCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); await client.delete(args.collection, args.id); @@ -297,6 +302,7 @@ const publishCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); await client.publish(args.collection, args.id); @@ -324,6 +330,7 @@ const unpublishCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); await client.unpublish(args.collection, args.id); @@ -356,6 +363,7 @@ const scheduleCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); await client.schedule(args.collection, args.id, { at: args.at }); @@ -383,6 +391,7 @@ const restoreCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); await client.restore(args.collection, args.id); @@ -410,6 +419,7 @@ const translationsCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const translations = await client.translations(args.collection, args.id); diff --git a/packages/core/src/cli/commands/login.ts b/packages/core/src/cli/commands/login.ts index 5317367..06d4b8d 100644 --- a/packages/core/src/cli/commands/login.ts +++ b/packages/core/src/cli/commands/login.ts @@ -29,6 +29,7 @@ import { resolveCredentialKey, saveCredentials, } from "../credentials.js"; +import { configureOutputMode } from "../output.js"; // --------------------------------------------------------------------------- // Types for discovery + device flow responses @@ -423,6 +424,7 @@ export const whoamiCommand = defineCommand({ }, }, async run({ args }) { + configureOutputMode(args); const baseUrl = args.url || "http://localhost:4321"; // Resolve token: --token flag > EMDASH_TOKEN env > stored credentials diff --git a/packages/core/src/cli/commands/media.ts b/packages/core/src/cli/commands/media.ts index 1fef564..c8bc1c4 100644 --- a/packages/core/src/cli/commands/media.ts +++ b/packages/core/src/cli/commands/media.ts @@ -11,7 +11,7 @@ import { defineCommand } from "citty"; import { consola } from "consola"; import { connectionArgs, createClientFromArgs } from "../client-factory.js"; -import { output } from "../output.js"; +import { configureOutputMode, output } from "../output.js"; const listCommand = defineCommand({ meta: { @@ -34,6 +34,7 @@ const listCommand = defineCommand({ }, }, async run({ args }) { + configureOutputMode(args); const client = createClientFromArgs(args); try { @@ -73,6 +74,7 @@ const uploadCommand = defineCommand({ }, }, async run({ args }) { + configureOutputMode(args); const client = createClientFromArgs(args); const filename = basename(args.file); @@ -108,6 +110,7 @@ const getCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); const client = createClientFromArgs(args); try { @@ -134,6 +137,7 @@ const deleteCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); const client = createClientFromArgs(args); try { diff --git a/packages/core/src/cli/commands/menu.ts b/packages/core/src/cli/commands/menu.ts index 8c196de..0d51de4 100644 --- a/packages/core/src/cli/commands/menu.ts +++ b/packages/core/src/cli/commands/menu.ts @@ -8,7 +8,7 @@ import { defineCommand } from "citty"; import { consola } from "consola"; import { connectionArgs, createClientFromArgs } from "../client-factory.js"; -import { output } from "../output.js"; +import { configureOutputMode, output } from "../output.js"; const listCommand = defineCommand({ meta: { @@ -19,6 +19,7 @@ const listCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const menus = await client.menus(); @@ -44,6 +45,7 @@ const getCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const menu = await client.menu(args.name); diff --git a/packages/core/src/cli/commands/schema.ts b/packages/core/src/cli/commands/schema.ts index 2b1befd..c7db5e6 100644 --- a/packages/core/src/cli/commands/schema.ts +++ b/packages/core/src/cli/commands/schema.ts @@ -8,7 +8,7 @@ import { defineCommand } from "citty"; import { consola } from "consola"; import { connectionArgs as commonArgs, createClientFromArgs } from "../client-factory.js"; -import { output } from "../output.js"; +import { configureOutputMode, output } from "../output.js"; const listCommand = defineCommand({ meta: { @@ -19,6 +19,7 @@ const listCommand = defineCommand({ ...commonArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const collections = await client.collections(); @@ -44,6 +45,7 @@ const getCommand = defineCommand({ ...commonArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const collection = await client.collection(args.collection); @@ -82,6 +84,7 @@ const createCommand = defineCommand({ ...commonArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const data = await client.createCollection({ @@ -117,6 +120,7 @@ const deleteCommand = defineCommand({ ...commonArgs, }, async run({ args }) { + configureOutputMode(args); try { if (!args.force) { const confirmed = await consola.prompt(`Delete collection "${args.collection}"?`, { @@ -170,6 +174,7 @@ const addFieldCommand = defineCommand({ ...commonArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const data = await client.createField(args.collection, { @@ -206,6 +211,7 @@ const removeFieldCommand = defineCommand({ ...commonArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); await client.deleteField(args.collection, args.field); diff --git a/packages/core/src/cli/commands/search-cmd.ts b/packages/core/src/cli/commands/search-cmd.ts index b8e9a4c..6db274c 100644 --- a/packages/core/src/cli/commands/search-cmd.ts +++ b/packages/core/src/cli/commands/search-cmd.ts @@ -8,7 +8,7 @@ import { defineCommand } from "citty"; import { consola } from "consola"; import { connectionArgs, createClientFromArgs } from "../client-factory.js"; -import { output } from "../output.js"; +import { configureOutputMode, output } from "../output.js"; export const searchCommand = defineCommand({ meta: { @@ -38,6 +38,7 @@ export const searchCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const results = await client.search(args.query, { diff --git a/packages/core/src/cli/commands/taxonomy.ts b/packages/core/src/cli/commands/taxonomy.ts index 0336d12..57bea69 100644 --- a/packages/core/src/cli/commands/taxonomy.ts +++ b/packages/core/src/cli/commands/taxonomy.ts @@ -8,7 +8,7 @@ import { defineCommand } from "citty"; import { consola } from "consola"; import { connectionArgs, createClientFromArgs } from "../client-factory.js"; -import { output } from "../output.js"; +import { configureOutputMode, output } from "../output.js"; /** Pattern to replace whitespace with hyphens for slug generation */ const WHITESPACE_PATTERN = /\s+/g; @@ -22,6 +22,7 @@ const listCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const taxonomies = await client.taxonomies(); @@ -56,6 +57,7 @@ const termsCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const result = await client.terms(args.name, { @@ -97,6 +99,7 @@ const addTermCommand = defineCommand({ ...connectionArgs, }, async run({ args }) { + configureOutputMode(args); try { const client = createClientFromArgs(args); const label = args.name; diff --git a/packages/core/src/cli/output.ts b/packages/core/src/cli/output.ts index f722e0e..37cb23c 100644 --- a/packages/core/src/cli/output.ts +++ b/packages/core/src/cli/output.ts @@ -4,6 +4,20 @@ interface OutputArgs { json?: boolean; } +/** + * Redirect consola output to stderr so it doesn't pollute JSON on stdout. + * + * Call this early in any command that uses `output()` with `--json`. + * Safe to call multiple times — only applies the redirect once. + */ +export function configureOutputMode(args: OutputArgs): void { + if (args.json || !process.stdout.isTTY) { + // Send all consola output to stderr so stdout is clean JSON + consola.options.stdout = process.stderr; + consola.options.stderr = process.stderr; + } +} + /** * Output data as JSON or pretty-printed. * diff --git a/packages/core/tests/integration/client/comments.test.ts b/packages/core/tests/integration/client/comments.test.ts index 627a8e5..2632239 100644 --- a/packages/core/tests/integration/client/comments.test.ts +++ b/packages/core/tests/integration/client/comments.test.ts @@ -14,7 +14,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import type { TestServerContext } from "../server.js"; import { assertNodeVersion, createTestServer } from "../server.js"; -const PORT = 4398; +const PORT = 4396; const TIMEOUT = 60_000; /** Helper: raw fetch with auth headers */ diff --git a/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts b/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts index faadf78..2f2bf93 100644 --- a/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts +++ b/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts @@ -1,5 +1,6 @@ import { execFile, spawn } from "node:child_process"; -import { resolve } from "node:path"; +import { rmSync } from "node:fs"; +import { join, resolve } from "node:path"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; @@ -203,6 +204,12 @@ describe.sequential("Site smoke matrix", () => { async () => { await ensureBuilt(); + // Remove stale database files so each run starts fresh. + // SQLite demos use data.db; WAL/SHM sidecars may also exist. + for (const file of ["data.db", "data.db-wal", "data.db-shm"]) { + rmSync(join(site.dir, file), { force: true }); + } + const baseUrl = `http://localhost:${site.port}`; const serverProcess = spawn("pnpm", ["exec", "astro", "dev", "--port", String(site.port)], { cwd: site.dir,