mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
206 lines
7.4 KiB
Go
206 lines
7.4 KiB
Go
// Package workflows owns Cobra presentation for the `ao workflows` command
|
|
// family. The module builds its command tree and delegates every direct
|
|
// filesystem/git effect (checkout resolution, target resolution, and the
|
|
// link/unlink sweeps) to internal/workflowsapp, so this package performs no
|
|
// direct effect — mirroring the skills / skillsapp split.
|
|
package workflows
|
|
|
|
import (
|
|
"path/filepath"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/boshu2/agentops/cli/internal/clicontract"
|
|
"github.com/boshu2/agentops/cli/internal/workflowsapp"
|
|
)
|
|
|
|
// Module owns Cobra presentation for the workflows command family.
|
|
type Module struct {
|
|
host clicontract.HostOptions
|
|
|
|
// link / unlink
|
|
linkInto string
|
|
linkJSON bool
|
|
unlinkInto string
|
|
unlinkJSON bool
|
|
}
|
|
|
|
func (m *Module) outputMode() string {
|
|
if m.host.OutputMode == nil {
|
|
return ""
|
|
}
|
|
return m.host.OutputMode()
|
|
}
|
|
|
|
// NewModule constructs the workflows command module from its host seams.
|
|
func NewModule(host clicontract.HostOptions) *Module {
|
|
return &Module{host: host}
|
|
}
|
|
|
|
// Command builds the `ao workflows` command tree.
|
|
func (m *Module) Command() *cobra.Command {
|
|
root := &cobra.Command{
|
|
Use: "workflows",
|
|
Short: "Install the repo's Claude-harness workflow scripts into a project",
|
|
GroupID: "knowledge",
|
|
Long: `Tooling for the top-level workflows/ source-of-truth: the Claude-harness
|
|
workflow scripts (workflows/*.js). Workflows are a CLAUDE-ONLY runtime
|
|
adapter — the same doctrine as skills-codex/ being Codex-only — installed
|
|
into the project-local .claude/workflows/ directory where the Claude Code
|
|
harness resolves named workflows. There is no multi-runtime fan-out.
|
|
|
|
The repo bans tracked symlinks, so installation is this runtime link step,
|
|
not tracked links: run ` + "`ao workflows link`" + ` from inside the agentops checkout
|
|
to install into the current project.`,
|
|
}
|
|
root.AddCommand(m.linkCommand())
|
|
root.AddCommand(m.unlinkCommand())
|
|
return root
|
|
}
|
|
|
|
func (m *Module) linkCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "link",
|
|
Short: "Symlink the checkout's workflows/*.js into a project's .claude/workflows/",
|
|
Long: `Scan the agentops checkout's workflows/ directory and create a symlink in
|
|
the target project's .claude/workflows/ for every workflow script (*.js)
|
|
that has no entry yet. The SOURCE is the checkout the command runs from
|
|
(resolved by walking up from the current directory, fail-closed outside the
|
|
repo); the TARGET is the current working directory's git root joined with
|
|
.claude/workflows/ (created if absent), or the single dir named by --into.
|
|
|
|
Workflows are a Claude-only runtime adapter (skills-codex/ is the Codex
|
|
twin doctrine): exactly one destination, no multi-runtime fan-out.
|
|
|
|
Idempotent and non-destructive: a script already linked to this checkout is
|
|
left alone. A pre-existing REAL file, or a symlink pointing anywhere else,
|
|
is reported as a conflict and never replaced — resolving ownership of an
|
|
operator-owned path is operator judgment. Conflicts do not fail the exit;
|
|
inspect and resolve the named path explicitly.
|
|
|
|
ao workflows link # link into <git-root>/.claude/workflows
|
|
ao workflows link --dry-run # preview without writing
|
|
ao workflows link --dry-run --json # machine-readable preview
|
|
ao workflows link --into /path/to/dir # link into ONE specific dir`,
|
|
Args: cobra.NoArgs,
|
|
RunE: m.runLink,
|
|
}
|
|
cmd.Flags().StringVar(&m.linkInto, "into", "", "Link into this single dir instead of <cwd-git-root>/.claude/workflows")
|
|
cmd.Flags().BoolVar(&m.linkJSON, "json", false, "Emit machine-readable JSON")
|
|
return cmd
|
|
}
|
|
|
|
func (m *Module) runLink(cmd *cobra.Command, _ []string) error {
|
|
srcDir, err := workflowsapp.ResolveRepoWorkflowsDir()
|
|
if err != nil {
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
dest, err := m.resolveTarget(srcDir, m.linkInto)
|
|
if err != nil {
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
|
|
res, err := workflowsapp.LinkWorkflows(srcDir, dest, m.host.DryRun())
|
|
if err != nil {
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
|
|
if m.outputMode() == "yaml" {
|
|
return clicontract.WriteYAML(cmd.OutOrStdout(), res)
|
|
}
|
|
if m.linkJSON || m.outputMode() == "json" {
|
|
return clicontract.WriteJSON(cmd.OutOrStdout(), res)
|
|
}
|
|
workflowsapp.RenderLinkResult(cmd.OutOrStdout(), res)
|
|
return nil
|
|
}
|
|
|
|
func (m *Module) unlinkCommand() *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "unlink",
|
|
Short: "Remove the workflow symlinks that `workflows link` minted",
|
|
Long: `The clean uninstall inverse of ` + "`ao workflows link`" + `. Sweep the target
|
|
project's .claude/workflows/ (or the single dir named by --into) and remove
|
|
EXACTLY the symlinks that link minted — those whose target resolves into
|
|
THIS checkout's workflows/ tree. A foreign symlink pointing elsewhere and a
|
|
real file are both reported as foreign and never removed. A stale link to a
|
|
workflow since removed from the checkout is still cleaned up.
|
|
|
|
Must be run from inside the agentops checkout — it needs the checkout's
|
|
workflows/ path to know which links are its own.
|
|
|
|
ao workflows unlink # remove owned links from <git-root>/.claude/workflows
|
|
ao workflows unlink --dry-run # preview without removing
|
|
ao workflows unlink --into /path/to/dir # sweep ONE specific dir`,
|
|
Args: cobra.NoArgs,
|
|
RunE: m.runUnlink,
|
|
}
|
|
cmd.Flags().StringVar(&m.unlinkInto, "into", "", "Sweep this single dir instead of <cwd-git-root>/.claude/workflows")
|
|
cmd.Flags().BoolVar(&m.unlinkJSON, "json", false, "Emit machine-readable JSON")
|
|
return cmd
|
|
}
|
|
|
|
func (m *Module) runUnlink(cmd *cobra.Command, _ []string) error {
|
|
srcDir, err := workflowsapp.ResolveRepoWorkflowsDir()
|
|
if err != nil {
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
dest, err := m.resolveTarget(srcDir, m.unlinkInto)
|
|
if err != nil {
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
|
|
res, err := workflowsapp.UnlinkWorkflows(srcDir, dest, m.host.DryRun())
|
|
if err != nil {
|
|
cmd.SilenceUsage = true
|
|
return err
|
|
}
|
|
|
|
if m.outputMode() == "yaml" {
|
|
return clicontract.WriteYAML(cmd.OutOrStdout(), res)
|
|
}
|
|
if m.unlinkJSON || m.outputMode() == "json" {
|
|
return clicontract.WriteJSON(cmd.OutOrStdout(), res)
|
|
}
|
|
workflowsapp.RenderUnlinkResult(cmd.OutOrStdout(), res)
|
|
return nil
|
|
}
|
|
|
|
// resolveTarget avoids launching Git during a dry-run. ResolveRepoWorkflowsDir
|
|
// already proved that cwd is inside the canonical agentops checkout, so its
|
|
// parent is the same git root ResolveTargetDir would return. An explicit target
|
|
// never needs discovery.
|
|
func (m *Module) resolveTarget(srcDir, explicit string) (string, error) {
|
|
if m.host.DryRun != nil && m.host.DryRun() && explicit == "" {
|
|
root, err := filepath.EvalSymlinks(filepath.Dir(srcDir))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Join(root, ".claude", "workflows"), nil
|
|
}
|
|
return workflowsapp.ResolveTargetDir(explicit)
|
|
}
|
|
|
|
// Contract documents the workflows family's effect and profile shape for the
|
|
// family architecture gate, mirroring the skills module: the family does not
|
|
// attach a capabilities contract, so the composition does not attach this one
|
|
// either.
|
|
func (*Module) Contract() clicontract.CommandContract {
|
|
return clicontract.CommandContract{
|
|
ID: "ao.workflows",
|
|
Profiles: clicontract.ProfileDefault | clicontract.ProfileFlywheel | clicontract.ProfileLegacy | clicontract.ProfileCombined,
|
|
Args: clicontract.ArgsPolicy{Name: "arbitrary", Validate: cobra.ArbitraryArgs},
|
|
Output: clicontract.OutputText,
|
|
Effects: clicontract.EffectFilesystem,
|
|
ExitClasses: map[int]clicontract.ExitClass{
|
|
0: clicontract.ExitSuccess,
|
|
1: clicontract.ExitFailure,
|
|
},
|
|
}
|
|
}
|