Skip to content

fix: sync skills incrementally during update#1042

Merged
zhangheng023 merged 1 commit into
mainfrom
fix/incremental-skills-update
May 26, 2026
Merged

fix: sync skills incrementally during update#1042
zhangheng023 merged 1 commit into
mainfrom
fix/incremental-skills-update

Conversation

@zhangheng023
Copy link
Copy Markdown
Collaborator

@zhangheng023 zhangheng023 commented May 22, 2026

Summary

This PR makes lark-cli update sync bundled skills incrementally instead of reinstalling every official skill. It records official skill state, updates installed and newly added official skills, preserves intentionally deleted skills, and falls back to a full sync when incremental discovery fails.

Changes

  • Wire cmd/update/update.go to run skill sync state handling during update flows and include skill sync status in human and JSON output.
  • Add incremental skill planning, parsing, fallback sync, and state persistence in internal/skillscheck/sync.go and internal/skillscheck/state.go.
  • Extend internal/selfupdate/updater.go with skill list/install command helpers for official, global, selected, and full skill sync operations.
  • Replace legacy stamp tracking with state-based tests across cmd/update/update_test.go, internal/skillscheck/*_test.go, and internal/selfupdate/updater_test.go.

Test Plan

  • skipped: make unit-test not rerun during PR drafting; branch already contains committed test updates
  • skipped: validate not rerun during PR drafting
  • skipped: local-eval not rerun during PR drafting
  • skipped: acceptance-reviewer not rerun during PR drafting
  • skipped: manual verification not rerun during PR drafting

Related Issues

N/A

Summary by CodeRabbit

  • Refactor

    • Replaced stamp-based skills tracking with a persisted JSON state, normalized version comparison, and adjusted drift/cold-start behavior; update flow now uses state/version-based syncing and skips certain cleanup in check mode.
  • New Features

    • Added end-to-end skills synchronization with official/local listing, incremental installs with full-install fallback, state persistence, and user-facing skills list/install operations via the updater.
  • Tests

    • Expanded unit and integration tests covering parsing, planning, sync flows, state persistence, normalization, and cold-start/integration scenarios.

Review Change Stack

@zhangheng023 zhangheng023 added bug Something isn't working domain/core CLI framework and core libraries size/L Large or sensitive change across domains or core paths labels May 22, 2026
@github-actions github-actions Bot removed the domain/core CLI framework and core libraries label May 22, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 22, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Migrate skills persistence from a single-line stamp file to JSON skills-state.json; add parsing, sync planning, and SyncSkills orchestration; add Updater skills commands; integrate state-based dedup/reporting into cmd/update; and update tests and notifier usage.

Changes

Skills State and Synchronization Overhaul

Layer / File(s) Summary
Notice composition tests with state
cmd/root_integration_test.go
Seed skills-state.json via WriteState in notice tests and update cold-start wording.
Update command state-based skills sync
cmd/update/update.go, cmd/update/update_test.go
Use runSkillsAndState + ReadSyncedVersion/SyncResult; skip CleanupStaleFiles() on --check; introduce syncSkills var and applySkillsStatus; update JSON/text reporting and tests to skills_summary/skills_action.
State persistence foundation
internal/skillscheck/state.go, internal/skillscheck/state_test.go
Add SkillsState, ErrUnreadableState, ReadState, WriteState, atomic pretty JSON writes, and ReadSyncedVersion with cold-start semantics.
Skill list parsing and sync planning
internal/skillscheck/sync.go, internal/skillscheck/sync_test.go
Parse CLI outputs, compute SyncPlan, perform incremental installs or fallback full install, write SkillsState, and return SyncResult; extensive unit tests and fallback coverage.
Updater skill command methods
internal/selfupdate/updater.go, internal/selfupdate/updater_test.go
Replace no-arg hook with SkillsCommandOverride(args...), add ListOfficialSkills/ListGlobalSkills/InstallSkill/InstallAllSkills and runSkillsCommand variadic invocation; tests validate npx arg construction and fallback.
Init and notice coordination
internal/skillscheck/check.go, internal/skillscheck/notice.go, internal/skillscheck/check_test.go
Switch Init from stamp to state via ReadSyncedVersion, normalize leading v/V for comparisons, and update docs/tests to reflect state-based drift detection.
Sync unit tests and update test migration
internal/skillscheck/sync_test.go, cmd/update/update_test.go
Add/adjust tests: migrate from stamp-based mocks to SkillsCommandOverride, add runSkillsAndState unit tests, live npx integration tests, and assert skills-state.json contents and new fields.

Sequence Diagram(s)

sequenceDiagram
  participant UpdateCmd as update command
  participant RunSkillsAndState
  participant SyncSkills as SyncSkills orchestration
  participant Runner as Updater
  participant StateFile as skills-state.json
  UpdateCmd->>RunSkillsAndState: targetVersion
  alt Dedup hit (synced version matches)
    RunSkillsAndState-->>UpdateCmd: return prior state, skip sync
  else Need sync (cold start or new version)
    RunSkillsAndState->>SyncSkills: SyncOptions{Version, Force, Runner}
    SyncSkills->>Runner: ListOfficialSkills()
    Runner-->>SyncSkills: NpmResult(stdout)
    SyncSkills->>SyncSkills: ParseSkillsList → PlanSync
    alt Incremental install available
      SyncSkills->>Runner: InstallSkill(ToUpdate)
      Runner-->>SyncSkills: NpmResult
    else Fall back to full
      SyncSkills->>Runner: InstallAllSkills()
      Runner-->>SyncSkills: NpmResult
    end
    SyncSkills->>StateFile: WriteState(updated version, lists)
    StateFile-->>SyncSkills: persisted
    SyncSkills-->>RunSkillsAndState: SyncResult
  end
  RunSkillsAndState-->>UpdateCmd: SyncResult or nil
  UpdateCmd->>UpdateCmd: emit JSON/text via applySkillsStatus or emitSkillsTextHints
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • larksuite/cli#723: Prior work on skills drift/notify and update flow related to migrating stamp-based behavior.
  • larksuite/cli#1008: Overlapping edits around skills persistence and update reporting; closely related on same code paths.
  • larksuite/cli#965: Parallel migration of skills synchronization to state-based persistence affecting similar files.

Suggested reviewers

  • liangshuo-1
  • MaxHuang22

Poem

🐰 From single-line stamps we hopped to state,
JSON nests now hold the version's fate,
We parse and plan, install with care,
Write timestamps, tests, and fallback flair,
A rabbit cheers: "Sync — accurate!"

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description covers summary, changes, test plan, and related issues. However, all test plan items are marked as 'skipped,' indicating incomplete verification before PR submission. Complete at least the unit tests and manual verification before merging, or clarify why all verification steps were skipped during PR drafting.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: incremental skill syncing during update instead of full reinstalls.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/incremental-skills-update

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 22, 2026

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@136dba2203d8a41da9eda389f37ca524e6319fd2

🧩 Skill update

npx skills add larksuite/cli#fix/incremental-skills-update -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/skillscheck/state.go`:
- Around line 46-54: ReadState currently accepts JSON with extra/unexpected
fields because it uses json.Unmarshal into SkillsState; change the second
unmarshal to use a json.Decoder with DisallowUnknownFields so unknown fields
cause an error. Specifically, after the initial syntax check (the first
json.Unmarshal into raw), create a decoder from the input bytes (e.g.,
bytes.NewReader(data)), call decoder.DisallowUnknownFields(), then
decoder.Decode(&state) into the SkillsState and return ErrUnreadableState on
decode errors; ensure errors reference ErrUnreadableState where currently
returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b579cd2e-e130-4a76-a940-42a58c44413e

📥 Commits

Reviewing files that changed from the base of the PR and between d5d2fee and 6a295f0.

📒 Files selected for processing (14)
  • cmd/root_integration_test.go
  • cmd/update/update.go
  • cmd/update/update_test.go
  • internal/selfupdate/updater.go
  • internal/selfupdate/updater_test.go
  • internal/skillscheck/check.go
  • internal/skillscheck/check_test.go
  • internal/skillscheck/notice.go
  • internal/skillscheck/stamp.go
  • internal/skillscheck/stamp_test.go
  • internal/skillscheck/state.go
  • internal/skillscheck/state_test.go
  • internal/skillscheck/sync.go
  • internal/skillscheck/sync_test.go
💤 Files with no reviewable changes (2)
  • internal/skillscheck/stamp_test.go
  • internal/skillscheck/stamp.go

Comment thread internal/skillscheck/state.go
@zhangheng023 zhangheng023 added feature and removed bug Something isn't working labels May 22, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 22, 2026

Codecov Report

❌ Patch coverage is 81.08108% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.99%. Comparing base (ffcf778) to head (136dba2).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
internal/skillscheck/sync.go 81.55% 26 Missing and 12 partials ⚠️
cmd/update/update.go 83.05% 6 Missing and 4 partials ⚠️
internal/selfupdate/updater.go 66.66% 9 Missing ⚠️
internal/skillscheck/state.go 83.78% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1042      +/-   ##
==========================================
+ Coverage   67.75%   67.99%   +0.23%     
==========================================
  Files         590      604      +14     
  Lines       55188    56057     +869     
==========================================
+ Hits        37392    38115     +723     
- Misses      14684    14785     +101     
- Partials     3112     3157      +45     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zhangheng023 zhangheng023 force-pushed the fix/incremental-skills-update branch from 2b24d66 to b411e4c Compare May 22, 2026 11:25
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cmd/update/update.go (1)

39-44: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle uppercase V in version normalization too.

normalizeVersion only strips lowercase v, so values like V1.0.21 can be treated as out-of-sync in this command path.

Suggested patch
 func normalizeVersion(s string) string {
-	return strings.TrimPrefix(strings.TrimSpace(s), "v")
+	s = strings.TrimSpace(s)
+	s = strings.TrimPrefix(s, "v")
+	return strings.TrimPrefix(s, "V")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/update/update.go` around lines 39 - 44, normalizeVersion currently only
removes a lowercase "v" prefix so inputs like "V1.0.21" remain unchanged; update
the normalizeVersion function to strip either "v" or "V" (e.g., check the first
rune and remove it if it equals 'v' or 'V', or use a case-insensitive trim)
after trimming whitespace so both "v1.2.3" and "V1.2.3" normalize to "1.2.3".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/skillscheck/sync.go`:
- Around line 254-257: The code currently treats empty/nil from ParseSkillsList
as “no local skills” which can silently mis-plan syncs; update the block that
calls opts.Runner.ListGlobalSkills() so that after verifying localResult != nil
&& localResult.Err == nil you parse stdout into parsed :=
ParseSkillsList(localResult.Stdout.String()) and then detect an unparsable case
(stdout is non-empty but parsed is nil/empty) and in that case mark it as
unparsable and fall back to the full-install path (e.g., set local to nil or set
the same fallback flag used by the official-list handling) instead of treating
it as no skills; keep using the same symbols ParseSkillsList, localResult,
opts.Runner.ListGlobalSkills and the local variable so the planner will choose
full install when parsing fails.

---

Outside diff comments:
In `@cmd/update/update.go`:
- Around line 39-44: normalizeVersion currently only removes a lowercase "v"
prefix so inputs like "V1.0.21" remain unchanged; update the normalizeVersion
function to strip either "v" or "V" (e.g., check the first rune and remove it if
it equals 'v' or 'V', or use a case-insensitive trim) after trimming whitespace
so both "v1.2.3" and "V1.2.3" normalize to "1.2.3".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3a049394-a990-42e9-944f-00eed52d7298

📥 Commits

Reviewing files that changed from the base of the PR and between 2b24d66 and b411e4c.

📒 Files selected for processing (14)
  • cmd/root_integration_test.go
  • cmd/update/update.go
  • cmd/update/update_test.go
  • internal/selfupdate/updater.go
  • internal/selfupdate/updater_test.go
  • internal/skillscheck/check.go
  • internal/skillscheck/check_test.go
  • internal/skillscheck/notice.go
  • internal/skillscheck/stamp.go
  • internal/skillscheck/stamp_test.go
  • internal/skillscheck/state.go
  • internal/skillscheck/state_test.go
  • internal/skillscheck/sync.go
  • internal/skillscheck/sync_test.go
💤 Files with no reviewable changes (2)
  • internal/skillscheck/stamp_test.go
  • internal/skillscheck/stamp.go
✅ Files skipped from review due to trivial changes (1)
  • internal/skillscheck/notice.go

Comment thread internal/skillscheck/sync.go
@zhangheng023 zhangheng023 force-pushed the fix/incremental-skills-update branch from ead4f97 to 136dba2 Compare May 26, 2026 08:24
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
internal/skillscheck/state.go (1)

46-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject unknown fields when decoding skills state JSON.

ReadState currently accepts JSON documents with extra fields, so incompatible schema changes can be treated as valid state instead of unreadable state.

Suggested fix
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"io/fs"
 	"path/filepath"
@@
-	var state SkillsState
-	if err := json.Unmarshal(data, &state); err != nil {
+	var state SkillsState
+	dec := json.NewDecoder(bytes.NewReader(data))
+	dec.DisallowUnknownFields()
+	if err := dec.Decode(&state); err != nil {
+		return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
+	}
+	if err := dec.Decode(&struct{}{}); err != io.EOF {
 		return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
 	}
 	return &state, true, nil
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/skillscheck/state.go` around lines 46 - 54, ReadState currently
allows extra JSON fields because it unmarshals into a map first and then into
SkillsState; instead decode strictly by creating a json.Decoder over the data
(e.g., json.NewDecoder(bytes.NewReader(data))), call
decoder.DisallowUnknownFields(), and decode directly into the SkillsState
variable (SkillsState) so any unknown fields produce an error you wrap with
ErrUnreadableState; remove the initial raw map unmarshal and ensure the error
path still returns nil, false, and fmt.Errorf("%w: %v", ErrUnreadableState,
err).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/update/update_test.go`:
- Around line 1265-1286: The live integration test
TestUpdateCommand_RealSkillsSyncRewritesState currently runs whenever npx is
present, making CI flaky; update the test to be opt-in by adding a gating check
at the start (before exec.LookPath): skip unless either testing.Short() is false
AND an explicit env var is set (for example require
os.Getenv("RUN_LIVE_NPX_SKILLS_TESTS") == "1"), or simply require the env var;
apply the same gating change to the other live npx skills test referenced in the
diff (the second Test* block around lines 1378-1401) so both tests only run when
the env var is present (and optionally when not testing.Short()).
- Around line 44-53: The tests that don't exercise the live skills command are
missing a stub for SkillsCommandOverride, so updateRun can call
runSkillsAndState and accidentally execute a real npx skills; modify the test
helper(s) that build the test updater (e.g., mockDetectAndNpm and any similar
helper that assigns newUpdater) to always set u.SkillsCommandOverride =
successfulSkillsCommand() on the returned *selfupdate.Updater (and ensure
newUpdater replacement used by TestUpdateAlreadyUpToDate_* and
TestUpdateManual_* goes through that same helper), so every non-live test uses
the stubbed skills command instead of invoking the real binary.

In `@cmd/update/update.go`:
- Around line 380-381: The status map currently sets status["skipped_deleted"] =
state.SkippedDeletedSkills (a []string) which breaks JSON consumers expecting
counts for skills_status; change this to emit the count instead, e.g.
status["skipped_deleted"] = len(state.SkippedDeletedSkills), so the key remains
a numeric count like "official" and "updated" (alternatively, if you prefer to
keep the slice, rename the key to "skipped_deleted_skills" and leave the slice
value). Update the assignment in the same block where status is populated
(referencing state.SkippedDeletedSkills and the status map) accordingly.

In `@internal/skillscheck/sync.go`:
- Around line 329-338: The current branch returns a SyncResult with Action
"fallback_synced" when WriteState(state) fails, masking the write failure;
update the error handling in the WriteState error path inside the function that
constructs SyncResult so that instead of returning a successful
"fallback_synced" result with no Err, you return a SyncResult that includes the
write error (set Err to writeErr) and/or change the control flow to propagate
the writeErr upward (return nil, writeErr) so the caller sees the persistence
failure; locate the block that calls WriteState(state) and constructs the
SyncResult and ensure Err is populated (or the function returns the error) when
WriteState returns an error.

---

Duplicate comments:
In `@internal/skillscheck/state.go`:
- Around line 46-54: ReadState currently allows extra JSON fields because it
unmarshals into a map first and then into SkillsState; instead decode strictly
by creating a json.Decoder over the data (e.g.,
json.NewDecoder(bytes.NewReader(data))), call decoder.DisallowUnknownFields(),
and decode directly into the SkillsState variable (SkillsState) so any unknown
fields produce an error you wrap with ErrUnreadableState; remove the initial raw
map unmarshal and ensure the error path still returns nil, false, and
fmt.Errorf("%w: %v", ErrUnreadableState, err).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c034d82-e411-4461-a639-96a8aebcaba4

📥 Commits

Reviewing files that changed from the base of the PR and between ead4f97 and 136dba2.

📒 Files selected for processing (14)
  • cmd/root_integration_test.go
  • cmd/update/update.go
  • cmd/update/update_test.go
  • internal/selfupdate/updater.go
  • internal/selfupdate/updater_test.go
  • internal/skillscheck/check.go
  • internal/skillscheck/check_test.go
  • internal/skillscheck/notice.go
  • internal/skillscheck/stamp.go
  • internal/skillscheck/stamp_test.go
  • internal/skillscheck/state.go
  • internal/skillscheck/state_test.go
  • internal/skillscheck/sync.go
  • internal/skillscheck/sync_test.go
💤 Files with no reviewable changes (2)
  • internal/skillscheck/stamp.go
  • internal/skillscheck/stamp_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/skillscheck/notice.go

Comment thread cmd/update/update_test.go
Comment thread cmd/update/update_test.go
Comment thread cmd/update/update.go
Comment thread internal/skillscheck/sync.go
@zhangheng023 zhangheng023 merged commit 137176e into main May 26, 2026
21 checks passed
@zhangheng023 zhangheng023 deleted the fix/incremental-skills-update branch May 26, 2026 11:23
@liangshuo-1 liangshuo-1 mentioned this pull request May 26, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants