Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ dhq completion bash | zsh | fish | powershell
dhq doctor (health check)
dhq update (self-update to latest version)
dhq skills list | install (auto-detect AI agents and install the DeployHQ skill)
dhq setup claude | codex | cursor | windsurf (install agent plugins, --project for project-level)
dhq setup claude | codex | cursor | windsurf (deprecated — use 'dhq skills')
dhq mcp (start MCP server in stdio mode)
```

Expand Down Expand Up @@ -318,9 +318,9 @@ bare `dhq skills install`; **project-scope** agents write into the current
repository and require an explicit `--agent` flag so login never mutates a repo
as a side effect.

`dhq setup <agent>` is a narrower alternative that installs the agent plugin for
a single tool (Claude Code, Codex, Cursor, or Windsurf), with `--project` for
project-level installs.
> **Deprecated:** `dhq setup <agent>` (Claude Code, Codex, Cursor, Windsurf) is
> the older, narrower predecessor of `dhq skills install`. It still works but
> warns on use and will be removed in a future release — prefer `dhq skills`.

### Other agent helpers

Expand Down
57 changes: 50 additions & 7 deletions internal/commands/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type agentSetup struct {
Use string
Short string
Name string
SkillsName string // equivalent target name for `dhq skills install --agent`
PathFor func(scope) (string, error)
Content func() []byte
StrategyFor func(scope) writeStrategy
Expand All @@ -59,6 +60,7 @@ var agents = []agentSetup{
Use: "claude",
Short: "Install Claude Code skill",
Name: "Claude Code",
SkillsName: "claude-code",
PathFor: pathClaude,
Content: func() []byte { return []byte(skillFrontmatter + skillBody) },
StrategyFor: always(strategyOverwrite),
Expand All @@ -67,6 +69,7 @@ var agents = []agentSetup{
Use: "codex",
Short: "Install OpenAI Codex AGENTS.md section",
Name: "Codex",
SkillsName: "codex",
PathFor: pathCodex,
Content: func() []byte { return []byte(skillBody) },
StrategyFor: always(strategyMarkedBlock),
Expand All @@ -75,15 +78,17 @@ var agents = []agentSetup{
Use: "cursor",
Short: "Install Cursor project rule",
Name: "Cursor",
SkillsName: "cursor",
PathFor: pathCursor,
Content: func() []byte { return []byte(cursorFrontmatter + skillBody) },
StrategyFor: always(strategyOverwrite),
},
{
Use: "windsurf",
Short: "Install Windsurf integration",
Name: "Windsurf",
PathFor: pathWindsurf,
Use: "windsurf",
Short: "Install Windsurf integration",
Name: "Windsurf",
SkillsName: "windsurf",
PathFor: pathWindsurf,
Content: func() []byte { return []byte(skillBody) },
// User-level writes into the shared ~/.codeium/.../global_rules.md, so we
// must merge with a marker block. Project-level writes a dedicated file we own.
Expand All @@ -99,8 +104,12 @@ var agents = []agentSetup{
func newSetupCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "setup",
Short: "Install agent plugins",
Long: "Install DeployHQ agent integration files for AI coding assistants.",
Short: "Install agent plugins (deprecated — use 'dhq skills install')",
Long: "Install DeployHQ agent integration files for AI coding assistants.\n\n" +
"DEPRECATED: 'dhq setup' is superseded by 'dhq skills install', which\n" +
"auto-detects installed agents and supports 12 of them (vs the 4 here).\n" +
"This command still works but will be removed in a future release.\n" +
"Migrate with 'dhq skills install' (or 'dhq skills install --agent <name>').",
}
for _, a := range agents {
cmd.AddCommand(newAgentSetupCmd(a))
Expand All @@ -118,6 +127,14 @@ func newAgentSetupCmd(a agentSetup) *cobra.Command {
Long: longHelp(a),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
env := cliCtx.Envelope
// 'dhq setup' is deprecated in favour of 'dhq skills'. Warn on every
// use (stderr, so JSON/data on stdout is unaffected). The note is
// mode-aware: 'dhq skills' has no uninstall and installs at each
// agent's own default scope, so we don't claim a like-for-like
// replacement for the --uninstall or --project paths.
env.Warn("%s", setupDeprecationNote(a, uninstall, project))

sc := scopeUser
if project {
sc = scopeProject
Expand All @@ -133,7 +150,6 @@ func newAgentSetupCmd(a agentSetup) *cobra.Command {
return &output.InternalError{Message: "resolve install path", Cause: err}
}

env := cliCtx.Envelope
strategy := a.StrategyFor(sc)
if uninstall {
return runUninstall(env, a, path, strategy)
Expand All @@ -148,12 +164,39 @@ func newAgentSetupCmd(a agentSetup) *cobra.Command {
return cmd
}

// setupDeprecationNote builds the per-run deprecation warning for `dhq setup`.
// It is mode-aware because the successor command isn't a like-for-like drop-in:
// - `dhq skills` has no uninstall, so the --uninstall path keeps using setup;
// - `dhq skills install --agent <name>` installs at that agent's own default
// scope (e.g. Cursor is user-scope there, project-scope here), so we don't
// promise scope equivalence on the --project path.
func setupDeprecationNote(a agentSetup, uninstall, project bool) string {
switch {
case uninstall:
return fmt.Sprintf(
"'dhq setup' is deprecated and will be removed in a future release. "+
"'dhq skills' has no uninstall yet, so 'dhq setup %s --uninstall' "+
"remains the way to remove this integration for now.", a.Use)
case project:
return fmt.Sprintf(
"'dhq setup' is deprecated; prefer 'dhq skills install --agent %s'. "+
"Note 'dhq skills' installs at that agent's default scope rather than "+
"project-local, and 'dhq setup' will be removed in a future release.", a.SkillsName)
default:
return fmt.Sprintf(
"'dhq setup' is deprecated; use 'dhq skills install --agent %s' instead "+
"(auto-detects agents and supports 12 of them). 'dhq setup' will be "+
"removed in a future release.", a.SkillsName)
}
}

func longHelp(a agentSetup) string {
userPath, userErr := a.PathFor(scopeUser)
projPath, _ := a.PathFor(scopeProject)

var b strings.Builder
fmt.Fprintf(&b, "Install %s integration files.\n\n", a.Name)
fmt.Fprintf(&b, "DEPRECATED: use 'dhq skills install --agent %s' instead.\n\n", a.SkillsName)
if userErr == nil {
fmt.Fprintf(&b, "Default (user-global): %s\n", userPath)
} else {
Expand Down
97 changes: 97 additions & 0 deletions internal/commands/setup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package commands

import (
"bytes"
"io"
"strings"
"testing"

"github.com/deployhq/deployhq-cli/internal/cli"
"github.com/deployhq/deployhq-cli/internal/output"
"github.com/deployhq/deployhq-cli/internal/skillinstaller"
)

// TestSetupSkillsNamesResolveToTargets locks the setup→skills agent-name
// mapping: every SkillsName the deprecation note points at must resolve to a
// real registered skills target, or the migration hint sends users to a
// nonexistent --agent value.
func TestSetupSkillsNamesResolveToTargets(t *testing.T) {
for _, a := range agents {
if a.SkillsName == "" {
t.Errorf("agent %q has no SkillsName for the deprecation hint", a.Use)
continue
}
if skillinstaller.Find(a.SkillsName) == nil {
t.Errorf("agent %q maps to SkillsName %q, which is not a registered 'dhq skills' target",
a.Use, a.SkillsName)
}
}
}

// TestSetupDeprecationNote verifies the note is mode-aware: it must not tell
// uninstall users to run an install command, and must not promise scope
// equivalence on the --project path.
func TestSetupDeprecationNote(t *testing.T) {
a := agentSetup{Use: "claude", SkillsName: "claude-code"}

t.Run("install", func(t *testing.T) {
note := setupDeprecationNote(a, false, false)
mustContain(t, note, "deprecated")
mustContain(t, note, "dhq skills install --agent claude-code")
})

t.Run("project", func(t *testing.T) {
note := setupDeprecationNote(a, false, true)
mustContain(t, note, "claude-code")
// Must flag that skills uses its own default scope, not project-local.
mustContain(t, note, "default scope")
})

t.Run("uninstall", func(t *testing.T) {
note := setupDeprecationNote(a, true, false)
mustContain(t, note, "no uninstall")
mustContain(t, note, "dhq setup claude --uninstall")
// Must NOT point uninstall users at an install command.
if strings.Contains(note, "install --agent") {
t.Errorf("uninstall note should not suggest an install command: %q", note)
}
})
}

// TestSetupCommand_WarnsOnStderrNotStdout drives a real setup subcommand and
// asserts the deprecation warning lands on stderr while stdout stays clean —
// the load-bearing output-contract property of the deprecation.
func TestSetupCommand_WarnsOnStderrNotStdout(t *testing.T) {
t.Setenv("HOME", t.TempDir()) // isolate from the real ~/.claude

var stdout, stderr bytes.Buffer
origCtx := cliCtx
t.Cleanup(func() { cliCtx = origCtx })
cliCtx = &cli.Context{
Envelope: &output.Envelope{Stdout: &stdout, Stderr: &stderr, Logger: output.NewLogger()},
}

root := newSetupCmd()
// --uninstall on a fresh HOME removes nothing (no files written), so this
// exercises the warning path without filesystem side effects.
root.SetArgs([]string{"claude", "--uninstall"})
root.SetOut(io.Discard)
root.SetErr(io.Discard)
if err := root.Execute(); err != nil {
t.Fatalf("execute: %v", err)
}

if !strings.Contains(stderr.String(), "deprecated") {
t.Errorf("deprecation warning missing from stderr: %q", stderr.String())
}
if strings.TrimSpace(stdout.String()) != "" {
t.Errorf("stdout must stay clean (data channel), got: %q", stdout.String())
}
}

func mustContain(t *testing.T, s, sub string) {
t.Helper()
if !strings.Contains(s, sub) {
t.Errorf("expected %q to contain %q", s, sub)
}
}
48 changes: 24 additions & 24 deletions skills/deployhq/references/auth-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,39 +110,39 @@ account = "mycompany"

## Agent Setup

### `dhq setup claude`
Install Claude Code integration files.
### `dhq skills` (preferred)

| Flag | Description |
|------|-------------|
| `--project` | Install to project directory (`.claude/`) instead of user (`~/.claude/`) |
| `--uninstall` | Remove integration files |

```bash
dhq setup claude
dhq setup claude --project
dhq setup claude --uninstall
```

### `dhq setup codex`
Install OpenAI Codex integration.
Install the DeployHQ skill into the AI coding agents on this machine. `dhq skills`
auto-detects installed agents and supports 12 of them (Aider, Antigravity, Claude
Code, Cline, Codex CLI, Continue.dev, Cursor, Gemini CLI, GitHub Copilot, Kiro CLI,
OpenCode, Windsurf).

```bash
dhq setup codex
dhq skills list # detected agents + skill status
dhq skills install # install for detected user-scope agents
dhq skills install --agent claude-code # install for a specific agent
dhq skills install --agent copilot # project-scope agents are opt-in via --agent
```

### `dhq setup cursor`
Install Cursor integration.
User-scope agents install into your home directory and are covered by the bare
`dhq skills install`; project-scope agents write into the current repository and
require an explicit `--agent` flag.

```bash
dhq setup cursor
```
### `dhq setup` (deprecated)

### `dhq setup windsurf`
Install Windsurf integration.
> **Deprecated:** `dhq setup claude|codex|cursor|windsurf` is the older, narrower
> predecessor of `dhq skills install`. It still works but warns on use and will be
> removed in a future release. Prefer `dhq skills`. Two caveats when migrating:
> `dhq skills` has no uninstall yet (use `dhq setup <agent> --uninstall` to remove),
> and it installs at each agent's own default scope, which may differ from
> `dhq setup --project`.

```bash
dhq setup windsurf
dhq setup claude # ~ dhq skills install --agent claude-code
dhq setup codex # ~ dhq skills install --agent codex
dhq setup cursor # ~ dhq skills install --agent cursor
dhq setup windsurf # ~ dhq skills install --agent windsurf
dhq setup claude --uninstall # remove (no dhq skills equivalent yet)
```

### `dhq mcp`
Expand Down
Loading