From 48f4ddb0c7565e6d2103faa38470b572f1dac548 Mon Sep 17 00:00:00 2001 From: Javier Leandro Arancibia Date: Sat, 30 May 2026 10:05:15 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20harden=20CLI=20mechanics=20=E2=80=94=20v?= =?UTF-8?q?ersion=20flag,=20stderr=20errors,=20dotbot/gum=20plugin=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix #4: dotbot plugin.json — plugin field in install_guidance is now an array instead of a string, matching multi-value field convention - fix #5: gum write — corrected description from 'write to file' to 'interactive TUI multi-line text input' in both plugin.json and SKILL.md - feat: add --version flag to Node.js CLI — early exit with version info in both human and JSON modes, aligning with Zig CLI behavior - fix: error output — JSON error envelopes now go to stderr (not stdout), matching the human mode behavior - test: add 4 tests covering --version (human, json, compact, exit code) - test: add 3 tests covering error output routing (stderr vs stdout) and non-zero exit codes on invalid commands --- __tests__/supercli-args.test.js | 67 +++++++++++++++++++++++ __tests__/supercli-version.test.js | 74 ++++++++++++++++++++++++++ cli/supercli.js | 25 ++++++++- plugins/dotbot/plugin.json | 2 +- plugins/gum/plugin.json | 4 +- plugins/gum/skills/quickstart/SKILL.md | 2 +- 6 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 __tests__/supercli-version.test.js diff --git a/__tests__/supercli-args.test.js b/__tests__/supercli-args.test.js index c9b813f4..591a5584 100644 --- a/__tests__/supercli-args.test.js +++ b/__tests__/supercli-args.test.js @@ -61,4 +61,71 @@ describe("supercli args parser", () => { expect(plan.args.foo).toBe("bar") expect(plan.args.baz).toBe("") }) + + test("bare -- does not create empty flag key", () => { + const out = execSync(`node ${CLI} plan testns testres testact -- --foo bar --json`, { + env, + encoding: "utf-8" + }) + const plan = JSON.parse(out) + // bare -- should be ignored as a flag; --foo should still parse + expect(plan.args.foo).toBe("bar") + expect(Object.keys(plan.args)).not.toContain("") + }) + + test("--flag at end of args defaults to true", () => { + const out = execSync(`node ${CLI} plan testns testres testact --flag --json`, { + env, + encoding: "utf-8" + }) + const plan = JSON.parse(out) + expect(plan.args.flag).toBe(true) + }) + + test("--flag --other does not consume --other as value", () => { + const out = execSync(`node ${CLI} plan testns testres testact --flag --other value --json`, { + env, + encoding: "utf-8" + }) + const plan = JSON.parse(out) + expect(plan.args.flag).toBe(true) + expect(plan.args.other).toBe("value") + }) + + test("--flag= with empty string value", () => { + const out = execSync(`node ${CLI} plan testns testres testact --empty= --json`, { + env, + encoding: "utf-8" + }) + const plan = JSON.parse(out) + expect(plan.args.empty).toBe("") + }) +}) + const plan = JSON.parse(out) + expect(plan.args.foo).toBe("bar") + expect(plan.args.baz).toBe("") + }) + + test("error for unknown namespace goes to stderr", () => { + const cmd = `node ${CLI} nonexistent --json` + expect(() => execSync(cmd, { env, encoding: "utf-8" })).toThrow() + }) + + test("inspect with missing args errors on stderr not stdout", () => { + try { + execSync(`node ${CLI} inspect --json`, { env, stdio: ["pipe", "pipe", "pipe"] }) + } catch (e) { + expect(e.stderr.toString()).toContain("Usage: supercli inspect") + expect(e.stdout.toString()).toBe("") + } + }) + + test("unknown namespace exits with non-zero code", () => { + try { + execSync(`node ${CLI} nonexistent --json`, { env, stdio: ["pipe", "pipe", "pipe"] }) + } catch (e) { + expect(e.status).toBeGreaterThan(0) + expect(e.stderr.toString()).toContain("not found") + } + }) }) diff --git a/__tests__/supercli-version.test.js b/__tests__/supercli-version.test.js new file mode 100644 index 00000000..d4d740b7 --- /dev/null +++ b/__tests__/supercli-version.test.js @@ -0,0 +1,74 @@ +const { execSync } = require("child_process") +const path = require("path") + +const CLI = path.join(__dirname, "..", "cli", "supercli.js") + +describe("supercli --version", () => { + test("--version --human returns version text", () => { + const out = execSync(`node ${CLI} --version --human`, { + encoding: "utf-8" + }) + expect(out).toContain("SuperCLI v") + expect(out).toContain("JavaScript") + }) + + test("--version --json returns version envelope", () => { + const out = execSync(`node ${CLI} --version --json`, { + encoding: "utf-8" + }) + const data = JSON.parse(out) + expect(data.name).toBe("SuperCLI") + expect(data.implementation).toBe("JavaScript") + expect(data.version).toBeDefined() + expect(data.node_version).toBeDefined() + }) + + test("--version alone produces exit code 0 with JSON (non-TTY)", () => { + const out = execSync(`node ${CLI} --version`, { + encoding: "utf-8" + }) + const data = JSON.parse(out) + expect(data.name).toBe("SuperCLI") + expect(data.version).toBe("1.31.1") + }) + + test("--version --compact returns compact version envelope", () => { + const out = execSync(`node ${CLI} --version --compact`, { + encoding: "utf-8" + }) + const data = JSON.parse(out) + expect(data.n).toBe("SuperCLI") + expect(data.v).toBe("1.31.1") + }) +}) + expect(out).toContain("SuperCLI v") + expect(out).toContain("JavaScript") + }) + + test("--version --json returns version envelope", () => { + const out = execSync(`node ${CLI} --version --json`, { + encoding: "utf-8" + }) + const data = JSON.parse(out) + expect(data.name).toBe("SuperCLI") + expect(data.implementation).toBe("JavaScript") + expect(data.version).toBeDefined() + expect(data.node_version).toBeDefined() + }) + + test("--version alone produces exit code 0", () => { + const out = execSync(`node ${CLI} --version`, { + encoding: "utf-8" + }) + expect(out).toBeTruthy() + }) + + test("--version --compact returns compact version envelope", () => { + const out = execSync(`node ${CLI} --version --compact`, { + encoding: "utf-8" + }) + const data = JSON.parse(out) + expect(data.name).toBe("SuperCLI") + expect(data.v).toBeDefined() + }) +}) diff --git a/cli/supercli.js b/cli/supercli.js index 8ecde25f..a8afdbc6 100755 --- a/cli/supercli.js +++ b/cli/supercli.js @@ -39,6 +39,7 @@ for (let i = 0; i < rawArgs.length; i++) { const arg = rawArgs[i]; if (arg.startsWith("--")) { const kv = arg.slice(2); + if (kv === "") continue; // bare --, skip (prevents flags[""] = ...) const eqIdx = kv.indexOf("="); if (eqIdx !== -1) { const key = kv.slice(0, eqIdx); @@ -71,12 +72,15 @@ const RESERVED_FLAGS = [ "schema", "help-json", "help", + "version", "no-color", "show-dag", "format", "on-conflict", ]; +const CLI_VERSION = "1.31.1"; + function compactKeys(obj) { if (Array.isArray(obj)) return obj.map(compactKeys); if (obj && typeof obj === "object") { @@ -94,6 +98,7 @@ function compactKeys(obj) { error: "err", message: "msg", suggestions: "sug", + name: "n", }; const out = {}; for (const [k, v] of Object.entries(obj)) { @@ -176,7 +181,7 @@ function outputError(error) { ); } } else { - console.log(JSON.stringify(compactMode ? compactKeys(envelope) : envelope)); + process.stderr.write(JSON.stringify(compactMode ? compactKeys(envelope) : envelope) + "\n"); } process.exit(envelope.error.code); } @@ -325,6 +330,24 @@ async function main() { } } + // Early exit for --version (align with Zig CLI behavior) + if (flags.version) { + if (humanMode) { + console.log(`SuperCLI v${CLI_VERSION}`); + console.log("Implementation: JavaScript (Node.js)"); + console.log("Binary: supercli"); + } else { + output({ + name: "SuperCLI", + implementation: "JavaScript", + version: CLI_VERSION, + node_version: process.version, + binary_name: "supercli", + }); + } + return; + } + // Check for namespace passthrough before handling --help // This allows commands like "cline --help --json" to pass through { diff --git a/plugins/dotbot/plugin.json b/plugins/dotbot/plugin.json index 4704779a..e151e379 100644 --- a/plugins/dotbot/plugin.json +++ b/plugins/dotbot/plugin.json @@ -10,7 +10,7 @@ } ], "install_guidance": { - "plugin": "dotbot", + "plugin": ["dotbot"], "binary": "dotbot", "check": "which dotbot", "install_steps": [ diff --git a/plugins/gum/plugin.json b/plugins/gum/plugin.json index afdfc4ab..a9feef54 100644 --- a/plugins/gum/plugin.json +++ b/plugins/gum/plugin.json @@ -10,7 +10,7 @@ } ], "install_guidance": { - "plugin": "gum", + "plugin": ["gum"], "binary": "gum", "check": "which gum", "install_steps": [ @@ -247,7 +247,7 @@ "namespace": "gum", "resource": "write", "action": "text", - "description": "Write text from stdin to a file", + "description": "Interactive TUI multi-line text input with syntax highlighting", "adapter": "process", "adapterConfig": { "command": "gum", diff --git a/plugins/gum/skills/quickstart/SKILL.md b/plugins/gum/skills/quickstart/SKILL.md index 260a1ced..07801cd5 100644 --- a/plugins/gum/skills/quickstart/SKILL.md +++ b/plugins/gum/skills/quickstart/SKILL.md @@ -14,7 +14,7 @@ A tool for glamorous shell scripts — style, format, and interact like a charm. - `gum format render` — Format text using markdown, template, code, or emoji formatters - `gum table render` — Render a table from CSV/TSV or stdin data - `gum join text` — Join text vertically or horizontally -- `gum write text` — Write text from stdin to a file with a text area +- `gum write text` — Interactive TUI multi-line text input with syntax highlighting - `gum file pick` — Pick a file from the filesystem ## Usage Examples