Skip to content

Commit a6751a7

Browse files
committed
feat: ai skills post-install-hook init, fastInstall implemented
1 parent ef3b043 commit a6751a7

3 files changed

Lines changed: 237 additions & 0 deletions

File tree

install.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ execute() {
5757
log_info "installed ${BINDIR}/${binexe}"
5858
done
5959
rm -rf "${tmpdir}"
60+
"${BINDIR}/auth0" ai skills post-install-hook || true
6061
}
6162
get_binaries() {
6263
case "$PLATFORM" in

internal/cli/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ func buildRootCmd(cli *cli) *cobra.Command {
8888
prepareInteractivity(cmd)
8989
cli.configureRenderer()
9090

91+
if cmd.CommandPath() != "auth0 ai skills post-install-hook" && !skillsSentinelExists() {
92+
fmt.Fprintln(os.Stdout, skillsInstallTip)
93+
writeSkillsSentinel()
94+
}
95+
9196
if !commandRequiresAuthentication(cmd.CommandPath()) {
9297
return nil
9398
}
@@ -121,6 +126,7 @@ func commandRequiresAuthentication(invokedCommandName string) bool {
121126
"auth0 logout",
122127
"auth0 tenants use",
123128
"auth0 tenants list",
129+
"auth0 ai skills post-install-hook",
124130
}
125131

126132
for _, cmd := range commandsWithNoAuthRequired {
@@ -175,6 +181,7 @@ func addSubCommands(rootCmd *cobra.Command, cli *cli) {
175181
rootCmd.AddCommand(networkACLCmd(cli))
176182
rootCmd.AddCommand(tenantSettingsCmd(cli))
177183
rootCmd.AddCommand(tokenExchangeCmd(cli))
184+
rootCmd.AddCommand(aiCmd(cli))
178185

179186
// Keep completion at the bottom.
180187
rootCmd.AddCommand(completionCmd(cli))

internal/cli/skills.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"time"
8+
9+
"github.com/AlecAivazis/survey/v2"
10+
"github.com/spf13/cobra"
11+
12+
"github.com/auth0/auth0-cli/internal/ansi"
13+
"github.com/auth0/auth0-cli/internal/iostream"
14+
15+
"github.com/auth0/auth0-cli/internal/ai/skills"
16+
)
17+
18+
const (
19+
skillsSentinelPath = ".config/auth0/agents/.post-install-ran"
20+
skillsInstallTip = "Run 'auth0 ai skills install' any time to set up Auth0 skills for your AI assistant."
21+
22+
skillsPluginRepo = "https://github.com/auth0/agent-skills"
23+
skillsPluginRef = "main"
24+
)
25+
26+
func pluginTargetDir() (string, error) {
27+
home, err := os.UserHomeDir()
28+
if err != nil {
29+
return "", err
30+
}
31+
return filepath.Join(home, ".config", "auth0", "agents", "plugins", "auth0"), nil
32+
}
33+
34+
func globalLockPath(targetDir string) string {
35+
return filepath.Join(targetDir, "skills-lock.json")
36+
}
37+
38+
func skillsSentinel() string {
39+
home, _ := os.UserHomeDir()
40+
return filepath.Join(home, skillsSentinelPath)
41+
}
42+
43+
func writeSkillsSentinel() {
44+
sentinel := skillsSentinel()
45+
_ = os.MkdirAll(filepath.Dir(sentinel), 0o755)
46+
_ = os.WriteFile(sentinel, []byte{}, 0o644)
47+
}
48+
49+
func skillsSentinelExists() bool {
50+
_, err := os.Stat(skillsSentinel())
51+
return err == nil
52+
}
53+
54+
func aiCmd(cli *cli) *cobra.Command {
55+
cmd := &cobra.Command{
56+
Use: "ai",
57+
Short: "Manage Auth0 AI capabilities",
58+
Long: "Manage Auth0 AI capabilities including skills for your AI coding assistants.",
59+
}
60+
61+
cmd.AddCommand(aiSkillsCmd(cli))
62+
63+
return cmd
64+
}
65+
66+
func aiSkillsCmd(cli *cli) *cobra.Command {
67+
cmd := &cobra.Command{
68+
Use: "skills",
69+
Short: "Manage Auth0 AI skills for coding assistants",
70+
Long: "Manage Auth0 AI skills that provide Auth0-specific guidance to your AI coding assistants.",
71+
}
72+
73+
cmd.AddCommand(postInstallHookCmd(cli))
74+
75+
return cmd
76+
}
77+
78+
func postInstallHookCmd(cli *cli) *cobra.Command {
79+
return &cobra.Command{
80+
Use: "post-install-hook",
81+
Hidden: true,
82+
Short: "Run post-install setup for Auth0 AI skills",
83+
RunE: func(cmd *cobra.Command, args []string) error {
84+
if skillsSentinelExists() {
85+
return nil
86+
}
87+
88+
if !iostream.IsInputTerminal() || !iostream.IsOutputTerminal() {
89+
fmt.Fprintln(os.Stdout, skillsInstallTip)
90+
writeSkillsSentinel()
91+
return nil
92+
}
93+
94+
const (
95+
choiceAuto = "Auto — Detect installed AI agents and install all skills globally"
96+
choiceManual = "Manual — Choose which skills, agents, and scope to configure"
97+
choiceSkip = "Skip — I will run 'auth0 ai skills install' later"
98+
)
99+
100+
fmt.Fprintln(os.Stdout, "\nAuth0 AI skills add Auth0-specific guidance to your AI coding assistant.")
101+
fmt.Fprintln(os.Stdout, "")
102+
103+
var choice string
104+
prompt := &survey.Select{
105+
Message: "How would you like to install them?",
106+
Options: []string{choiceAuto, choiceManual, choiceSkip},
107+
Default: choiceAuto,
108+
}
109+
110+
if err := survey.AskOne(prompt, &choice); err != nil {
111+
// User pressed Ctrl+C or closed the terminal — skip gracefully.
112+
fmt.Fprintln(os.Stdout, skillsInstallTip)
113+
writeSkillsSentinel()
114+
return nil
115+
}
116+
117+
writeSkillsSentinel()
118+
119+
switch choice {
120+
case choiceAuto:
121+
return runInstallFast(cli)
122+
case choiceManual:
123+
return runInstallInteractive(cli)
124+
default:
125+
fmt.Fprintln(os.Stdout, skillsInstallTip)
126+
}
127+
128+
return nil
129+
},
130+
}
131+
}
132+
133+
// runInstallFast detects all installed AI agents and installs all available Auth0
134+
// skills globally into each one. Equivalent to `auth0 ai skills install --fast`.
135+
func runInstallFast(_ *cli) error {
136+
targetDir, err := pluginTargetDir()
137+
if err != nil {
138+
return fmt.Errorf("resolve plugin directory: %w", err)
139+
}
140+
141+
lockPath := globalLockPath(targetDir)
142+
143+
// Download (or skip if already up-to-date).
144+
var commitSHA string
145+
if err := ansi.Waiting(func() error {
146+
commitSHA, err = downloadSkillsIfNeeded(targetDir, lockPath)
147+
return err
148+
}); err != nil {
149+
return fmt.Errorf("download Auth0 skills: %w", err)
150+
}
151+
152+
// List skills that were downloaded.
153+
skillsDir := filepath.Join(targetDir, "skills")
154+
available, err := skills.ListAvailableSkills(skillsDir)
155+
if err != nil || len(available) == 0 {
156+
return fmt.Errorf("no skills found in %s", skillsDir)
157+
}
158+
159+
skillNames := make([]string, len(available))
160+
for i, s := range available {
161+
skillNames[i] = s.Name
162+
}
163+
164+
// Install into every detected agent.
165+
agents := skills.FastPriorityAgents()
166+
var installedAgents []string
167+
168+
for _, agent := range agents {
169+
agentSkillsDir, resolveErr := agent.ResolvedGlobalSkillsDir()
170+
if resolveErr != nil {
171+
continue
172+
}
173+
for _, skillName := range skillNames {
174+
sourceSkillDir := filepath.Join(skillsDir, skillName)
175+
if linkErr := skills.CreateSkillLink(sourceSkillDir, agentSkillsDir, skillName, false); linkErr != nil {
176+
fmt.Fprintf(os.Stderr, "warning: could not install skill %q for %s: %v\n", skillName, agent.DisplayName, linkErr)
177+
}
178+
}
179+
installedAgents = append(installedAgents, agent.ID)
180+
}
181+
182+
// Write the global lock file.
183+
now := time.Now()
184+
lock := &skills.Lock{
185+
Repo: skillsPluginRepo,
186+
Ref: skillsPluginRef,
187+
CommitSHA: commitSHA,
188+
InstalledAt: now,
189+
UpdatedAt: now,
190+
LastCheckedAt: now,
191+
Skills: skillNames,
192+
Agents: installedAgents,
193+
Scope: skills.ScopeGlobal,
194+
}
195+
if writeErr := skills.WriteLock(lockPath, lock); writeErr != nil {
196+
fmt.Fprintf(os.Stderr, "warning: could not write lock file: %v\n", writeErr)
197+
}
198+
199+
fmt.Fprintf(os.Stdout, "\nInstalled %d Auth0 skill(s) for %d agent(s).\n", len(skillNames), len(installedAgents))
200+
for _, s := range available {
201+
fmt.Fprintf(os.Stdout, " - %s\n", s.Name)
202+
}
203+
204+
return nil
205+
}
206+
207+
// downloadSkillsIfNeeded downloads the skills plugin if the lock file is absent or
208+
// records a different commit SHA than the remote. Returns the commit SHA in use.
209+
func downloadSkillsIfNeeded(targetDir, lockPath string) (string, error) {
210+
lock, err := skills.ReadLock(lockPath)
211+
if err != nil {
212+
return "", fmt.Errorf("read lock file: %w", err)
213+
}
214+
215+
if lock != nil && lock.CommitSHA != "" {
216+
// Already installed — reuse the cached SHA.
217+
return lock.CommitSHA, nil
218+
}
219+
220+
return skills.DownloadPlugin(targetDir, skillsPluginRef)
221+
}
222+
223+
// runInstallInteractive runs the full interactive install flow.
224+
// Skills install customization coming soon.
225+
func runInstallInteractive(_ *cli) error {
226+
fmt.Fprintln(os.Stdout, "Skills install customization coming soon.")
227+
fmt.Fprintln(os.Stdout, skillsInstallTip)
228+
return nil
229+
}

0 commit comments

Comments
 (0)