From c74d109865d499a6bd696fb17e0ff375775c84dc Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 26 Mar 2026 18:15:16 +1100 Subject: [PATCH 1/3] Update CLI format --- packages/cli/package.json | 2 +- packages/cli/src/commands/posts.ts | 14 +--- packages/cli/src/formatter.ts | 104 +++++++++++++++++++++++++++++ packages/cli/src/index.ts | 2 +- 4 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 packages/cli/src/formatter.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index d96bcab..abb7ea5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@changespage/cli", - "version": "0.1.0", + "version": "0.2.0", "type": "module", "bin": { "chp": "./dist/index.js" diff --git a/packages/cli/src/commands/posts.ts b/packages/cli/src/commands/posts.ts index 695ae12..d730053 100644 --- a/packages/cli/src/commands/posts.ts +++ b/packages/cli/src/commands/posts.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; import { ApiClient } from "../client.js"; import { getSecretKey } from "../config.js"; +import { output } from "../formatter.js"; function readStdin(): Promise { if (process.stdin.isTTY) { @@ -31,15 +32,6 @@ function getClient(cmd: Command): ApiClient { }); } -function output(data: unknown, cmd: Command) { - const opts = cmd.optsWithGlobals(); - if (opts.pretty) { - console.log(JSON.stringify(data, null, 2)); - } else { - console.log(JSON.stringify(data)); - } -} - function parseTags(tags: string): string[] { return tags.split(",").map((t) => t.trim()); } @@ -51,7 +43,7 @@ export function registerPostsCommand(program: Command) { .command("list") .description("List posts") .option("--status ", "Filter by status (draft|published|archived)") - .option("--limit ", "Max number of posts", "20") + .option("--limit ", "Max number of posts", "5") .option("--offset ", "Offset for pagination", "0") .action(async function (this: Command) { const client = getClient(this); @@ -145,6 +137,6 @@ export function registerPostsCommand(program: Command) { .action(async function (this: Command, id: string) { const client = getClient(this); await client.deletePost(id); - console.log(JSON.stringify({ deleted: true, id })); + output({ deleted: true, id }, this); }); } diff --git a/packages/cli/src/formatter.ts b/packages/cli/src/formatter.ts new file mode 100644 index 0000000..cccc538 --- /dev/null +++ b/packages/cli/src/formatter.ts @@ -0,0 +1,104 @@ +import { Command } from "commander"; + +const isTTY = process.stdout.isTTY ?? false; +const BOLD = isTTY ? "\x1b[1m" : ""; +const DIM = isTTY ? "\x1b[2m" : ""; +const CYAN = isTTY ? "\x1b[36m" : ""; +const RESET = isTTY ? "\x1b[0m" : ""; + +const DATE_KEYS = new Set(["created_at", "updated_at", "publish_at", "publication_date"]); + +function formatValue(value: unknown, key?: string): string { + if (value === null || value === undefined) return ""; + if (Array.isArray(value)) return value.join(", "); + if (typeof value === "object") return JSON.stringify(value); + if (key && DATE_KEYS.has(key) && typeof value === "string") { + const d = new Date(value); + if (!isNaN(d.getTime())) return d.toLocaleString(); + } + return String(value); +} + +function wrapText(str: string, width: number, indent: number): string { + if (width < 10) width = 10; + if (str.length <= width) return str; + + const lines: string[] = []; + const pad = " ".repeat(indent); + let remaining = str; + + while (remaining.length > 0) { + const max = lines.length === 0 ? width : width; + if (remaining.length <= max) { + lines.push(remaining); + break; + } + let breakAt = remaining.lastIndexOf(" ", max); + if (breakAt <= 0) breakAt = max; + lines.push(remaining.slice(0, breakAt)); + remaining = remaining.slice(breakAt).trimStart(); + } + + return lines.join("\n" + pad); +} + +function formatList(rows: Record[]) { + const termWidth = process.stdout.columns ?? 80; + + for (let i = 0; i < rows.length; i++) { + const entries = Object.entries(rows[i]).filter(([, v]) => v !== null && v !== undefined && v !== ""); + const maxKeyLen = entries.reduce((max, [k]) => Math.max(max, k.length), 0); + const valueWidth = termWidth - maxKeyLen - 3; + + const indent = maxKeyLen + 3; + for (const [key, value] of entries) { + const label = `${BOLD}${CYAN}${key.padEnd(maxKeyLen)}${RESET}`; + const formatted = wrapText(formatValue(value, key), Math.max(valueWidth, 20), indent); + console.log(`${label}${DIM} : ${RESET}${formatted}`); + } + + if (i < rows.length - 1) { + console.log(`${DIM}${"─".repeat(Math.min(termWidth, 60))}${RESET}`); + } + } +} + +function formatKeyValue(obj: Record) { + const termWidth = process.stdout.columns ?? 80; + const entries = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined && v !== ""); + const maxKeyLen = entries.reduce((max, [k]) => Math.max(max, k.length), 0); + const valueWidth = termWidth - maxKeyLen - 3; + const indent = maxKeyLen + 3; + + for (const [key, value] of entries) { + const label = `${BOLD}${CYAN}${key.padEnd(maxKeyLen)}${RESET}`; + const formatted = wrapText(formatValue(value, key), Math.max(valueWidth, 20), indent); + console.log(`${label}${DIM} : ${RESET}${formatted}`); + } +} + +export function output(data: unknown, cmd: Command) { + const opts = cmd.optsWithGlobals(); + if (opts.json) { + console.log(JSON.stringify(data)); + return; + } + + if (data === null || data === undefined) return; + + if (Array.isArray(data)) { + if (data.length === 0) { + console.log(DIM + "(no results)" + RESET); + return; + } + formatList(data); + return; + } + + if (typeof data === "object") { + formatKeyValue(data as Record); + return; + } + + console.log(String(data)); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4b8f48a..24e76d3 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -9,7 +9,7 @@ program .description("CLI for changes.page") .version("0.1.0") .option("--secret-key ", "Page secret key") - .option("--pretty", "Pretty-print JSON output"); + .option("--json", "Output raw JSON"); registerConfigureCommand(program); registerPostsCommand(program); From 09c660c503c08a0b551614c7b29bbfebd379c13b Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 26 Mar 2026 18:19:15 +1100 Subject: [PATCH 2/3] Update landing page --- .../components/layout/footer.component.tsx | 2 +- .../components/layout/header.component.tsx | 16 +- apps/web/components/marketing/cli-demo.tsx | 160 +++++++++ .../components/marketing/dual-interface.tsx | 92 ++++++ apps/web/components/marketing/faq.tsx | 65 ++-- apps/web/components/marketing/features.tsx | 261 ++++++++------- .../components/marketing/get-started-hero.tsx | 57 +--- apps/web/components/marketing/hero.tsx | 303 ++++++++++-------- .../web/components/marketing/how-it-works.tsx | 67 ++++ .../marketing/open-source-banner.tsx | 51 +++ .../components/marketing/pricing-section.tsx | 172 +++++----- apps/web/data/marketing.data.ts | 7 +- apps/web/pages/_document.js | 13 +- apps/web/pages/index.tsx | 51 +-- apps/web/pages/pricing.tsx | 6 +- apps/web/styles/global.css | 10 +- apps/web/tailwind.config.js | 7 +- 17 files changed, 846 insertions(+), 494 deletions(-) create mode 100644 apps/web/components/marketing/cli-demo.tsx create mode 100644 apps/web/components/marketing/dual-interface.tsx create mode 100644 apps/web/components/marketing/how-it-works.tsx create mode 100644 apps/web/components/marketing/open-source-banner.tsx diff --git a/apps/web/components/layout/footer.component.tsx b/apps/web/components/layout/footer.component.tsx index 23cab8c..f5cb496 100644 --- a/apps/web/components/layout/footer.component.tsx +++ b/apps/web/components/layout/footer.component.tsx @@ -86,7 +86,7 @@ export default function FooterComponent() { />