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
246 changes: 230 additions & 16 deletions libs/kit-generator/src/kit/api.clj
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
(ns kit.api
"Public API for kit-generator."
(:require
[clojure.java.io :as jio]
[clojure.string :as str]
[clojure.test :as t]
[kit.generator.hooks :as hooks]
[kit.generator.io :as io]
[kit.generator.modules :as modules]
[kit.generator.modules-log :refer [track-installation installed-modules
module-installed?]]
[kit.generator.modules-log :as modules-log :refer [module-installed?]]
[kit.generator.modules.dependencies :as deps]
[kit.generator.modules.generator :as generator]
[kit.generator.snippets :as snippets]))
Expand Down Expand Up @@ -181,31 +181,245 @@
([module-key kit-edn-path {:keys [accept-hooks? dry?] :as opts}]
(if dry?
(print-installation-plan module-key kit-edn-path opts)
(let [{:keys [ctx pending-modules installed-modules]} (installation-plan module-key kit-edn-path opts)
(let [{:keys [ctx pending-modules installed-modules] plan-opts :opts} (installation-plan module-key kit-edn-path opts)
accept-hooks-atom (atom accept-hooks?)]
(report-already-installed installed-modules)
(doseq [{:module/keys [key resolved-config] :as module} pending-modules]
(try
(track-installation ctx key
(generator/generate ctx module)
(hooks/run-hooks :post-install resolved-config
{:confirm (partial prompt-run-hooks accept-hooks-atom)})
(report-install-module-success key resolved-config))
(catch Exception e
(report-install-module-error key e))))))
(let [feature-flag (get-in plan-opts [key :feature-flag] :default)]
(try
(generator/generate ctx module)
(let [manifest (modules-log/build-installation-manifest ctx module feature-flag)]
(hooks/run-hooks :post-install resolved-config
{:confirm (partial prompt-run-hooks accept-hooks-atom)})
(modules-log/record-installation ctx key manifest)
(report-install-module-success key resolved-config))
(catch Exception e
(modules-log/record-installation ctx key {:status :failed})
(report-install-module-error key e)))))))
:done))

(defn list-installed-modules
"Lists installed modules and modules that failed to install, for the current
project."
[]
(doseq [[id status] (-> (read-ctx)
(installed-modules))]
(println id (if (= status :success)
"installed successfully"
"failed to install")))
(let [ctx (read-ctx)
modules-root (modules/root ctx)
log (modules-log/read-modules-log modules-root)]
(doseq [[id entry] log]
(let [status (if (map? entry) (:status entry) entry)]
(println id (case status
:success "installed successfully"
:failed "failed to install"
(str "unknown status: " status))))))
:done)

;; --- Module removal ---

(defn- check-file-status
"Checks whether a file created by module installation can be safely removed.
Returns :safe if unchanged (SHA matches), :modified if changed, :missing if deleted."
[{:keys [target sha256]}]
(let [f (jio/file target)]
(cond
(not (.exists f))
:missing

(nil? sha256)
:modified

:else
(let [current-sha (modules-log/sha256
(java.nio.file.Files/readAllBytes (.toPath f)))]
(if (= sha256 current-sha)
:safe
:modified)))))

(defn- find-dependents
"Returns the set of installed module keys that depend on target-key.
Resolves each installed module using the feature flag it was installed with."
[ctx target-key]
(let [modules-root (modules/root ctx)
log (modules-log/read-modules-log modules-root)
installed-keys (->> log
(filter (fn [[_ entry]]
(let [status (if (map? entry) (:status entry) entry)]
(= :success status))))
keys)
;; Build opts map with each module's stored feature-flag
resolve-opts (->> installed-keys
(map (fn [mk]
(let [entry (get log mk)
ff (if (map? entry)
(:feature-flag entry :default)
:default)]
[mk {:feature-flag ff}])))
(into {}))
loaded-ctx (modules/load-modules ctx resolve-opts)]
(deps/immediate-dependents loaded-ctx installed-keys target-key)))

