diff --git a/CHANGELOG.md b/CHANGELOG.md index dc862bb..3f31a74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +--- + +## [1.5.1] - 2026-11-10 + ### Added +- **Deterministic Project Update & Lockfile Sync (`#202`, `#203`):** Added `updateInstalledSkills()` in [`src/sync/adapters.ts`](./src/sync/adapters.ts) and the `skillops update [name...]` (aliases: `sync`, `upgrade`) CLI command in [`src/cli.ts`](./src/cli.ts) to refresh installed Skills and Subagents across all active AI harnesses to the latest catalog version while preserving local user edits via SHA-256 drift detection (with `--dry-run` and `--force` support). +- **Project Status & SHA-256 Drift Diagnostics (`#200`, `#201`):** Added `inspectProjectStatus()` and `formatProjectStatusReport()` in [`src/sync/adapters.ts`](./src/sync/adapters.ts) and the `skillops status` (aliases: `list`, `ls`) CLI command in [`src/cli.ts`](./src/cli.ts) with 5-state SHA-256 artifact classification (`= HEALTHY`, `~ OUTDATED`, `! DRIFT`, `? MISSING`, `+ UNTRACK`) and `table`, `json`, and `markdown` outputs. +- **Multilingual Documentation (`PT-BR` & `ES`):** Added full Portuguese ([`docs/README.pt-BR.md`](./docs/README.pt-BR.md)) and Spanish ([`docs/README.es.md`](./docs/README.es.md)) translations linked from the header of [`README.md`](./README.md). - **5-Pillar User Trust Model & Security Documentation (`#193`):** Documented the 5-Pillar Supply-Chain Security & User Trust Architecture (`CODEOWNERS` curated registry, `INV-SKILL-06` AST + CodeQL SAST script gate, Anti-Trojan Unicode/boundary guards, Sigstore NPM `--provenance`, and `--dry-run` + `.skillops-lock.json` SHA-256 verification) in [`README.md`](./README.md) and [`.github/SECURITY.md`](./.github/SECURITY.md). - **`INV-SKILL-06` Security Invariant & Mandatory PR Security Review Checklist (`#192`):** Codified `INV-SKILL-06` (*Zero-Network, Trojan-Free & Boundary-Safe Security Contract*) in [`SKILL_CONTRACT.md`](./SKILL_CONTRACT.md) and added a mandatory supply-chain security review checklist in [`.github/PULL_REQUEST_TEMPLATE.md`](./.github/PULL_REQUEST_TEMPLATE.md) and [`CONTRIBUTING.md`](./CONTRIBUTING.md). - **Anti-Trojan Unicode, Symlink, Binary & Path Containment Guards (`#191`):** Extended [`src/auditor/security.ts`](./src/auditor/security.ts), [`src/auditor/parser.ts`](./src/auditor/parser.ts), and [`src/sync/adapters.ts`](./src/sync/adapters.ts) (`resolveSafeRepoTargetPath`) to block Trojan-Source (CVE-2021-42574) bidirectional control characters, LLM invisible Unicode Tag smuggling (`U+E0000..U+E007F`), zero-width script identifier spoofing, symbolic links, opaque compiled binary/archive payloads (`.exe`, `.dll`, `.so`, `.dylib`, `.bin`, `.wasm`, `.pyc`, `.zip`, `.tar.gz`), and repository boundary path traversal. diff --git a/README.md b/README.md index 4182974..e42c31b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- npm version + npm version CI CodeQL SAST 28 AI IDEs & CLIs @@ -50,6 +50,8 @@ Install globally (`npm i -g skillops-cli`) or run directly with `npx skillops-cl | :--- | :--- | | `skillops add` | Open the interactive TUI (select target AI IDEs → select Skills & Subagents) | | `skillops add -t cursor,claude` | Non-interactive install into specific targets (`-t all` for all 28 tools) | +| `skillops status` (`list`, `ls`) | Inspect installed Skills, Subagents, and SHA-256 health across AI harnesses | +| `skillops update [name...]` (`sync`, `upgrade`) | Refresh installed modules to the latest catalog version while preserving local edits | | `skillops remove [name...]` | Interactively or directly uninstall managed artifacts and update `.skillops-lock.json` | > **Flags:** `--all` selects every item in the catalog, `--dry-run` previews target file paths without writing to disk, and `--force` overwrites locally modified files. diff --git a/docs/README.es.md b/docs/README.es.md index d6f3167..c181288 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -9,7 +9,7 @@

- npm version + npm version CI CodeQL SAST 28 IDEs & CLIs de IA @@ -50,6 +50,8 @@ Instálalo globalmente (`npm i -g skillops-cli`) o ejecútalo directamente con ` | :--- | :--- | | `skillops add` | Abre la TUI interactiva (selecciona IDEs de IA de destino → selecciona Skills y Subagents) | | `skillops add -t cursor,claude` | Instalación no interactiva en destinos específicos (`-t all` para las 28 herramientas) | +| `skillops status` (`list`, `ls`) | Inspecciona las Skills y Subagents instalados y la integridad SHA-256 en las IDEs | +| `skillops update [name...]` (`sync`, `upgrade`) | Actualiza los módulos instalados a la versión más reciente del catálogo preservando ediciones locales | | `skillops remove [name...]` | Desinstala artefactos gestionados de forma interactiva o directa y actualiza `.skillops-lock.json` | > **Flags:** `--all` selecciona todos los elementos del catálogo, `--dry-run` previsualiza las rutas de destino sin escribir en disco y `--force` sobrescribe archivos modificados localmente. diff --git a/docs/README.pt-BR.md b/docs/README.pt-BR.md index 4750c48..70ec25a 100644 --- a/docs/README.pt-BR.md +++ b/docs/README.pt-BR.md @@ -9,7 +9,7 @@

- npm version + npm version CI CodeQL SAST 28 IDEs & CLIs de IA @@ -50,6 +50,8 @@ Instale globalmente (`npm i -g skillops-cli`) ou execute diretamente com `npx sk | :--- | :--- | | `skillops add` | Abre o TUI interativo (selecione as IDEs de IA de destino → selecione Skills e Subagents) | | `skillops add -t cursor,claude` | Instalação não interativa em alvos específicos (`-t all` para todas as 28 ferramentas) | +| `skillops status` (`list`, `ls`) | Inspeciona Skills e Subagents instalados e a integridade SHA-256 dos arquivos nas IDEs | +| `skillops update [name...]` (`sync`, `upgrade`) | Atualiza módulos instalados para a versão mais recente do catálogo preservando edições locais | | `skillops remove [name...]` | Desinstala artefatos gerenciados de forma interativa ou direta e atualiza o `.skillops-lock.json` | > **Flags:** `--all` seleciona todos os itens do catálogo, `--dry-run` pré-visualiza os caminhos de destino sem gravar no disco e `--force` sobrescreve arquivos modificados localmente. diff --git a/package-lock.json b/package-lock.json index 991b6d4..501602d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "skillops", - "version": "1.3.10", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "skillops", - "version": "1.3.10", + "version": "1.5.1", "license": "MIT", "dependencies": { "yaml": "^2.7.0", diff --git a/package.json b/package.json index 858e5c6..a1ee0e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skillops-cli", - "version": "1.3.10", + "version": "1.5.1", "description": "Install and manage AI coding Skills (SKILL.md) and Subagents (SUBAGENT.md) across 28 AI IDEs and CLIs (Cursor, Claude Code, Copilot, Antigravity, Windsurf, Codex, Gemini CLI & more).", "type": "module", "license": "MIT", diff --git a/src/cli.ts b/src/cli.ts index f07a9dd..b5897db 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -32,6 +32,7 @@ import { formatSyncReport, inspectProjectStatus, removeSkillOrSubagent, + updateInstalledSkills, } from "./sync/index.js"; import { findCommandHelpSpec, @@ -692,6 +693,47 @@ export function runCli(argv: readonly string[]): number { } } + if (command === "update" || command === "sync" || command === "upgrade") { + const positionals = getPositionalArgs(args); + const repoPath = resolveTargetRepoPath(args); + const cfgRes = loadSkillOpsConfig(repoPath); + if (cfgRes.error) { + process.stderr.write( + `Invalid .skillops.yml at ${cfgRes.configPath}: ${cfgRes.error}\n`, + ); + return ExitCode.BUDGET_OR_CONTRACT_ERROR; + } + const catalogDir = resolveCatalogDir(args, repoPath, cfgRes.config); + const rawTargetsArg = resolveTargetsArg(args); + const dryRun = args.includes("--dry-run"); + const force = args.includes("--force"); + const rawFormat = getFlagValue(args, "--format", "-f") ?? "table"; + const format: "table" | "json" | "markdown" = + rawFormat === "json" + ? "json" + : rawFormat === "markdown" || rawFormat === "md" + ? "markdown" + : "table"; + + try { + const targets = rawTargetsArg + ? resolveRuntimeTargets(rawTargetsArg, repoPath) + : undefined; + const report = updateInstalledSkills(repoPath, catalogDir, { + targets, + names: positionals.length > 0 ? positionals : undefined, + dryRun, + force, + }); + process.stdout.write(formatSyncReport(report, format)); + return report.passed ? ExitCode.SUCCESS : ExitCode.ERROR; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write(`Error: ${msg}\n`); + return ExitCode.ERROR; + } + } + const suggestion = suggestCommand(command); const hint = suggestion ? ` Did you mean '${suggestion}'?` : ""; process.stderr.write( diff --git a/src/models.ts b/src/models.ts index bdc8632..3aba8b3 100644 --- a/src/models.ts +++ b/src/models.ts @@ -10,7 +10,7 @@ import fs from "node:fs"; import YAML from "yaml"; import { z } from "zod"; -export const SKILLOPS_VERSION = "1.3.10"; +export const SKILLOPS_VERSION = "1.5.1"; const SKILL_NAME_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const SEMVER_REGEX = /^\d+\.\d+\.\d+$/; diff --git a/src/ui/help.ts b/src/ui/help.ts index 3ebb125..9e135c6 100644 --- a/src/ui/help.ts +++ b/src/ui/help.ts @@ -157,6 +157,49 @@ export const COMMAND_HELP_SPECS: readonly CommandHelpSpec[] = [ }, ], }, + { + name: "update", + aliases: ["sync", "upgrade"], + summary: "Update installed Skills & Subagents while preserving local edits", + usage: "npx skillops-cli update [name...] [options]", + options: [ + { + flags: "-p, --project

", + description: "Target repository directory (default: current directory)", + }, + { + flags: "-g, --global", + description: "Update user home directory (~) instead of current project", + }, + { + flags: "-t, --target ", + description: "Restrict update to specific AI harnesses", + }, + { + flags: "--dry-run", + description: "Preview planned updates without modifying disk", + }, + { + flags: "--force", + description: + "Overwrite locally drifted files with latest catalog version", + }, + { + flags: "-f, --format ", + description: "Output format (default: table)", + }, + ], + examples: [ + { + command: "skillops update", + description: "Update all installed Skills & Subagents in project", + }, + { + command: "skillops update mcp-architect --force", + description: "Force-refresh mcp-architect overwriting local edits", + }, + ], + }, ]; /** @@ -203,6 +246,11 @@ export function renderGlobalHelp( color: CHARM_PALETTE.cyan, desc: "Inspect installed Skills, Agents & SHA-256 health", }, + { + cmd: "update [name...]", + color: CHARM_PALETTE.amber, + desc: "Update installed Skills & Agents (preserves local edits)", + }, { cmd: "remove [name...]", color: CHARM_PALETTE.hotPink, @@ -217,7 +265,7 @@ export function renderGlobalHelp( ) .join("\n"); - const usageLine = `${bar} ${fgHex(CHARM_PALETTE.lavender, style.bold("Usage:", c), c)} ${style.bold(style.green("skillops", c), c)} ${fgHex(CHARM_PALETTE.cyan, "", c)} ${style.dim("[options]", c)}`; + const usageLine = `${bar} ${fgHex(CHARM_PALETTE.lavender, style.bold("Usage:", c), c)} ${style.bold(style.green("skillops", c), c)} ${fgHex(CHARM_PALETTE.cyan, "", c)} ${style.dim("[options]", c)}`; return [ banner, diff --git a/src/ui/terminal.ts b/src/ui/terminal.ts index 99999a0..43644dc 100644 --- a/src/ui/terminal.ts +++ b/src/ui/terminal.ts @@ -560,6 +560,9 @@ export const DEFAULT_VALID_COMMANDS: readonly string[] = [ "status", "list", "ls", + "update", + "sync", + "upgrade", "remove", "uninstall", "rm", diff --git a/tests/npx_core.test.ts b/tests/npx_core.test.ts index 82224f5..32bcc0c 100644 --- a/tests/npx_core.test.ts +++ b/tests/npx_core.test.ts @@ -179,8 +179,9 @@ describe("SkillOps Core Models & Subagent Contracts", () => { expect(runCli(["node", "cli.js", "--help"])).toBe(ExitCode.SUCCESS); expect(runCli(["node", "cli.js", "help", "add"])).toBe(ExitCode.SUCCESS); expect(runCli(["node", "cli.js", "help", "status"])).toBe(ExitCode.SUCCESS); + expect(runCli(["node", "cli.js", "help", "update"])).toBe(ExitCode.SUCCESS); expect(runCli(["node", "cli.js", "help", "remove"])).toBe(ExitCode.SUCCESS); - expect(SKILLOPS_VERSION).toBe("1.3.10"); + expect(SKILLOPS_VERSION).toBe("1.5.1"); }); it("validates Top-28 AI Coding Agent registry, auto-detection matrix, and target resolution", () => { diff --git a/tests/router_sync.test.ts b/tests/router_sync.test.ts index e539ae7..e920d1f 100644 --- a/tests/router_sync.test.ts +++ b/tests/router_sync.test.ts @@ -1570,6 +1570,55 @@ describe("SkillOps Multi-Runtime Add & Remove Engine", () => { names: ["non-existent-module"], }), ).toThrow(/No installed artifacts found for 'non-existent-module'/); + + // 9. CLI invocation for 'skillops update', 'skillops sync', and 'skillops upgrade' (Issue #203) + fs.appendFileSync( + catalogSkillMd, + "\n\n", + "utf-8", + ); + fs.appendFileSync(cursorMdc, "\n\n", "utf-8"); + + expect( + runCli([ + "node", + "cli.js", + "update", + "mcp-architect", + "--repo", + tmpRepo, + "--catalog", + tmpCatalog, + ]), + ).toBe(ExitCode.ERROR); + + expect( + runCli([ + "node", + "cli.js", + "sync", + "mcp-architect", + "--repo", + tmpRepo, + "--catalog", + tmpCatalog, + "--force", + ]), + ).toBe(ExitCode.SUCCESS); + + expect( + runCli([ + "node", + "cli.js", + "upgrade", + "--repo", + tmpRepo, + "--catalog", + tmpCatalog, + "-f", + "json", + ]), + ).toBe(ExitCode.SUCCESS); } finally { fs.rmSync(tmpRepo, { recursive: true, force: true }); fs.rmSync(tmpCatalog, { recursive: true, force: true }); diff --git a/tests/tui_terminal.test.ts b/tests/tui_terminal.test.ts index 6b762bd..7e82243 100644 --- a/tests/tui_terminal.test.ts +++ b/tests/tui_terminal.test.ts @@ -60,10 +60,10 @@ describe("Zero-Dependency ANSI TUI Aesthetic Engine", () => { it("renders rounded Unicode panels, clean tree header banners, and multi-column aligned tables", () => { const banner = renderHeaderBanner("Framework & Library", { - version: "1.3.10", + version: "1.5.1", colorEnabled: false, }); - expect(banner).toContain("◆ SkillOps v1.3.10"); + expect(banner).toContain("◆ SkillOps v1.5.1"); expect(banner).toContain("Framework & Library"); expect(banner).not.toContain("⚡"); @@ -117,7 +117,7 @@ describe("Zero-Dependency ANSI TUI Aesthetic Engine", () => { expect(visibleLength(line)).toBeLessThanOrEqual(78); } - const helpOut = renderGlobalHelp("1.3.10", false); + const helpOut = renderGlobalHelp("1.5.1", false); expect(helpOut).not.toMatch(/(?:🚀|📚|🛠️|⚙️|🌐|💡|⚡|📦|🔍)/u); expect(helpOut).toContain("add [name...]"); expect(helpOut).toContain("remove [name...]"); @@ -466,7 +466,7 @@ describe("Zero-Dependency ANSI TUI Aesthetic Engine", () => { true, ); expect(stripAnsi(syncReportOut)).toContain( - "◆ SkillOps v1.3.10 — Skills & Subagents Installed", + "◆ SkillOps v1.5.1 — Skills & Subagents Installed", ); expect(stripAnsi(syncReportOut)).toContain("+ CREATED"); expect(stripAnsi(syncReportOut)).toContain("(cursor)");