From 17e0b5e3f696a4f78429e2c01b54d49b99e1652e Mon Sep 17 00:00:00 2001
From: OdevMatheus
Date: Tue, 10 Nov 2026 13:07:34 -0300
Subject: [PATCH] feat(cli): add 'skillops update' (aliases 'sync', 'upgrade'),
bump to v1.5.1 & update multilingual docs (closes #203)
---
CHANGELOG.md | 7 ++++++
README.md | 4 ++-
docs/README.es.md | 4 ++-
docs/README.pt-BR.md | 4 ++-
package-lock.json | 4 +--
package.json | 2 +-
src/cli.ts | 42 ++++++++++++++++++++++++++++++++
src/models.ts | 2 +-
src/ui/help.ts | 50 +++++++++++++++++++++++++++++++++++++-
src/ui/terminal.ts | 3 +++
tests/npx_core.test.ts | 3 ++-
tests/router_sync.test.ts | 49 +++++++++++++++++++++++++++++++++++++
tests/tui_terminal.test.ts | 8 +++---
13 files changed, 169 insertions(+), 13 deletions(-)
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 @@
-
+
@@ -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 @@
-
+
@@ -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 @@
-
+
@@ -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)");