(defn removal-report
"Generates a removal report for an installed module. Returns a map with:
- :module-key
- :has-manifest? (whether detailed installation manifest is available)
- :safe-to-remove (files that can be auto-deleted, SHA matches)
- :modified-files (files modified since installation)
- :missing-files (files deleted since installation)
- :manual-steps (human-readable injection removal instructions)
- :dependents (installed modules that depend on this one)
Returns nil if the module is not installed."
([module-key]
(removal-report module-key default-edn {}))
([module-key opts]
(removal-report module-key default-edn opts))
([module-key kit-edn-path opts]
(let [ctx (read-ctx kit-edn-path)
modules-root (modules/root ctx)
log (modules-log/read-modules-log modules-root)
entry (get log module-key)]
(when entry
(let [manifest? (map? entry)
manifest (when manifest? entry)
dependents (try
(find-dependents ctx module-key)
(catch Exception e
(println "WARNING: error resolving dependents:" (.getMessage e))
#{}))]
(if manifest?
;; New format: detailed manifest with SHA comparison
(let [file-statuses (map (fn [asset]
(assoc asset :file-status (check-file-status asset)))
(:assets manifest))
safe (vec (keep #(when (= :safe (:file-status %)) (:target %)) file-statuses))
modified (vec (keep #(when (= :modified (:file-status %))
{:path (:target %) :reason "content changed since installation"})
file-statuses))
missing (vec (keep #(when (= :missing (:file-status %)) (:target %)) file-statuses))
manual-steps (mapv :description (:injections manifest))]
{:module-key module-key
:has-manifest? true
:safe-to-remove safe
:modified-files modified
:missing-files missing
:manual-steps manual-steps
:dependents dependents})
;; Old format: best-effort report from module config on disk
(let [loaded-ctx (try (modules/load-modules ctx) (catch Exception _ nil))
descriptions (when loaded-ctx
(try
(let [module (modules/lookup-module loaded-ctx module-key)]
(generator/describe-actions module))
(catch Exception _ nil)))]
{:module-key module-key
:has-manifest? false
:safe-to-remove []
:modified-files []
:manual-steps (vec (or descriptions
["Module config not found. Review your project files manually."]))
:dependents dependents})))))))

(defn print-removal-report
"Prints a human-readable removal report for an installed module."
([module-key]
(print-removal-report module-key default-edn {}))
([module-key opts]
(print-removal-report module-key default-edn opts))
([module-key kit-edn-path opts]
(let [report (removal-report module-key kit-edn-path opts)]
(if (nil? report)
(println "Module" module-key "is not installed.")
(do
(println "REMOVAL REPORT for" (:module-key report))
(println)
(when (seq (:dependents report))
(println "WARNING: The following installed modules depend on" module-key ":")
(doseq [dep (:dependents report)]
(println " " dep))
(println "Consider removing those first.")
(println))
(when (seq (:safe-to-remove report))
(println "FILES (safe to auto-remove, unchanged since installation):")
(doseq [f (:safe-to-remove report)]
(println " DELETE" f))
(println))
(when (seq (:modified-files report))
(println "FILES (modified since installation, review before deleting):")
(doseq [{:keys [path reason]} (:modified-files report)]
(println " REVIEW" path "-" reason))
(println))
(when (seq (:missing-files report))
(println "FILES (already deleted, no action needed):")
(doseq [f (:missing-files report)]
(println " GONE" f))
(println))
(when (seq (:manual-steps report))
(println "MANUAL STEPS (undo code injections):")
(doseq [step (:manual-steps report)]
(println " -" step))
(println))
(when-not (:has-manifest? report)
(println "NOTE: This module was installed before removal tracking was added.")
(println " The above report is based on the module config and may not be")
(println " perfectly accurate. SHA comparison is not available.")
(println)))))))

(defn remove-module
"Removes a module from the project. Auto-deletes files that are unchanged
since installation. Prints instructions for manual cleanup of injections.

Options:
:force? - remove even if other modules depend on it
:dry? - only print the removal report, don't delete anything"
([module-key]
(remove-module module-key default-edn {}))
([module-key opts]
(remove-module module-key default-edn opts))
([module-key kit-edn-path {:keys [force? dry?] :as opts}]
(let [report (removal-report module-key kit-edn-path opts)]
(cond
(nil? report)
(println "ERROR: Module" module-key "is not installed.")

(and (seq (:dependents report)) (not force?))
(do (println "ERROR: Cannot remove" module-key
"because the following modules depend on it:")
(doseq [dep (:dependents report)]
(println " " dep))
(println "Use :force? true to override."))

dry?
(print-removal-report module-key kit-edn-path opts)

:else
(do
;; Auto-delete safe files
(doseq [f (:safe-to-remove report)]
(println "Deleting" f)
(jio/delete-file (jio/file f) true))

;; Report modified files
(when (seq (:modified-files report))
(println)
(println "The following files were modified since installation.")
(println "Please review and delete manually:")
(doseq [{:keys [path]} (:modified-files report)]
(println " -" path)))

;; Report manual injection steps
(when (seq (:manual-steps report))
(println)
(println "The following code was injected into existing files.")
(println "Please remove manually:")
(doseq [step (:manual-steps report)]
(println " -" step)))

;; Remove from install log
(let [ctx (read-ctx kit-edn-path)]
(modules-log/untrack-module ctx module-key))

(println)
(println "Module" module-key "has been uninstalled."))))
:done))

(def snippets-db
(let [db (atom nil)]
(fn [ctx & [reload?]]
Expand Down
14 changes: 14 additions & 0 deletions libs/kit-generator/src/kit/generator/modules/dependencies.clj
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,17 @@
(->> (dependency-tree ctx module-key opts)
(dependency-order)
(map #(modules/lookup-module ctx %))))

(defn immediate-dependents
"Returns the set of installed module keys that directly require target-key.
Used to check whether a module can be safely removed."
[ctx installed-module-keys target-key]
(->> installed-module-keys
(filter (fn [mk]
(when (not= mk target-key)
(try
(let [module (modules/lookup-module ctx mk)
requires (get-in module [:module/resolved-config :requires])]
(some #{target-key} requires))
(catch Exception _ false)))))
set))
Loading