diff --git a/ai-code-backends.el b/ai-code-backends.el index 2c87d63..79ca5c7 100644 --- a/ai-code-backends.el +++ b/ai-code-backends.el @@ -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" @@ -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." + 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))) @@ -606,6 +622,7 @@ 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) @@ -613,8 +630,17 @@ skills repository URL." (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)) diff --git a/ai-code.el b/ai-code.el index e161119..ed255b6 100644 --- a/ai-code.el +++ b/ai-code.el @@ -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)) diff --git a/test/test_ai-code-backends.el b/test/test_ai-code-backends.el index 4422bf3..3ed8dea 100644 --- a/test/test_ai-code-backends.el +++ b/test/test_ai-code-backends.el @@ -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." @@ -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))) @@ -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) @@ -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."