Skip to content

fix: sync skills incrementally during update#1042

Open
zhangheng023 wants to merge 2 commits into
mainfrom
fix/incremental-skills-update
Open

fix: sync skills incrementally during update#1042
zhangheng023 wants to merge 2 commits 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 and normalized version comparisons; drift/cold-start behavior adjusted and update command uses state-based syncing (skips certain cleanup in check mode).
  • New Features

    • Added end-to-end skills sync: listing official/local skills, planning incremental installs with full-install fallback, persisting sync state, and user-facing skills list/install operations.
  • 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68fc3faf-bf94-4770-82ee-d1cb2cc38d3a

📥 Commits

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

📒 Files selected for processing (4)
  • cmd/update/update.go
  • cmd/update/update_test.go
  • internal/skillscheck/sync.go
  • internal/skillscheck/sync_test.go

📝 Walkthrough

Walkthrough

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

Changes

Skills State and Synchronization Overhaul

Layer / File(s) Summary
Notice composition tests with state
cmd/root_integration_test.go
Update tests to seed skills-state.json via WriteState and adjust cold-start wording.
Update command state-based skills sync
cmd/update/update.go
Use runSkillsAndState + ReadSyncedVersion/SyncResult, skip CleanupStaleFiles() on --check, introduce syncSkills var, and emit state-derived JSON/text fields.
Update command test migration and new tests
cmd/update/update_test.go
Migrate mocks to SkillsCommandOverride, simplify mockDetectAndNpm, add runSkillsAndState unit tests and live integration tests, and assert skills-state.json contents and new skills_summary fields.
Updater skill command methods
internal/selfupdate/updater.go
Add SkillsCommandOverride and Updater methods ListOfficialSkills, ListGlobalSkills, InstallSkill, InstallAllSkills, centralizing execution via runSkillsCommand(args...).
Updater skills command tests
internal/selfupdate/updater_test.go
Add tests that validate constructed npx args and fallback invocation via SkillsCommandOverride.
Skills check init and notice coordination
internal/skillscheck/check.go, internal/skillscheck/check_test.go, internal/skillscheck/notice.go
Switch Init from ReadStamp to ReadSyncedVersion, normalize leading v/V in comparisons, and update docs and tests accordingly.
State persistence foundation
internal/skillscheck/state.go, internal/skillscheck/state_test.go
Add SkillsState, ErrUnreadableState, ReadState, WriteState, and ReadSyncedVersion with atomic JSON writes and cold-start semantics.
Skill list parsing and sync planning
internal/skillscheck/sync.go, internal/skillscheck/sync_test.go
Parse two CLI output formats, compute SyncPlan, perform incremental or fallback installs, write updated SkillsState, and return SyncResult; include comprehensive tests.

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, runner}
    SyncSkills->>Runner: ListOfficialSkills()
    Runner-->>SyncSkills: NpmResult(stdout)
    SyncSkills->>SyncSkills: ParseSkillsList → plan
    SyncSkills->>Runner: InstallSkill(ToUpdate) or InstallAllSkills()
    Runner-->>SyncSkills: result
    SyncSkills->>StateFile: WriteState(updated version, skills 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#1008: Overlapping edits around skills persistence and update reporting; closely related on the same code paths.
  • larksuite/cli#965: Parallel migration of skills synchronization to state-based persistence affecting similar files.
  • larksuite/cli#723: Prior work on skills drift/notify and update flow that this PR migrates further from stamp to state.

Suggested labels

size/XL

Suggested reviewers

  • liangshuo-1
  • MaxHuang22
  • sang-neo03

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 | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: sync skills incrementally during update' directly reflects the main change: shifting from full skill reinstall to incremental synchronization during update operations.
Description check ✅ Passed The PR description covers all required template sections: a clear summary, detailed list of changes across multiple files, and test plan with explicit mentions of verification steps (though marked skipped during drafting).
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@ead4f9769a0827cfb4175a3e3fac40609a427d62

🧩 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 on lines +46 to +54
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
}

var state SkillsState
if err := json.Unmarshal(data, &state); err != nil {
return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unknown-schema state files are currently treated as valid.

Line 46 validates JSON syntax, but not schema. A payload with unexpected fields (for example schema_version) still unmarshals into SkillsState, so ReadState() returns ok=true instead of ErrUnreadableState.

💡 Proposed fix (strict decode with unknown-field rejection)
 import (
+	"bytes"
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
 	"io/fs"
 	"path/filepath"
@@
-	var raw map[string]interface{}
-	if err := json.Unmarshal(data, &raw); err != nil {
-		return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
-	}
-
 	var state SkillsState
-	if err := json.Unmarshal(data, &state); err != nil {
+	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
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
}
var state SkillsState
if err := json.Unmarshal(data, &state); err != nil {
return nil, false, fmt.Errorf("%w: %v", ErrUnreadableState, err)
}
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
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.

@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.53846% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.82%. Comparing base (ffcf778) to head (ead4f97).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
internal/skillscheck/sync.go 84.02% 19 Missing and 12 partials ⚠️
internal/selfupdate/updater.go 58.06% 13 Missing ⚠️
cmd/update/update.go 83.05% 6 Missing and 4 partials ⚠️
internal/skillscheck/state.go 83.78% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1042      +/-   ##
==========================================
+ Coverage   67.75%   67.82%   +0.07%     
==========================================
  Files         590      591       +1     
  Lines       55188    55467     +279     
==========================================
+ Hits        37392    37622     +230     
- Misses      14684    14712      +28     
- Partials     3112     3133      +21     

☔ 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
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.

1 participant