mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 18:13:01 +08:00
Scaffold the lark-cli sec subsystem: the `sec` command tree (install, run, stop, status, config init) and the internal/sec package that drives it. The bootstrap manifest is embedded at build time as JSON, mapping (platform, arch, region) to download URLs. The installer resolves the right artifact for the current host, downloads with optional SHA256 verification, extracts into versions/<version>/, swaps the `current` symlink atomically (copy on Windows), and writes state.json. `sec run` enables the binary as a user-level system service (launchd / systemd-user / registry+VBS) so the OS supervises restarts. After this first install, lark-sec-cli takes over its own upgrade lifecycle.
180 lines
6.0 KiB
Go
180 lines
6.0 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
|
|
"github.com/larksuite/cli/cmd/api"
|
|
"github.com/larksuite/cli/cmd/auth"
|
|
"github.com/larksuite/cli/cmd/completion"
|
|
cmdconfig "github.com/larksuite/cli/cmd/config"
|
|
"github.com/larksuite/cli/cmd/doctor"
|
|
cmdevent "github.com/larksuite/cli/cmd/event"
|
|
"github.com/larksuite/cli/cmd/profile"
|
|
"github.com/larksuite/cli/cmd/schema"
|
|
"github.com/larksuite/cli/cmd/sec"
|
|
"github.com/larksuite/cli/cmd/service"
|
|
cmdupdate "github.com/larksuite/cli/cmd/update"
|
|
_ "github.com/larksuite/cli/events"
|
|
"github.com/larksuite/cli/internal/build"
|
|
"github.com/larksuite/cli/internal/cmdpolicy"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/hook"
|
|
"github.com/larksuite/cli/internal/keychain"
|
|
"github.com/larksuite/cli/shortcuts"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// BuildOption configures optional aspects of the command tree construction.
|
|
type BuildOption func(*buildConfig)
|
|
|
|
type buildConfig struct {
|
|
streams *cmdutil.IOStreams
|
|
keychain keychain.KeychainAccess
|
|
globals GlobalOptions
|
|
}
|
|
|
|
// WithIO sets the IO streams for the CLI by wrapping raw reader/writers.
|
|
// Terminal detection is delegated to cmdutil.NewIOStreams.
|
|
func WithIO(in io.Reader, out, errOut io.Writer) BuildOption {
|
|
return func(c *buildConfig) {
|
|
c.streams = cmdutil.NewIOStreams(in, out, errOut)
|
|
}
|
|
}
|
|
|
|
// WithKeychain sets the secret storage backend. If not provided, the platform keychain is used.
|
|
func WithKeychain(kc keychain.KeychainAccess) BuildOption {
|
|
return func(c *buildConfig) {
|
|
c.keychain = kc
|
|
}
|
|
}
|
|
|
|
// HideProfile sets the visibility policy for the root-level --profile flag.
|
|
// When hide is true the flag stays registered (so existing invocations still
|
|
// parse) but is omitted from help and shell completion. Typically called as
|
|
// HideProfile(isSingleAppMode()).
|
|
func HideProfile(hide bool) BuildOption {
|
|
return func(c *buildConfig) {
|
|
c.globals.HideProfile = hide
|
|
}
|
|
}
|
|
|
|
// Build constructs the full command tree. It also installs registered
|
|
// plugins and emits the Startup lifecycle event during assembly --
|
|
// so Plugin.On(Startup) handlers run even if the returned command is
|
|
// never dispatched. The matching Shutdown event is only emitted by
|
|
// Execute; callers that bypass Execute will not see Shutdown fire.
|
|
//
|
|
// Returns only the cobra.Command; Factory and hook Registry are internal.
|
|
// Use Execute for the standard production entry point.
|
|
func Build(ctx context.Context, inv cmdutil.InvocationContext, opts ...BuildOption) *cobra.Command {
|
|
_, rootCmd, _ := buildInternal(ctx, inv, opts...)
|
|
return rootCmd
|
|
}
|
|
|
|
// buildInternal is a pure assembly function: it wires the command tree from
|
|
// inv and BuildOptions alone. Any state-dependent decision (disk, network,
|
|
// env) belongs in the caller and must be threaded in via BuildOption.
|
|
//
|
|
// Returns (factory, rootCmd, registry). The registry is nil when plugin
|
|
// install failed (FailClosed guard installed) or when no plugin produced
|
|
// hooks; callers that wire Shutdown emit must nil-check before calling
|
|
// hook.Emit.
|
|
func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...BuildOption) (*cmdutil.Factory, *cobra.Command, *hook.Registry) {
|
|
// cfg.globals.Profile is left zero here; it's bound to the --profile
|
|
// flag in RegisterGlobalFlags and filled by cobra's parse step.
|
|
cfg := &buildConfig{}
|
|
for _, o := range opts {
|
|
if o != nil {
|
|
o(cfg)
|
|
}
|
|
}
|
|
// Default streams when WithIO is not supplied so the root command's
|
|
// SetIn/Out/Err calls below don't deref nil. NewDefault also normalizes
|
|
// partial streams internally; keep both in sync so cfg.streams reflects
|
|
// the same values the Factory ends up using.
|
|
if cfg.streams == nil {
|
|
cfg.streams = cmdutil.SystemIO()
|
|
}
|
|
|
|
f := cmdutil.NewDefault(cfg.streams, inv)
|
|
if cfg.keychain != nil {
|
|
f.Keychain = cfg.keychain
|
|
}
|
|
rootCmd := &cobra.Command{
|
|
Use: "lark-cli",
|
|
Short: "Lark/Feishu CLI — OAuth authorization, UAT management, API calls",
|
|
Long: rootLong,
|
|
Version: build.Version,
|
|
}
|
|
|
|
rootCmd.SetContext(ctx)
|
|
rootCmd.SetIn(cfg.streams.In)
|
|
rootCmd.SetOut(cfg.streams.Out)
|
|
rootCmd.SetErr(cfg.streams.ErrOut)
|
|
|
|
installTipsHelpFunc(rootCmd)
|
|
rootCmd.SilenceErrors = true
|
|
|
|
RegisterGlobalFlags(rootCmd.PersistentFlags(), &cfg.globals)
|
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
|
cmd.SilenceUsage = true
|
|
f.CurrentCommand = cmd
|
|
}
|
|
|
|
rootCmd.AddCommand(cmdconfig.NewCmdConfig(f))
|
|
rootCmd.AddCommand(auth.NewCmdAuth(f))
|
|
rootCmd.AddCommand(profile.NewCmdProfile(f))
|
|
rootCmd.AddCommand(doctor.NewCmdDoctor(f))
|
|
rootCmd.AddCommand(api.NewCmdApiWithContext(ctx, f, nil))
|
|
rootCmd.AddCommand(schema.NewCmdSchema(f, nil))
|
|
rootCmd.AddCommand(completion.NewCmdCompletion(f))
|
|
rootCmd.AddCommand(cmdupdate.NewCmdUpdate(f))
|
|
rootCmd.AddCommand(cmdevent.NewCmdEvents(f))
|
|
rootCmd.AddCommand(sec.NewCmdSec(f))
|
|
service.RegisterServiceCommandsWithContext(ctx, rootCmd, f)
|
|
shortcuts.RegisterShortcutsWithContext(ctx, rootCmd, f)
|
|
|
|
installUnknownSubcommandGuard(rootCmd)
|
|
|
|
if mode := f.ResolveStrictMode(ctx); mode.IsActive() {
|
|
pruneForStrictMode(rootCmd, mode)
|
|
}
|
|
|
|
installResult, installErr := installPluginsAndHooks(cfg.streams.ErrOut)
|
|
if installErr != nil {
|
|
installPluginInstallErrorGuard(rootCmd, installErr)
|
|
return f, rootCmd, nil
|
|
}
|
|
var pluginRules []cmdpolicy.PluginRule
|
|
var registry *hook.Registry
|
|
if installResult != nil {
|
|
pluginRules = installResult.PluginRules
|
|
registry = installResult.Registry
|
|
}
|
|
|
|
// Policy errors fail-CLOSED when a plugin contributed (security
|
|
// intent must not be silently dropped); yaml-only errors fail-OPEN
|
|
// with a warning so a typo can't lock the user out.
|
|
if err := applyUserPolicyPruning(rootCmd, pluginRules); err != nil {
|
|
if len(pluginRules) > 0 {
|
|
installPluginConflictGuard(rootCmd, err)
|
|
return f, rootCmd, nil
|
|
}
|
|
warnPolicyError(cfg.streams.ErrOut, err)
|
|
}
|
|
|
|
if registry != nil {
|
|
if err := wireHooks(ctx, rootCmd, registry); err != nil {
|
|
installPluginLifecycleErrorGuard(rootCmd, err)
|
|
return f, rootCmd, nil
|
|
}
|
|
}
|
|
|
|
recordInventory(installResult)
|
|
return f, rootCmd, registry
|
|
}
|