Files
wehub-resource-sync bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:22:54 +08:00

337 lines
11 KiB
Go

// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT
package service
import (
"encoding/json"
"fmt"
"io/fs"
"strings"
"github.com/larksuite/cli/internal/affordance"
"github.com/larksuite/cli/internal/cmdmeta"
"github.com/larksuite/cli/internal/cmdutil"
"github.com/larksuite/cli/internal/meta"
"github.com/spf13/cobra"
)
// PrepareDomainHelp appends navigational guidance (routing line, risk legend,
// skill pointer) to a top-level Lark domain's description, returning false for
// anything that is not such a domain. Built lazily at help time because
// shortcuts attach after service registration. skillFS (nil-safe) gates the
// skill pointer.
//
// A hand-authored Long is preserved as the base (e.g. event's "Use 'event
// consume <EventKey>'…"); service domains carry only a Short at this point, so
// we fall back to it. The pristine base is captured once into an annotation so
// re-rendering does not append the guidance twice.
func PrepareDomainHelp(cmd *cobra.Command, skillFS fs.FS) bool {
if cmd.Annotations[schemaPathAnnotation] != "" {
return false // a method command
}
// Direct child of root only — so Domain() reads this command's own tag, and
// nested resource groups are excluded.
if cmd.Parent() == nil || cmd.Parent().Parent() != nil {
return false
}
// A domain is service-sourced or shortcut-tagged; CLI tooling has neither.
if src, _ := cmdmeta.SourceOf(cmd); src != cmdmeta.SourceService && cmdmeta.Domain(cmd) == "" {
return false
}
if !cmd.HasAvailableSubCommands() {
return false
}
hasShortcuts, hasResources := false, false
for _, c := range cmd.Commands() {
if c.Hidden || c.Name() == "help" || c.Name() == "completion" {
continue
}
if strings.HasPrefix(c.Name(), "+") {
hasShortcuts = true
} else {
hasResources = true
}
}
var b strings.Builder
b.WriteString(domainHelpBase(cmd))
if hasShortcuts && hasResources { // routing only matters when both styles exist
b.WriteString("\n\nPrefer a +-prefixed shortcut when one matches your task; otherwise use the raw API resource below.")
}
b.WriteString("\n\nRisk levels (read | write | high-risk-write) appear in each command's --help; high-risk-write requires --yes, only after the user confirms.")
if skill := "lark-" + cmd.Name(); skillFS != nil {
if _, err := fs.Stat(skillFS, skill+"/SKILL.md"); err == nil {
fmt.Fprintf(&b, "\n\nDomain guide (concepts, command choice, conventions): lark-cli skills read %s", skill)
}
}
cmd.Long = b.String()
return true
}
// domainHelpBase returns the description to seed domain help with — the
// hand-authored Long when present, else the Short.
func domainHelpBase(cmd *cobra.Command) string {
return captureHelpBase(cmd, domainBaseAnnotation)
}
// captureHelpBase records a command's pristine lead text once — its
// hand-authored Long, or Short when Long is empty — into the given annotation,
// so lazy re-renders compose onto the original text instead of onto an
// already-augmented Long. This is what lets a shortcut's PostMount-authored
// Long survive: it becomes the base the affordance block is appended below.
func captureHelpBase(cmd *cobra.Command, key string) string {
if base, ok := cmd.Annotations[key]; ok {
return base
}
base := cmd.Long
if base == "" {
base = cmd.Short
}
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmd.Annotations[key] = base
return base
}
// methodLong is the build-time Long (description + schema pointer +
// params-only addendum). Agent guidance is added lazily by PrepareMethodHelp,
// so command construction never parses the overlay.
func methodLong(description, schemaPath, paramsOnly string) string {
var b strings.Builder
b.WriteString(description)
fmt.Fprintf(&b, "\n\nFull parameter schema:\n lark-cli schema %s", schemaPath)
b.WriteString(paramsOnly)
return b.String()
}
// Annotation keys PrepareMethodHelp reads to rebuild a method command's Long.
// The affordance overlay coordinates live in cmdmeta (shared with shortcuts).
const (
schemaPathAnnotation = "method-schema-path"
paramsOnlyAnnotation = "method-params-only"
domainBaseAnnotation = "affordance-domain-base"
shortcutBaseAnnotation = "affordance-shortcut-base"
)
// setMethodHelpData records the coordinates PrepareMethodHelp needs (storing a
// few strings is the only build-time cost; the overlay stays untouched).
func setMethodHelpData(cmd *cobra.Command, service, methodID, schemaPath, paramsOnly string) {
if cmd.Annotations == nil {
cmd.Annotations = map[string]string{}
}
cmdmeta.SetAffordanceRef(cmd, service, methodID)
cmd.Annotations[schemaPathAnnotation] = schemaPath
if paramsOnly != "" {
cmd.Annotations[paramsOnlyAnnotation] = paramsOnly
}
}
// PrepareMethodHelp rebuilds a generated method command's Long with the agent
// guidance at the TOP (Risk, then the affordance block, then the schema
// pointer), returning false for non-method commands. The overlay is parsed
// here — only when help is rendered. skillFS (nil-safe) gates the related-skill
// pointers: each is emitted only when it resolves in the skill tree (see
// affordance.SkillStatPath), so a typo or a build without embedded skills never
// prints a `skills read` that cannot be opened.
func PrepareMethodHelp(cmd *cobra.Command, skillFS fs.FS) bool {
ann := cmd.Annotations
if ann == nil {
return false
}
schemaPath, ok := ann[schemaPathAnnotation]
if !ok {
return false
}
var b strings.Builder
b.WriteString(cmd.Short)
writeRisk(&b, cmd)
var skills []string
if raw, ok := affordanceRaw(cmd); ok {
if a, ok := (meta.Method{Affordance: raw}).ParsedAffordance(); ok {
if block := renderAffordanceValue(a); block != "" {
b.WriteString("\n\n")
b.WriteString(block)
}
skills = a.Skills
}
}
fmt.Fprintf(&b, "\n\nFull parameter schema:\n lark-cli schema %s", schemaPath)
b.WriteString(ann[paramsOnlyAnnotation])
writeRelatedSkills(&b, skills, skillFS)
cmd.Long = b.String()
return true
}
// PrepareShortcutHelp composes a +-prefixed shortcut's Long from its affordance
// overlay — the same top layout as method help (description, Risk, guidance
// block, related skills) minus the schema pointer, which shortcuts have none
// of. Returns false when the command is not a shortcut or carries no overlay
// entry, so shortcuts without guidance keep the default help plus the bottom
// risk/tips append.
//
// The lead is the command's pristine base (captureHelpBase): a shortcut that
// set a hand-authored Long in PostMount (e.g. the docs shortcuts' "agents MUST
// read the skill" directive) keeps it — the affordance block is appended below,
// never clobbering it.
//
// Tips precedence (intentional, not a bug): the overlay's ### Tips win. The
// shortcut's declarative Tips (the Go Tips field) are only a fallback used when
// the overlay declares none; when the overlay has tips, the Go tips are dropped
// (replaced, not merged) so tips never render twice. Authoring a ### Tips block
// therefore silently retires that shortcut's Go Tips — consolidate into one.
func PrepareShortcutHelp(cmd *cobra.Command, skillFS fs.FS) bool {
if src, _ := cmdmeta.SourceOf(cmd); src != cmdmeta.SourceShortcut {
return false
}
raw, ok := affordanceRaw(cmd)
if !ok {
return false
}
a, ok := (meta.Method{Affordance: raw}).ParsedAffordance()
if !ok {
return false
}
if len(a.Tips) == 0 {
a.Tips = cmdutil.GetTips(cmd)
}
var b strings.Builder
b.WriteString(captureHelpBase(cmd, shortcutBaseAnnotation))
writeRisk(&b, cmd)
if block := renderAffordanceValue(a); block != "" {
b.WriteString("\n\n")
b.WriteString(block)
}
writeRelatedSkills(&b, a.Skills, skillFS)
cmd.Long = b.String()
return true
}
// writeRisk appends the "Risk: <level>" line, warning agents not to self-approve
// high-risk-write commands. A no-op when the command has no risk annotation.
func writeRisk(b *strings.Builder, cmd *cobra.Command) {
level, ok := cmdutil.GetRisk(cmd)
if !ok {
return
}
// --yes asserts the USER confirmed; the agent must not self-approve.
if level == cmdutil.RiskHighRiskWrite {
fmt.Fprintf(b, "\n\nRisk: %s (requires explicit user confirmation to execute; the agent must NOT add --yes on its own — only pass --yes after the user has confirmed)", level)
} else {
fmt.Fprintf(b, "\n\nRisk: %s", level)
}
}
// writeRelatedSkills appends the "Related skills" block for the entries that
// exist in skillFS. Nothing is written when skillFS is nil or no entry resolves,
// so help never prints a `skills read` pointer that cannot be opened.
func writeRelatedSkills(b *strings.Builder, skills []string, skillFS fs.FS) {
if skillFS == nil || len(skills) == 0 {
return
}
var avail []string
for _, s := range skills {
if _, err := fs.Stat(skillFS, affordance.SkillStatPath(s)); err == nil {
avail = append(avail, s)
}
}
if len(avail) == 0 {
return
}
b.WriteString("\n\nRelated skills (read for end-to-end usage):")
for _, s := range avail {
fmt.Fprintf(b, "\n lark-cli skills read %s", s)
}
}
// affordanceLookup is the overlay source; a package var so tests can inject.
var affordanceLookup = affordance.For
// RenderAffordanceForCmd renders a method command's affordance block, or "" when
// it carries none.
func RenderAffordanceForCmd(cmd *cobra.Command) string {
raw, ok := affordanceRaw(cmd)
if !ok {
return ""
}
return renderAffordance(meta.Method{Affordance: raw})
}
func affordanceRaw(cmd *cobra.Command) (json.RawMessage, bool) {
service, methodID, ok := cmdmeta.AffordanceRef(cmd)
if !ok {
return nil, false
}
return affordanceLookup(service, methodID)
}
// renderAffordance renders a method's affordance as a help block, or "" when it
// has none. Sections are joined with blank lines so they scan as distinct groups.
func renderAffordance(m meta.Method) string {
a, ok := m.ParsedAffordance()
if !ok {
return ""
}
return renderAffordanceValue(a)
}
// renderAffordanceValue renders an already-parsed affordance. Split from
// renderAffordance so callers can render a value they have adjusted first (e.g.
// a shortcut folding its declarative tips into an overlay that has none).
func renderAffordanceValue(a meta.Affordance) string {
var sections []string
bullets := func(title string, items []string) {
var nonEmpty []string
for _, it := range items {
if strings.TrimSpace(it) != "" {
nonEmpty = append(nonEmpty, it)
}
}
if len(nonEmpty) == 0 {
return
}
var s strings.Builder
fmt.Fprintf(&s, "%s:\n", title)
for _, it := range nonEmpty {
fmt.Fprintf(&s, " • %s\n", it)
}
sections = append(sections, strings.TrimRight(s.String(), "\n"))
}
bullets("When to use", a.UseWhen)
bullets("Avoid when", a.AvoidWhen)
bullets("Prerequisites", a.Prerequisites)
bullets("Tips", a.Tips)
if len(a.Examples) > 0 {
var lines []string
for _, ex := range a.Examples {
if ex.Command == "" {
continue
}
if ex.Description != "" {
lines = append(lines, fmt.Sprintf(" • %s\n %s", ex.Description, ex.Command))
} else {
lines = append(lines, fmt.Sprintf(" • %s", ex.Command))
}
}
if len(lines) > 0 {
sections = append(sections, "Examples:\n"+strings.Join(lines, "\n"))
}
}
for _, ext := range a.Extensions {
bullets(ext.Label, ext.Items)
}
bullets("Related", a.Related)
return strings.Join(sections, "\n\n")
}