-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: add --skills suite mode to update for partial skill install #1600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6d4f458
d592022
eab2063
95ee561
9e2cac6
45399ca
7eabb27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,10 +86,12 @@ | |
|
|
||
| // UpdateOptions holds inputs for the update command. | ||
| type UpdateOptions struct { | ||
| Factory *cmdutil.Factory | ||
| JSON bool | ||
| Force bool | ||
| Check bool | ||
| Factory *cmdutil.Factory | ||
| JSON bool | ||
| Force bool | ||
| Check bool | ||
| Skills []string | ||
| SuiteProvided bool | ||
| } | ||
|
|
||
| // NewCmdUpdate creates the update command. | ||
|
|
@@ -108,13 +110,16 @@ | |
| Use --json for structured output (for AI agents and scripts). | ||
| Use --check to only check for updates without installing.`, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| opts.SuiteProvided = cmd.Flags().Changed("skills") | ||
| return updateRun(opts) | ||
| }, | ||
| } | ||
| cmdutil.DisableAuthCheck(cmd) | ||
| cmd.Flags().BoolVar(&opts.JSON, "json", false, "structured JSON output") | ||
| cmd.Flags().BoolVar(&opts.Force, "force", false, "force reinstall even if already up to date") | ||
| cmd.Flags().BoolVar(&opts.Check, "check", false, "only check for updates, do not install") | ||
| cmd.Flags().StringSliceVar(&opts.Skills, "skills", nil, | ||
| "comma-separated lark skill names to install and remember (the suite); use --skills all to reset to all official skills") | ||
| cmdutil.SetRisk(cmd, "high-risk-write") | ||
|
|
||
| return cmd | ||
|
|
@@ -125,6 +130,18 @@ | |
| cur := currentVersion() | ||
| updater := newUpdater() | ||
|
|
||
| // 早期格式校验:在任何网络/安装动作之前 fail-fast。 | ||
| var suite *skillscheck.SuiteSelection | ||
| if opts.SuiteProvided { | ||
| parsed, err := skillscheck.ParseSuiteSelection(opts.Skills) | ||
| if err != nil { | ||
| return reportError(opts, io, "validation", | ||
| errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", err). | ||
| WithHint("e.g. --skills lark-calendar,lark-im (or --skills all to reset)")) | ||
| } | ||
| suite = parsed | ||
| } | ||
|
|
||
| if !opts.Check { | ||
| updater.CleanupStaleFiles() | ||
| } | ||
|
|
@@ -147,7 +164,10 @@ | |
| if !opts.Force && !update.IsNewer(latest, cur) { | ||
| var skillsResult *skillscheck.SyncResult | ||
| if !opts.Check { | ||
| skillsResult = runSkillsAndState(updater, io, cur, opts.Force) | ||
| skillsResult = runSkillsAndState(updater, io, cur, opts.Force, suite) | ||
| if err := suiteInputError(opts, io, skillsResult); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return reportAlreadyUpToDate(opts, io, cur, latest, skillsResult, opts.Check) | ||
| } | ||
|
|
@@ -162,22 +182,26 @@ | |
|
|
||
| // 6. Execute update | ||
| if !detect.CanAutoUpdate() { | ||
| return doManualUpdate(opts, io, cur, latest, detect, updater) | ||
| return doManualUpdate(opts, io, cur, latest, detect, updater, suite) | ||
| } | ||
| return doNpmUpdate(opts, io, cur, latest, updater) | ||
| return doNpmUpdate(opts, io, cur, latest, updater, suite) | ||
| } | ||
|
|
||
| // --- Output helpers --- | ||
|
|
||
| // reportError emits the failure on the requested surface: JSON mode prints the | ||
| // {ok:false, error:{type, message}} envelope to stdout and signals the typed | ||
| // error's exit code bare; human mode returns the typed error for the | ||
| // dispatcher to render. | ||
| // {ok:false, error:{type, message, hint?}} envelope to stdout and signals the | ||
| // typed error's exit code bare; human mode returns the typed error for the | ||
| // dispatcher to render. The hint is included only when the typed error carries | ||
| // one, so AI-agent/script consumers reading JSON get the same actionable | ||
| // guidance humans see on stderr. | ||
| func reportError(opts *UpdateOptions, io *cmdutil.IOStreams, errType string, typedErr errs.TypedError) error { | ||
| if opts.JSON { | ||
| output.PrintJson(io.Out, map[string]interface{}{ | ||
| "ok": false, "error": map[string]interface{}{"type": errType, "message": typedErr.ProblemDetail().Message}, | ||
| }) | ||
| errObj := map[string]interface{}{"type": errType, "message": typedErr.ProblemDetail().Message} | ||
| if hint := typedErr.ProblemDetail().Hint; hint != "" { | ||
| errObj["hint"] = hint | ||
| } | ||
| output.PrintJson(io.Out, map[string]interface{}{"ok": false, "error": errObj}) | ||
| return output.ErrBare(output.ExitCodeOf(typedErr)) | ||
| } | ||
| return typedErr | ||
|
|
@@ -207,8 +231,11 @@ | |
| return nil | ||
| } | ||
|
|
||
| func doManualUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string, detect selfupdate.DetectResult, updater *selfupdate.Updater) error { | ||
| skillsResult := runSkillsAndState(updater, io, cur, opts.Force) | ||
| func doManualUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string, detect selfupdate.DetectResult, updater *selfupdate.Updater, suite *skillscheck.SuiteSelection) error { | ||
| skillsResult := runSkillsAndState(updater, io, cur, opts.Force, suite) | ||
| if err := suiteInputError(opts, io, skillsResult); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| reason := detect.ManualReason() | ||
| if opts.JSON { | ||
|
|
@@ -231,7 +258,7 @@ | |
| return nil | ||
| } | ||
|
|
||
| func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string, updater *selfupdate.Updater) error { | ||
| func doNpmUpdate(opts *UpdateOptions, io *cmdutil.IOStreams, cur, latest string, updater *selfupdate.Updater, suite *skillscheck.SuiteSelection) error { | ||
| restore, err := updater.PrepareSelfReplace() | ||
| if err != nil { | ||
| return reportError(opts, io, "update_error", | ||
|
|
@@ -287,7 +314,10 @@ | |
| return output.ErrBare(output.ExitAPI) | ||
| } | ||
|
|
||
| skillsResult := runSkillsAndState(updater, io, latest, opts.Force) | ||
| skillsResult := runSkillsAndState(updater, io, latest, opts.Force, suite) | ||
| if err := suiteInputError(opts, io, skillsResult); err != nil { | ||
| return err | ||
| } | ||
|
Comment on lines
+317
to
+320
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Reject unknown In the npm path, 🤖 Prompt for AI Agents |
||
|
|
||
| if opts.JSON { | ||
| result := map[string]interface{}{ | ||
|
|
@@ -324,8 +354,8 @@ | |
| return fmt.Sprintf("automatic rollback is unavailable on this platform; reinstall manually (skills will not be synced): npm install -g %s@%s && npx skills add larksuite/cli -y -g, or download %s", selfupdate.NpmPackage, latest, releaseURL(latest)) | ||
| } | ||
|
|
||
| func runSkillsAndState(updater *selfupdate.Updater, io *cmdutil.IOStreams, stateVersion string, force bool) *skillscheck.SyncResult { | ||
| if !force { | ||
| func runSkillsAndState(updater *selfupdate.Updater, io *cmdutil.IOStreams, stateVersion string, force bool, suite *skillscheck.SuiteSelection) *skillscheck.SyncResult { | ||
| if !force && suite == nil { | ||
| if existing, ok := skillscheck.ReadSyncedVersion(); ok && normalizeVersion(existing) == normalizeVersion(stateVersion) { | ||
| return nil | ||
| } | ||
|
|
@@ -334,13 +364,25 @@ | |
| Version: stateVersion, | ||
| Force: force, | ||
| Runner: updater, | ||
| Suite: suite, | ||
| }) | ||
| if result.Err != nil && strings.Contains(result.Err.Error(), "state not written") { | ||
| fmt.Fprintf(io.ErrOut, "warning: %v\n", result.Err) | ||
| } | ||
| return result | ||
| } | ||
|
|
||
| // suiteInputError 把 suite 名字非法(InvalidInput)的 sync 结果映射为退出码 2 的 validation 错误。 | ||
| // 返回 nil 表示不是输入错误,调用方继续正常输出流程。 | ||
| func suiteInputError(opts *UpdateOptions, io *cmdutil.IOStreams, r *skillscheck.SyncResult) error { | ||
| if r == nil || !r.InvalidInput { | ||
| return nil | ||
| } | ||
| return reportError(opts, io, "validation", | ||
| errs.NewValidationError(errs.SubtypeInvalidArgument, "%s", r.Err). | ||
| WithHint("use a valid official skill name; nothing was installed")) | ||
| } | ||
|
|
||
| // reportAlreadyUpToDate emits the JSON / pretty output for the | ||
| // already-up-to-date branch, including any skills_action / skills_warning | ||
| // fields derived from skillsResult. When check is true, this is the pure | ||
|
|
@@ -402,6 +444,9 @@ | |
| env["skills_action"] = "synced" | ||
| env["skills_summary"] = skillsSummary(r) | ||
| } | ||
| if r != nil && len(r.Suite) > 0 { | ||
| env["skills_suite"] = r.Suite | ||
| } | ||
| } | ||
|
|
||
| func skillsSummary(r *skillscheck.SyncResult) map[string]interface{} { | ||
|
|
@@ -434,4 +479,7 @@ | |
| fmt.Fprintf(io.ErrOut, " To restore all official skills: lark-cli update --force\n") | ||
| } | ||
| } | ||
| if r != nil && len(r.Suite) > 0 { | ||
| fmt.Fprintf(io.ErrOut, " Suite: %s (run `lark-cli update --skills all` to restore all)\n", strings.Join(r.Suite, ", ")) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Attach
--skillsmetadata and preserve the underlying cause.Both branches are surfacing user-input validation, but they currently drop the flag name and wrapped error. That weakens the typed error contract and loses
errors.Is/errors.Unwrapon the original failure.Suggested fix
As per coding guidelines,
cmd/**/*.gouser flag validation must use.WithParam("--flag"), and**/*.goerrors must preserve underlying failures with.WithCause(err).Also applies to: 381-383
🤖 Prompt for AI Agents
Source: Coding guidelines