From adac712cfd6624003456efe8e27cf8d589154e55 Mon Sep 17 00:00:00 2001 From: Hartmut Obendorf Date: Fri, 12 Jun 2026 00:17:20 +0200 Subject: [PATCH 1/2] added npm installer --- .cli-skill-install-state.json | 97 ++++++++ README.md | 40 +-- package-lock.json | 32 +++ package.json | 36 +++ scripts/cli.js | 451 ++++++++++++++++++++++++++++++++++ 5 files changed, 628 insertions(+), 28 deletions(-) create mode 100644 .cli-skill-install-state.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/cli.js diff --git a/.cli-skill-install-state.json b/.cli-skill-install-state.json new file mode 100644 index 0000000..43ae0e4 --- /dev/null +++ b/.cli-skill-install-state.json @@ -0,0 +1,97 @@ +{ + "version": 1, + "files": { + "cli-skill/SKILL.md": { + "sourceHash": "571493ef1dd510d7b2c5efa388ab81805ca1bfb839b278554ac0e2d6028b11c8", + "installedHash": "571493ef1dd510d7b2c5efa388ab81805ca1bfb839b278554ac0e2d6028b11c8" + }, + "cli-skill/adapters/README.md": { + "sourceHash": "9a52f0a729dd8c2f054f4e462f1d4de9a0666d123a6161b82d7f87776f2c7dd2", + "installedHash": "9a52f0a729dd8c2f054f4e462f1d4de9a0666d123a6161b82d7f87776f2c7dd2" + }, + "cli-skill/adapters/claude-code/commands.yaml": { + "sourceHash": "10d1554e5d74376db1080eb82b1f6d4f296b268908f1684001e3d323051bd9f3", + "installedHash": "10d1554e5d74376db1080eb82b1f6d4f296b268908f1684001e3d323051bd9f3" + }, + "cli-skill/adapters/copilot/SKILL.md": { + "sourceHash": "30314e5ce0f67887a47a7f36fd51bb52defcd1bcb45aadd6e9c4479ea5f402ac", + "installedHash": "30314e5ce0f67887a47a7f36fd51bb52defcd1bcb45aadd6e9c4479ea5f402ac" + }, + "cli-skill/adapters/opencode/commands.json": { + "sourceHash": "d86ba471e92338ddaddd4c69558c78e6ad3594754bed8bfb2948aec88462c207", + "installedHash": "d86ba471e92338ddaddd4c69558c78e6ad3594754bed8bfb2948aec88462c207" + }, + "cli-skill/adapters/pi-coding-agent/SKILL.md": { + "sourceHash": "770f9f095f7fd00913ab21326fc71f6a69b2363be0f5ea5db9fbf46fd0507628", + "installedHash": "770f9f095f7fd00913ab21326fc71f6a69b2363be0f5ea5db9fbf46fd0507628" + }, + "cli-skill/commands/cli-behavioral-analysis.md": { + "sourceHash": "9c6545d9fd1cf9e8e42d797a9906f18e2895cc0b934b12c94da64643ff83dcc1", + "installedHash": "9c6545d9fd1cf9e8e42d797a9906f18e2895cc0b934b12c94da64643ff83dcc1" + }, + "cli-skill/commands/cli-check-help.md": { + "sourceHash": "16e36e23668ecdf0e12600092218574199ffbccb80179d4bdb05dcecaa5c9b4d", + "installedHash": "16e36e23668ecdf0e12600092218574199ffbccb80179d4bdb05dcecaa5c9b4d" + }, + "cli-skill/commands/cli-heuristic-analysis.md": { + "sourceHash": "9df2fb0997e5a6160b0c93920cf6ab8868469f84d6f5a701d9036cc9f191c748", + "installedHash": "9df2fb0997e5a6160b0c93920cf6ab8868469f84d6f5a701d9036cc9f191c748" + }, + "cli-skill/commands/cli-review.md": { + "sourceHash": "4ba55617d0bb026fc0d4d74b5bd12ba93a591477026e13da48223a2d44fdf886", + "installedHash": "4ba55617d0bb026fc0d4d74b5bd12ba93a591477026e13da48223a2d44fdf886" + }, + "cli-skill/commands/cli-semantic-analysis.md": { + "sourceHash": "d5ff5569ee41b777686c23fb3ae294b26f6583a12b6b1efbc8a6ae931eee52a6", + "installedHash": "d5ff5569ee41b777686c23fb3ae294b26f6583a12b6b1efbc8a6ae931eee52a6" + }, + "cli-skill/commands/install-cli-pr-workflow.md": { + "sourceHash": "49b1c6c837a158f9690257cc6dbdbfe65382f628cf6f936d0ceb8d9a7d5c2ebf", + "installedHash": "49b1c6c837a158f9690257cc6dbdbfe65382f628cf6f936d0ceb8d9a7d5c2ebf" + }, + "cli-skill/future-commands/cli-propose-command.md": { + "sourceHash": "2cda4adabb1ad05d9113c357c202248636d6223f32c830a0ec275728bca5e461", + "installedHash": "2cda4adabb1ad05d9113c357c202248636d6223f32c830a0ec275728bca5e461" + }, + "cli-skill/future-commands/cli-rename-command.md": { + "sourceHash": "653b62a22fceb60fd8ca090fcf713d4d93eb554650817ec132c9e01081a8b9b1", + "installedHash": "653b62a22fceb60fd8ca090fcf713d4d93eb554650817ec132c9e01081a8b9b1" + }, + "cli-skill/references/cli-help-examples.md": { + "sourceHash": "e4080251c73bd30734de33c1f31fb69d5be0569fb63043e32fd75aeeae93ef7d", + "installedHash": "e4080251c73bd30734de33c1f31fb69d5be0569fb63043e32fd75aeeae93ef7d" + }, + "cli-skill/references/cli-help.md": { + "sourceHash": "50765456253ceb63c3cd80bdcd63bd29c02644c3190cb149defc1892e5c24ad5", + "installedHash": "50765456253ceb63c3cd80bdcd63bd29c02644c3190cb149defc1892e5c24ad5" + }, + "cli-skill/references/cli-review-output-format.md": { + "sourceHash": "5ca3b52870c04341d6b9240c2f28edd1753ab33d8eba87ef247cb0b1492b0d10", + "installedHash": "5ca3b52870c04341d6b9240c2f28edd1753ab33d8eba87ef247cb0b1492b0d10" + }, + "cli-skill/references/cli-skill-review-pr.yml": { + "sourceHash": "f9bf075bdae3239fce0da1a558d766932a0d0a206573ab2283c291787d07f0ac", + "installedHash": "f9bf075bdae3239fce0da1a558d766932a0d0a206573ab2283c291787d07f0ac" + }, + "cli-skill/references/cli-standard.md": { + "sourceHash": "10f617f80610490d8c4a9795fbfbf5f1d4e7aac74f8da79d6db1c994a3c926d4", + "installedHash": "10f617f80610490d8c4a9795fbfbf5f1d4e7aac74f8da79d6db1c994a3c926d4" + }, + "cli-skill/references/deprecation.md": { + "sourceHash": "cb47534e93c4daf78a621ba9754f214e82f2e8975d895b3f0029727417a13a4e", + "installedHash": "cb47534e93c4daf78a621ba9754f214e82f2e8975d895b3f0029727417a13a4e" + }, + "cli-skill/schemas/commands.manifest.yaml": { + "sourceHash": "957b98b156ce1187ea932293b4a636c5d4d891bf3cafe5517780c66a21dbc803", + "installedHash": "957b98b156ce1187ea932293b4a636c5d4d891bf3cafe5517780c66a21dbc803" + }, + "cli-skill/shared/cli-discovery-preflight.md": { + "sourceHash": "cb50539228b6d99cd859d81efcc75bb2823c3438c865b75479f65671757e7ee3", + "installedHash": "cb50539228b6d99cd859d81efcc75bb2823c3438c865b75479f65671757e7ee3" + }, + ".pi/skills/cli-skill/SKILL.md": { + "sourceHash": "dce5fc40f9861cc42b4cfdeaf53d9aea0219b851b9b3dd7d4ec65df4c9161b64", + "installedHash": "dce5fc40f9861cc42b4cfdeaf53d9aea0219b851b9b3dd7d4ec65df4c9161b64" + } + } +} diff --git a/README.md b/README.md index c7e1406..b37f479 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,18 @@ cli-skill is a command based, cross agent CLI review framework. It uses one canonical command set and thin adapters so the same workflows can run in Copilot, Claude Code, Pi Coding Agent, and OpenCode. +## Install via npm + +This repository is also packaged as `canonical-cli-skill` for consumers that want to install the skill files into their own repo. + +```bash +npx canonical-cli-skill install +``` + +By default, the installer resolves the target directory to the top-level of the current Git repository, so generated paths are anchored at the repo root even when you run it from a subdirectory. If there is no Git repository, it falls back to the current directory. Use `--target ` to override this behavior. + +Auto-detection is based on repository markers for the supported agents. Existing unmanaged files are not overwritten unless `--force` is used, and managed files are only updated when they have not been edited since the last install. + ## Project Structure - Canonical skill index: [cli-skill/SKILL.md](cli-skill/SKILL.md) @@ -22,34 +34,6 @@ Future scaffolds: - [cli-skill/future-commands/cli-propose-command.md](cli-skill/future-commands/cli-propose-command.md) - [cli-skill/future-commands/cli-rename-command.md](cli-skill/future-commands/cli-rename-command.md) -## Use with Different Agents - -1. Sync adapters from the manifest (single maintained helper script): - - node scripts/sync-cli-skill-adapters.js - -2. Copilot (GitHub skills path): - -- Entry point: [.github/skills/cli-skill/SKILL.md](.github/skills/cli-skill/SKILL.md) -- Adapter file: [cli-skill/adapters/copilot/SKILL.md](cli-skill/adapters/copilot/SKILL.md) - -3. Pi Coding Agent: - -- Entry point: [.pi/skills/cli-skill/SKILL.md](.pi/skills/cli-skill/SKILL.md) -- Adapter file: [cli-skill/adapters/pi-coding-agent/SKILL.md](cli-skill/adapters/pi-coding-agent/SKILL.md) - -4. Claude Code: - -- Adapter file: [cli-skill/adapters/claude-code/commands.yaml](cli-skill/adapters/claude-code/commands.yaml) - -5. OpenCode: - -- Adapter file: [cli-skill/adapters/opencode/commands.json](cli-skill/adapters/opencode/commands.json) - -6. Adapter docs: - -- [cli-skill/adapters/README.md](cli-skill/adapters/README.md) - ## Quick Start for GitHub Action: cli-skill-build Use the packaged workflow at [.github/workflows/cli-skill-build.yml](.github/workflows/cli-skill-build.yml). diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..df59c06 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,32 @@ +{ + "name": "canonical-cli-skill", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "canonical-cli-skill", + "version": "0.1.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "15.0.0" + }, + "bin": { + "canonical-cli-skill": "scripts/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..62666b7 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "canonical-cli-skill", + "version": "0.1.0", + "description": "Cross-agent CLI review skill installer for Copilot, Pi, Claude Code, and OpenCode.", + "license": "MIT", + "type": "commonjs", + "bin": { + "canonical-cli-skill": "scripts/cli.js" + }, + "scripts": { + "postinstall": "node scripts/cli.js install --postinstall", + "install:skill": "node scripts/cli.js install", + "install:skill:dry-run": "node scripts/cli.js install --dry-run" + }, + "files": [ + "scripts/cli.js", + "cli-skill", + ".github/skills/cli-skill/SKILL.md", + ".pi/skills/cli-skill/SKILL.md", + "README.md" + ], + "keywords": [ + "cli", + "skill", + "copilot", + "pi", + "claude", + "opencode" + ], + "engines": { + "node": ">=18" + }, + "dependencies": { + "commander": "15.0.0" + } +} diff --git a/scripts/cli.js b/scripts/cli.js new file mode 100644 index 0000000..ef98754 --- /dev/null +++ b/scripts/cli.js @@ -0,0 +1,451 @@ +#!/usr/bin/env node + +const { Command } = require("commander"); +const fs = require("node:fs"); +const path = require("node:path"); +const crypto = require("node:crypto"); +const { execSync } = require("node:child_process"); + +const SUPPORTED_AGENTS = ["copilot", "pi", "claude", "opencode"]; +const STATE_FILE = ".cli-skill-install-state.json"; +const HELP_APPENDIX = ` + +Auto-detection: + When --agents is omitted, installer detects agents from repository markers. + If nothing is detected, only cli-skill core is installed. + +Managed updates: + Existing unmanaged files are never overwritten unless --force is set. + Existing managed files are auto-updated only when unchanged since last install. +`; + +function writeOut(str) { + process.stdout.write(str); +} + +function writeErr(str) { + process.stderr.write(str); +} + +const AGENT_MARKERS = { + copilot: { + paths: [ + ".github/copilot-instructions.md", + ".github/prompts", + ], + contentHints: [/copilot/i, /github\s+copilot/i], + }, + pi: { + paths: [ + ".pi", + ".pi/skills", + ], + contentHints: [/pi\s+coding\s+agent/i], + }, + claude: { + paths: [ + "CLAUDE.md", + ".claude", + ".claude/commands", + ], + contentHints: [/claude\s+code/i, /anthropic\s+claude/i], + }, + opencode: { + paths: [ + "opencode.json", + ".opencode", + ".opencode/commands", + ], + contentHints: [/\bopencode\b/i], + }, +}; + +function normalizeBool(value) { + if (!value) return false; + const lowered = String(value).toLowerCase(); + return lowered === "1" || lowered === "true" || lowered === "yes"; +} + +function parseAgents(raw) { + if (!raw) return []; + return String(raw) + .split(",") + .map((part) => part.trim().toLowerCase()) + .filter(Boolean) + .filter((agent) => SUPPORTED_AGENTS.includes(agent)); +} + +function fileContainsAnyPattern(filePath, patterns) { + if (!fs.existsSync(filePath)) return false; + let text = ""; + try { + text = fs.readFileSync(filePath, "utf-8"); + } catch { + return false; + } + + return patterns.some((pattern) => pattern.test(text)); +} + +function detectAgents(targetDir) { + const detected = []; + const reasons = {}; + const hintFiles = ["AGENTS.md"]; + + for (const agent of SUPPORTED_AGENTS) { + const marker = AGENT_MARKERS[agent]; + reasons[agent] = []; + + for (const relPath of marker.paths) { + if (fs.existsSync(path.join(targetDir, relPath))) { + reasons[agent].push(`marker:${relPath}`); + } + } + + for (const relPath of hintFiles) { + const absPath = path.join(targetDir, relPath); + if (fileContainsAnyPattern(absPath, marker.contentHints)) { + reasons[agent].push(`hint:${relPath}`); + } + } + + if (reasons[agent].length > 0) { + detected.push(agent); + } + } + + return { + agents: detected, + reasons, + }; +} + +function hashBuffer(buffer) { + return crypto.createHash("sha256").update(buffer).digest("hex"); +} + +function hashFile(filePath) { + return hashBuffer(fs.readFileSync(filePath)); +} + +function readState(targetDir) { + const statePath = path.join(targetDir, STATE_FILE); + + if (!fs.existsSync(statePath)) { + return { path: statePath, data: { version: 1, files: {} } }; + } + + try { + const parsed = JSON.parse(fs.readFileSync(statePath, "utf-8")); + if (!parsed || typeof parsed !== "object" || typeof parsed.files !== "object") { + return { path: statePath, data: { version: 1, files: {} } }; + } + return { path: statePath, data: parsed }; + } catch { + return { path: statePath, data: { version: 1, files: {} } }; + } +} + +function writeState(statePath, stateData, options, logs) { + if (options.dryRun) { + logs.push(`skip state write (dry-run): ${path.relative(options.targetDir, statePath)}`); + return; + } + + fs.mkdirSync(path.dirname(statePath), { recursive: true }); + fs.writeFileSync(statePath, `${JSON.stringify(stateData, null, 2)}\n`, "utf-8"); + logs.push(`wrote state: ${path.relative(options.targetDir, statePath)}`); +} + +function listFilesRecursive(rootDir) { + const out = []; + + function walk(currentDir) { + const entries = fs.readdirSync(currentDir, { withFileTypes: true }); + for (const entry of entries) { + const absPath = path.join(currentDir, entry.name); + if (entry.isDirectory()) { + walk(absPath); + } else if (entry.isFile()) { + out.push(absPath); + } + } + } + + walk(rootDir); + return out; +} + +function shouldOverwriteManaged(dstFile, srcHash, options, stateData) { + if (!fs.existsSync(dstFile)) { + return { overwrite: true, reason: "create" }; + } + + if (options.force) { + return { overwrite: true, reason: "force" }; + } + + const rel = path.relative(options.targetDir, dstFile); + const currentHash = hashFile(dstFile); + const tracked = stateData.files[rel]; + + if (currentHash === srcHash) { + return { overwrite: false, reason: "already-current" }; + } + + if (!tracked) { + return { overwrite: false, reason: "unmanaged-existing" }; + } + + if (tracked.installedHash === currentHash) { + return { overwrite: true, reason: "managed-update" }; + } + + return { overwrite: false, reason: "managed-modified" }; +} + +function copyManagedFile(srcFile, dstFile, options, logs, stateData) { + const srcHash = hashFile(srcFile); + const decision = shouldOverwriteManaged(dstFile, srcHash, options, stateData); + + if (!decision.overwrite) { + const rel = path.relative(options.targetDir, dstFile); + if (decision.reason === "already-current") { + logs.push(`skip file (up-to-date): ${rel}`); + stateData.files[rel] = { + sourceHash: srcHash, + installedHash: srcHash, + }; + return; + } + + if (decision.reason === "managed-modified") { + logs.push(`skip file (managed but modified): ${rel}`); + return; + } + + logs.push(`skip file (exists unmanaged): ${rel}`); + return; + } + + if (options.dryRun) { + logs.push(`${decision.reason}: ${path.relative(options.targetDir, srcFile)} -> ${path.relative(options.targetDir, dstFile)}`); + const rel = path.relative(options.targetDir, dstFile); + stateData.files[rel] = { + sourceHash: srcHash, + installedHash: srcHash, + }; + return; + } + + fs.mkdirSync(path.dirname(dstFile), { recursive: true }); + fs.copyFileSync(srcFile, dstFile); + logs.push(`${decision.reason}: ${path.relative(options.targetDir, dstFile)}`); + const rel = path.relative(options.targetDir, dstFile); + stateData.files[rel] = { + sourceHash: srcHash, + installedHash: srcHash, + }; +} + +function copyManagedTree(srcRootDir, dstRootDir, options, logs, stateData) { + const files = listFilesRecursive(srcRootDir); + for (const srcFile of files) { + const relFromSrc = path.relative(srcRootDir, srcFile); + const dstFile = path.join(dstRootDir, relFromSrc); + copyManagedFile(srcFile, dstFile, options, logs, stateData); + } +} + +function findGitTopLevel(startDir) { + try { + const result = execSync("git rev-parse --show-toplevel", { + cwd: startDir, + encoding: "utf-8", + stdio: ["ignore", "pipe", "ignore"], + }).trim(); + + if (!result) return null; + return path.resolve(result); + } catch { + return null; + } +} + +function resolveTargetDir(targetDirArg) { + if (targetDirArg) { + return { + targetDir: path.resolve(targetDirArg), + targetSource: "--target", + }; + } + + const startDir = path.resolve(process.env.INIT_CWD || process.cwd()); + const gitTopLevel = findGitTopLevel(startDir); + if (gitTopLevel) { + return { + targetDir: gitTopLevel, + targetSource: "git-root", + }; + } + + return { + targetDir: startDir, + targetSource: "cwd", + }; +} + +function installSkill({ agentsArg, dryRun = false, force = false, postinstall = false, targetDirArg = null }) { + const packageRoot = path.resolve(__dirname, ".."); + const { targetDir, targetSource } = resolveTargetDir(targetDirArg); + const logs = []; + const warnings = []; + const state = readState(targetDir); + + if (!fs.existsSync(path.join(packageRoot, "cli-skill"))) { + throw new Error("Package payload missing cli-skill directory."); + } + + const envAgents = parseAgents(process.env.CLI_SKILL_AGENTS); + const argAgents = parseAgents(agentsArg); + const detection = detectAgents(targetDir); + const detected = detection.agents; + + let selectionSource = "auto-detect"; + let selectedAgents = detected; + + if (argAgents.length > 0) { + selectionSource = "--agents"; + selectedAgents = argAgents; + } else if (envAgents.length > 0) { + selectionSource = "CLI_SKILL_AGENTS"; + selectedAgents = envAgents; + } + + selectedAgents = [...new Set(selectedAgents)]; + + const resolvedForce = force || normalizeBool(process.env.CLI_SKILL_FORCE); + const options = { dryRun, force: resolvedForce, targetDir }; + + logs.push(`target: ${targetDir}`); + logs.push(`target source: ${targetSource}`); + logs.push(`agents: ${selectedAgents.join(",")}`); + logs.push(`agents source: ${selectionSource}`); + logs.push(`mode: ${options.dryRun ? "dry-run" : "write"}${options.force ? ", force" : ""}`); + if (selectionSource === "auto-detect" && selectedAgents.length > 0) { + for (const agent of selectedAgents) { + logs.push(`detected ${agent}: ${detection.reasons[agent].join(", ")}`); + } + } + + const srcCliSkill = path.join(packageRoot, "cli-skill"); + const dstCliSkill = path.join(targetDir, "cli-skill"); + copyManagedTree(srcCliSkill, dstCliSkill, options, logs, state.data); + + const installs = { + copilot: [ + { + src: path.join(packageRoot, ".github", "skills", "cli-skill", "SKILL.md"), + dst: path.join(targetDir, ".github", "skills", "cli-skill", "SKILL.md"), + }, + ], + pi: [ + { + src: path.join(packageRoot, ".pi", "skills", "cli-skill", "SKILL.md"), + dst: path.join(targetDir, ".pi", "skills", "cli-skill", "SKILL.md"), + }, + ], + claude: [ + { + src: path.join(packageRoot, "cli-skill", "adapters", "claude-code", "commands.yaml"), + dst: path.join(targetDir, ".claude", "commands", "cli-skill.yaml"), + }, + ], + opencode: [ + { + src: path.join(packageRoot, "cli-skill", "adapters", "opencode", "commands.json"), + dst: path.join(targetDir, ".opencode", "commands", "cli-skill.json"), + }, + ], + }; + + for (const agent of selectedAgents) { + const mappings = installs[agent] || []; + logs.push(`installing adapter: ${agent}`); + + for (const mapping of mappings) { + if (!fs.existsSync(mapping.src)) { + warnings.push(`warning: missing source for ${agent}: ${mapping.src}`); + continue; + } + + copyManagedFile(mapping.src, mapping.dst, options, logs, state.data); + } + } + + writeState(state.path, state.data, options, logs); + + if (postinstall) { + logs.push("postinstall complete"); + logs.push("tip: run `npx canonical-cli-skill install --dry-run` to preview updates later"); + } + + if (detected.length === 0 && argAgents.length === 0 && envAgents.length === 0) { + warnings.push("warning: no known agent markers detected, installed cli-skill core only (set --agents to force adapter install)"); + } + + return { logs, warnings }; +} + +function runInstall(options, command) { + try { + const result = installSkill({ + agentsArg: options.agents || null, + dryRun: Boolean(options.dryRun), + force: Boolean(options.force), + postinstall: Boolean(options.postinstall), + targetDirArg: options.target || null, + }); + + result.logs.forEach((line) => writeOut(`${line}\n`)); + + if (result.warnings.length > 0) { + result.warnings.forEach((line) => writeErr(`${line}\n`)); + } + } catch (error) { + command.error(`Install failed: ${error instanceof Error ? error.message : String(error)}`, { + exitCode: 1, + code: "cli-skill.install-failed", + }); + } +} + +function main() { + const program = new Command(); + + program + .name("canonical-cli-skill") + .description("Install and maintain cli-skill files for supported coding agents.") + .configureOutput({ + writeOut, + writeErr, + }) + .showHelpAfterError() + .addHelpText("after", HELP_APPENDIX); + + program + .command("install") + .description("Install cli-skill core and detected/selected agent adapter files.") + .option("--dry-run", "Show changes without writing files") + .option("--force", "Overwrite existing files") + .option("--postinstall", "Internal flag used by npm postinstall") + .option("--agents ", "Comma-separated agent list (copilot,pi,claude,opencode)") + .option("--target ", "Target directory for installation") + .action((options, command) => { + runInstall(options, command); + }); + + program.parse(process.argv); +} + +main(); From e53ff631969faa2da38301cd3382340d847cf58b Mon Sep 17 00:00:00 2001 From: Hartmut Obendorf Date: Fri, 12 Jun 2026 10:38:15 +0200 Subject: [PATCH 2/2] scoring update --- cli-skill/commands/cli-review.md | 9 ++++----- cli-skill/references/cli-review-output-format.md | 2 +- scripts/calculate_cli_score.py | 8 ++++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/cli-skill/commands/cli-review.md b/cli-skill/commands/cli-review.md index a671250..7261cf3 100644 --- a/cli-skill/commands/cli-review.md +++ b/cli-skill/commands/cli-review.md @@ -32,11 +32,11 @@ CLI standard compliance review only. - Findings must map to explicit rules from the standard - Non-compliance and compliant evidence based on observed CLI behavior/docs/code - Use these rules to determine severity. Severity can only be [High|Medium|Low|Unrated]: - * High: violations of command structure and naming (DO NOT APPLY SHOULD RULES), use of positional parameters, accessibility/color violations + * High: violations of command structure and naming (SHOULD RULES DO NOT COUNT), use of positional parameters, accessibility/color violations * Medium: use if non-standard verbs for commands, inconsistent flag names or usage, extremely high complexity (eg. created by >20 commands) * Low: formatting violations, duplicate short/long flags * Unrated: use if no standard rule applies, or if the finding is not certain, there is a SHOULD rule, or the rule applies to a debug/secret command -- All SHOULD rules result in unrated violations - report them, but don't include them in the scoring. +- All SHOULD rules must be unrated violations - report them, but don't include them in the scoring. This can be a use of non-standard verbs, or in cases where the standard says "you should", or "prefer". ## Out Of Scope @@ -71,10 +71,9 @@ To calculate the score: 1. Create a JSON table with structure: `{"commands": , "issues": [{"severity": "High|Medium|Low|Unrated", "category": , "message": }, ...]}` 2. Execute: `python3 ` using the path resolution defined in `Execution Order` 3. This script returns JSON with `score` (0-100), `passed` (boolean), `rating badge`, and severity counts -4. Use this score and rating in the summary section of the markdown output -5. Use the rating badge in the summary section of the markdown output +4. Use this `score` and `rating badge` in the summary section of the markdown output -The script implements the standard algorithm: Start with 100%, weight W=100/#commands. For each High violation, reduce by 2*W; Medium violation by 1*W; Low violation by 0.5*W. Clamp to 0-100%. +The script implements the standard algorithm: Start with 100%, weight W=100/#commands. For each High violation, reduce by W; Medium violation by 0.5*W; Low violation by 0.2*W. Clamp to 0-100%. ### CLI Change Requirements Analyze the files that have been changes as part of this PR. Create a detailed summary of how each change affects the compliance of the CLI. diff --git a/cli-skill/references/cli-review-output-format.md b/cli-skill/references/cli-review-output-format.md index 5b8ea9c..dc296fc 100644 --- a/cli-skill/references/cli-review-output-format.md +++ b/cli-skill/references/cli-review-output-format.md @@ -13,7 +13,7 @@ | Unrated | <# unrated> |
| | **Total** | **<# total>** | | -**Overall rating:** +**Overall rating:** > The scoring algorithm starts with 100%, number of commands N, weight W=100/N. For each High violation, reduce by 2*W; Medium violation by 1*W; Low violation by 0.5*W. Clamp to 0-100. --- diff --git a/scripts/calculate_cli_score.py b/scripts/calculate_cli_score.py index b7fbede..ea23261 100644 --- a/scripts/calculate_cli_score.py +++ b/scripts/calculate_cli_score.py @@ -58,15 +58,15 @@ def calculate_score(issues_data: dict) -> dict: medium_count = sum(1 for issue in issues if issue.get("severity") == "Medium") low_count = sum(1 for issue in issues if issue.get("severity") == "Low") - # Calculate score: W = 100/N, High: -2W, Medium: -1W, Low: -0.5W + # Calculate score: W = 100/N, High: -W, Medium: -0.5W, Low: -0.2W if num_commands == 0: score = 100.0 else: weight = 100.0 / num_commands score = 100.0 - score -= high_count * 2 * weight - score -= medium_count * 1 * weight - score -= low_count * 0.5 * weight + score -= high_count * weight + score -= medium_count * 0.5 * weight + score -= low_count * 0.2 * weight score = max(0.0, min(100.0, score)) # Clamp to 0-100