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
42 changes: 34 additions & 8 deletions ai-code-backends.el
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ so the CLI itself handles the installation details."
:config "~/.claude.json"
:agent-file "CLAUDE.md"
:upgrade "npm install -g @anthropic-ai/claude-code@latest"
:install-skills ai-code-claude-code-install-skills
:install-skills nil
:cli "claude")
(gemini
:label "Gemini CLI"
Expand Down Expand Up @@ -584,16 +584,32 @@ ARG is the prefix argument to pass to the upgrade function."
"Fallback skills installation for backend LABEL.
Prompt user for a skills repository URL and ask the AI CLI session
to read the repo README and install the skills."
(let* ((url (read-string
(format "Skills repo URL for %s: " label)
(ai-code--manage-backend-skills-fallback label 'install))

(defun ai-code--manage-backend-skills-fallback (label action)
"Fallback backend skills management for LABEL and ACTION.
ACTION should be the symbol `install' or `uninstall'."
(let* ((action-name
(pcase action
('install "install")
('uninstall "uninstall")
(_ (user-error
"Invalid backend skills action: %S; expected `install' or `uninstall'"
action))))
(url (read-string
(format "Skills repo URL for %s %s: " label action-name)
nil nil "https://github.com/obra/superpowers"))
(default-prompt
(format
"Please read the README of %s and install/setup the skills described there for %s. Follow the installation instructions in the README."
url label))
(if (eq action 'uninstall)
(format
"Please read the README of %s and uninstall/remove the skills described there for %s. Follow the repository instructions to remove any installed skill files and cleanup related configuration."
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the uninstall fallback prompt, "cleanup related configuration" should be "clean up related configuration" (verb form). This prompt is user/AI-facing text, so tightening grammar here helps avoid ambiguity.

Suggested change
"Please read the README of %s and uninstall/remove the skills described there for %s. Follow the repository instructions to remove any installed skill files and cleanup related configuration."
"Please read the README of %s and uninstall/remove the skills described there for %s. Follow the repository instructions to remove any installed skill files and clean up related configuration."

Copilot uses AI. Check for mistakes.
url label)
(format
"Please read the README of %s and install/setup the skills described there for %s. Follow the installation instructions in the README."
url label)))
(prompt (if (called-interactively-p 'interactive)
(ai-code-read-string
(format "Edit install-skills prompt for %s: " label)
(format "Edit %s-skills prompt for %s: " action-name label)
default-prompt)
default-prompt)))
(ai-code-cli-send-command prompt)))
Expand All @@ -606,15 +622,25 @@ If the backend defines an :install-skills property, use it:
- symbol: call the function.
Otherwise fall back to prompting the AI session to install from a
skills repository URL."
;; DONE: firstly ask user if it is install or uninstall, then ask for the repo URL if needed, and finally run install / uninstall using corresponding prompt.
(interactive)
(let* ((spec (ai-code--backend-spec ai-code-selected-backend)))
(if (not spec)
(user-error "No backend is currently selected")
(ai-code--ensure-backend-loaded spec)
(let* ((plist (cdr spec))
(install-skills (plist-get plist :install-skills))
(label (ai-code-current-backend-label)))
(label (ai-code-current-backend-label))
(action-choice (completing-read
(format "Manage skills for %s: " label)
'("install" "uninstall")
nil t nil nil "install"))
(action (if (string= action-choice "uninstall")
'uninstall
'install)))
(cond
((eq action 'uninstall)
(ai-code--manage-backend-skills-fallback label 'uninstall))
((stringp install-skills)
(compile install-skills)
(message "Running skills installation for %s" label))
Expand Down
2 changes: 1 addition & 1 deletion ai-code.el
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ Shows the current backend label to the right."
("z" "Switch to AI CLI (C-u: hide)" ai-code-cli-switch-to-buffer-or-hide)
("s" ai-code-select-backend :description ai-code--select-backend-description)
("u" "Install / Upgrade AI CLI" ai-code-upgrade-backend)
("S" "Install skills for backend" ai-code-install-backend-skills)
("S" "(Un)Install skills for backend" ai-code-install-backend-skills)
("g" "Open backend config (eg. add mcp)" ai-code-open-backend-config)
("G" "Open backend repo agent file" ai-code-open-backend-agent-file)
("|" "Apply prompt on file" ai-code-apply-prompt-on-current-file))
Expand Down
65 changes: 59 additions & 6 deletions test/test_ai-code-backends.el
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,25 @@
(ert-deftest ai-code-test-install-skills-with-string-command ()
"Backend with :install-skills string runs it via compile."
(let* ((compiled-cmd nil)
(read-string-called nil)
(ai-code-backends '((test-backend
:label "Test Backend"
:install-skills "npm install skills"
:cli "test")))
(ai-code-selected-backend 'test-backend))
(cl-letf (((symbol-function 'compile)
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "install"))
((symbol-function 'read-string)
(lambda (&rest _args)
(setq read-string-called t)
"https://github.com/obra/superpowers"))
((symbol-function 'compile)
(lambda (cmd) (setq compiled-cmd cmd)))
((symbol-function 'message)
(lambda (&rest _args) nil)))
(ai-code-install-backend-skills)
(should (string= compiled-cmd "npm install skills")))))
(should (string= compiled-cmd "npm install skills"))
(should-not read-string-called))))

(ert-deftest ai-code-test-install-skills-with-function-symbol ()
"Backend with :install-skills as function symbol calls that function."
Expand All @@ -231,7 +239,9 @@
:install-skills ai-code-test--install-skills-fn
:cli "test")))
(ai-code-selected-backend 'test-backend))
(cl-letf (((symbol-function 'ai-code-test--install-skills-fn)
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "install"))
((symbol-function 'ai-code-test--install-skills-fn)
(lambda () (setq fn-called t)))
((symbol-function 'message)
(lambda (&rest _args) nil)))
Expand All @@ -241,22 +251,63 @@
(ert-deftest ai-code-test-install-skills-fallback-when-nil ()
"Backend without :install-skills falls back to prompting AI via send-command."
(let* ((sent-command nil)
(prompted-url nil)
(ai-code-backends '((test-backend
:label "Test Backend"
:cli "test")))
(ai-code-selected-backend 'test-backend))
(cl-letf (((symbol-function 'read-string)
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "install"))
((symbol-function 'read-string)
(lambda (_prompt &optional _initial _history _default &rest _rest)
(setq prompted-url t)
"https://github.com/obra/superpowers"))
((symbol-function 'ai-code-cli-send-command)
(lambda (cmd) (setq sent-command cmd)))
((symbol-function 'message)
(lambda (&rest _args) nil)))
(ai-code-install-backend-skills)
(should prompted-url)
(should (stringp sent-command))
(should (string-match-p "superpowers" sent-command))
(should (string-match-p "README" sent-command)))))

(ert-deftest ai-code-test-uninstall-skills-fallback-prompts-for-url-and-sends-command ()
"Uninstall should prompt for a repo URL and send an uninstall prompt."
(let* ((sent-command nil)
(prompted-url nil)
(ai-code-backends '((test-backend
:label "Test Backend"
:install-skills "npm install skills"
:cli "test")))
(ai-code-selected-backend 'test-backend))
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "uninstall"))
((symbol-function 'read-string)
(lambda (_prompt &optional _initial _history _default &rest _rest)
(setq prompted-url t)
"https://github.com/obra/superpowers"))
((symbol-function 'ai-code-cli-send-command)
(lambda (cmd) (setq sent-command cmd)))
((symbol-function 'message)
(lambda (&rest _args) nil)))
(ai-code-install-backend-skills)
(should prompted-url)
(should (stringp sent-command))
(should (string-match-p "superpowers" sent-command))
(should (string-match-p "uninstall" sent-command)))))

(ert-deftest ai-code-test-manage-backend-skills-fallback-errors-on-invalid-action ()
"Fallback helper should reject invalid backend skills actions."
(cl-letf (((symbol-function 'read-string)
(lambda (&rest _args)
"https://github.com/obra/superpowers"))
((symbol-function 'ai-code-cli-send-command)
(lambda (_cmd)
(ert-fail "invalid action should error before sending a command"))))
(should-error (ai-code--manage-backend-skills-fallback "Test Backend" 'remove)
:type 'user-error)))

(ert-deftest ai-code-test-select-backend-shows-onboarding-hint ()
"Explicit backend selection should show the onboarding next-step hint."
(let* ((hint-called nil)
Expand Down Expand Up @@ -323,8 +374,10 @@
:install-skills ai-code-test-nonexistent-install-fn
:cli "test")))
(ai-code-selected-backend 'test-backend))
(should-error (ai-code-install-backend-skills)
:type 'user-error)))
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _args) "install")))
(should-error (ai-code-install-backend-skills)
:type 'user-error))))

(ert-deftest ai-code-test-claude-code-backend-has-install-skills ()
"Claude Code backend spec should have :install-skills set to the dedicated function."
Expand Down
Loading