From be933edf284f0510b8e9b8de7c0f2e456e9388e7 Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 12:09:11 -0400 Subject: [PATCH 1/9] [README] minor mod --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f215084..01a5045 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ Have questions? See the [wiki](https://github.com/redline6561/coleslaw/wiki/Example-sites) for a list of coleslaw-powered blogs. +Coleslaw should run on any conforming Common Lisp implementations but +testing is primarily done on [SBCL](http://www.sbcl.org/) and [CCL](http://ccl.clozure.com/). + ## Features * Git for storage @@ -35,9 +38,6 @@ See the [wiki](https://github.com/redline6561/coleslaw/wiki/Example-sites) for a ## Installation -Coleslaw should run on any conforming Common Lisp implementation but -testing is primarily done on [SBCL](http://www.sbcl.org/) and -[CCL](http://ccl.clozure.com/). Coleslaw can either be run **manually** on a local machine or triggered **automatically** on git push to a server. If you want a From 5dd66dcf6216f8f9cbd3c541434ad575ae80dda2 Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 12:10:59 -0400 Subject: [PATCH 2/9] [CLI] coleslaw-cli: system for CLI command --- cli/cli.lisp | 283 +++++++++++++++++++++++++++++++++++++++++++++++ coleslaw-cli.asd | 14 +++ 2 files changed, 297 insertions(+) create mode 100644 cli/cli.lisp create mode 100644 coleslaw-cli.asd diff --git a/cli/cli.lisp b/cli/cli.lisp new file mode 100644 index 0000000..cb1e213 --- /dev/null +++ b/cli/cli.lisp @@ -0,0 +1,283 @@ +(defpackage :coleslaw-cli + (:use :cl :trivia)) + +(in-package :coleslaw-cli) + +(defun setup-coleslawrc (user &aux (path (merge-pathnames ".coleslawrc"))) + "Set up the default .coleslawrc file in the current directory." + (with-open-file (s path :direction :output :if-exists :supersede :if-does-not-exist :create) + (format t "~&Generating ~a ...~%" path) + ;; odd formatting in this source code because emacs has problem detecting the parenthesis inside a string + (format s ";;; -*- mode : lisp -*-~%(~ + ;; Required information + :author \"~a\" ;; to be placed on post pages and in the copyright/CC-BY-SA notice + :deploy-dir \"deploy/\" ;; for Coleslaw's generated HTML to go in + :domain \"https://~a.github.com\" ;; to generate absolute links to the site content + :routing ((:post \"posts/~~a\") ;; to determine the URL scheme of content on the site + (:tag-index \"tag/~~a\") + (:month-index \"date/~~a\") + (:numeric-index \"~~d\") + (:feed \"~~a.xml\") + (:tag-feed \"tag/~~a.xml\")) + :title \"Improved Means for Achieving Deteriorated Ends\" ;; a site title + :theme \"hyde\" ;; to select one of the themes in \"coleslaw/themes/\" + + ;; Optional information + :excerpt-sep \"\" ;; to set the separator for excerpt in content + :feeds (\"lisp\") + :plugins ((analytics :tracking-code \"foo\") + (disqus :shortname \"my-site-name\") + ; (incremental) ;; *Remove comment to enable incremental builds. + (mathjax) + (sitemap) + (static-pages) + ; (versioned) ;; *Remove comment to enable symlinked, timestamped deploys. + ) + :sitenav ((:url \"http://~a.github.com/\" :name \"Home\") + (:url \"http://twitter.com/~a\" :name \"Twitter\") + (:url \"http://github.com/~a\" :name \"Code\") + (:url \"http://soundcloud.com/~a\" :name \"Music\") + (:url \"http://redlinernotes.com/docs/talks/\" :name \"Talks\")) + :staging-dir \"/tmp/coleslaw/\" ;; for Coleslaw to do intermediate work, default: \"/tmp/coleslaw\" +) + +;; * Prerequisites described in plugin docs." + user + user + user + user + user + user))) + +(defun copy-theme (which &optional (target which)) + "Copy the theme named WHICH into the blog directory and rename it into TARGET" + (format t "~&Copying themes/~a ...~%" which) + (if (probe-file (format nil "themes/~a" which)) + (format t "~& themes/~a already exists.~%" which) + (progn + (ensure-directories-exist "themes/" :verbose t) + (uiop:run-program `("cp" "-v" "-r" + ,(namestring (coleslaw::app-path "themes/~a/" which)) + ,(namestring (merge-pathnames (format nil "themes/~a" target)))))))) + +(defun setup (&optional (user (uiop:getenv "USER"))) + (setup-coleslawrc user) + (copy-theme "hyde" "default")) + +(defun read-rc (&aux (path (merge-pathnames ".coleslawrc"))) + (with-open-file (s (if (probe-file path) + path + (merge-pathnames #p".coleslawrc" (user-homedir-pathname)))) + (read s))) + +(defun new (&optional (type "post") name) + (let ((sep (getf (read-rc) :separator ";;;;;"))) + (multiple-value-match (get-decoded-time) + ((second minute hour date month year _ _ _) + (let* ((name (or name + (format nil "~a-~2,,,'0@a-~2,,,'0@a" year month date))) + (path (merge-pathnames (make-pathname :name name :type type)))) + (with-open-file (s path + :direction :output :if-exists :error :if-does-not-exist :create) + (format s "~ +~a +title: ~a +tags: bar, baz +date: ~a-~2,,,'0@a-~2,,,'0@a ~2,,,'0@a:~2,,,'0@a:~2,,,'0@a +format: md +~:[~*~;URL: pages/~a.html~%~]~ +~a + + + + +Here is my content. + + + +Excerpt separator can also be extracted from content. +Add `excerpt: ` to the above metadata. +Excerpt separator is `` by default. +" + sep + name + year month date hour minute second + (string= type "page") name + sep) + (format *error-output* "~&Created a ~a \"~a\".~%" type name) + (format t "~&~a~%" path))))))) + +(defun generate () + (coleslaw:main *default-pathname-defaults*)) + +(defun preview (&optional (path (getf (read-rc) :deploy-dir))) + ;; clack depends on the global binding of *default-pathname-defaults*. + (let ((oldpath *default-pathname-defaults*)) + (unwind-protect + (progn + (when path + (setf *default-pathname-defaults* (truename path))) + (format t "~%Starting a Clack server at ~a~%" path) + (clack:clackup + (lack:builder + :accesslog + (:static :path (lambda (p) + (if (char= #\/ (alexandria:last-elt p)) + (concatenate 'string p "index.html") + p))) + #'identity) + :use-thread nil)) + (setf *default-pathname-defaults* oldpath)))) + +;; code from fs-watcher + +(defun mtime (pathname) + "Returns the mtime of a pathname" + (when (ignore-errors (probe-file pathname)) + (file-write-date pathname))) + +(defun dir-contents (pathnames test) + (remove-if-not test + ;; uiop:slurp-input-stream + (uiop:run-program `("find" ,@(mapcar #'namestring pathnames)) + :output :lines))) + +(defun run-loop (pathnames mtimes callback delay) + "The main loop constantly polling the filesystem" + (loop + (sleep delay) + (map nil + #'(lambda (pathname) + (let ((mtime (mtime pathname))) + (unless (eql mtime (gethash pathname mtimes)) + (funcall callback pathname) + (if mtime + (setf (gethash pathname mtimes) mtime) + (remhash pathname mtimes))))) + pathnames))) + +(defun watch (&optional (source-path *default-pathname-defaults*)) + (format t "~&Start watching! : ~a~%" source-path) + (let ((pathnames + (dir-contents (list source-path) + (lambda (p) (not (equal "fasl" (pathname-type p)))))) + (mtimes (make-hash-table))) + (dolist (pathname pathnames) + (setf (gethash pathname mtimes) (mtime pathname))) + (ignore-errors + (run-loop pathnames + mtimes + (lambda (pathname) + (format t "~&Changes detected! : ~a~%" pathname) + (finish-output) + (handler-case + (coleslaw:main source-path) + (error (c) + (format *error-output* "something happened... ~a" c)))) + 1)))) + +(defun watch-preview (&optional (source-path *default-pathname-defaults*)) + (when (member :swank *features*) + (warn "FIXME: This command does not do what you intend from a SLIME session.")) + (ignore-errors + (uiop:run-program + ;; The hackiness here is because clack fails? to handle? SIGINT correctly when run in a threaded mode + `("sh" "-c" ,(format nil "coleslaw watch ~a &~ + coleslaw preview &~ + jobs -p;~ + trap \"kill $(jobs -p)\" EXIT;~ + wait" source-path)) + :output :interactive + :error-output :interactive))) + +(defun help () + (format *error-output* " + + +Coleslaw, a Flexible Lisp Blogware. +Written by: Brit Butler . +Distributed by BSD license. + +Command Line Syntax: + +coleslaw setup [NAME] --- Sets up a new .coleslawrc file in the current directory. +coleslaw copy-theme THEME [TARGET] --- Copies the installed THEME in coleslaw to the current directory with a different name TARGET. +coleslaw new [TYPE] [NAME] --- Creates a new content file with the correct format. TYPE defaults to 'post', NAME defaults to the current date. +coleslaw generate --- Generates the static html according to .coleslawrc . +coleslaw preview [DIRECTORY] --- Runs a preview server at port 5000. DIRECTORY defaults to the deploy directory (described in .coleslawrc). +coleslaw watch [DIRECTORY] --- Watches the given directory and generates the site when changes are detected. Defaults to the current directory. +coleslaw --- Shorthand of 'coleslaw generate'. +coleslaw -h --- Show this help + +Corresponding REPL commands are available in coleslaw-cli package. + +```lisp + (ql:quickload :coleslaw-cli) + (coleslaw-cli:setup &optional name) + (coleslaw-cli:copy-theme theme &optional target) + (coleslaw-cli:new &optional type name) + (coleslaw-cli:generate) + (coleslaw-cli:preview &optional directory) + (coleslaw-cli:watch &optional directory) +``` + +Examples: + +* set up a blog + + mkdir yourblog ; cd yourblog + git init + coleslaw setup + git commit -a -m 'initial repo' + +* Copy the base theme to the current directory for modification + + coleslaw copy-theme hyde mytheme + +* Create a post + + coleslaw new + +* Create a page (static page) + + coleslaw new page + +* Generate a site + + coleslaw generate + # or just: + coleslaw + +* Preview a site + + coleslaw preview + # or + coleslaw preview . + +" + )) + +(defun main (&rest argv) + (declare (ignorable argv)) + (match argv + ((list* "setup" rest) + (apply #'setup rest)) + ((list* "preview" rest) + (apply #'preview rest)) + ((list* "watch" rest) + (apply #'watch rest)) + ((list* "watch-preview" rest) + (apply #'watch-preview rest)) + ((list* "new" rest) + (apply #'new rest)) + ((or nil (list "generate")) + (generate)) + ((list* "copy-theme" rest) + (apply #'copy-theme rest)) + ((list* (or "-v" "--version") _) + ) + ((list* (or "-h" "--help") _) + (help)))) + +(when (member :swank *features*) + (help)) diff --git a/coleslaw-cli.asd b/coleslaw-cli.asd new file mode 100644 index 0000000..cc0a4d4 --- /dev/null +++ b/coleslaw-cli.asd @@ -0,0 +1,14 @@ +(defsystem #:coleslaw-cli + :name "coleslaw" + :description "Flexible Lisp Blogware" + :version "0.9.7" + :license "BSD" + :author "Brit Butler " + :pathname "cli/" + :depends-on (:coleslaw + :clack + :trivia + :uiop) + :serial t + :components ((:file "cli"))) + From 5304158c0ad8188d936c4f7584a15d3819eaae7c Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 12:12:06 -0400 Subject: [PATCH 3/9] basic cli tests --- .travis.yml | 1 + cli-tests/basic.sh | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100755 cli-tests/basic.sh diff --git a/.travis.yml b/.travis.yml index b2e69df..c214342 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,3 +20,4 @@ cache: script: - ros -s prove -e "(ql:quickload '(coleslaw coleslaw-test))" -e '(or (prove:run :coleslaw-test) (uiop:quit -1))' + - cli-tests/basic.sh diff --git a/cli-tests/basic.sh b/cli-tests/basic.sh new file mode 100755 index 0000000..213a4de --- /dev/null +++ b/cli-tests/basic.sh @@ -0,0 +1,39 @@ +#!/bin/bash -x + +set -e + +dir=$(mktemp -d) + +cd $dir + +coleslaw setup + +cat .coleslawrc + +post=$(coleslaw new post "my first blog") + +echo "my firrrrrrst text!!!!" >> "$post" + +cat "$post" + +coleslaw generate + +coleslaw preview & +pid=$! + +trap "kill $pid; rm -rf $dir" EXIT + +sleep 3 + +wget 127.0.0.1:5000 -O- + +! wget 127.0.0.1:5000/nosuchurl -O- + +# ( +# wget 127.0.0.1:5000/nosuchurl -O- +# echo $? +# true +# ) + + + From 86fefef6feaff1e833663c67a92a646e919e5ab5 Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 12:12:17 -0400 Subject: [PATCH 4/9] roswell commands --- roswell/coleslaw.ros | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 roswell/coleslaw.ros diff --git a/roswell/coleslaw.ros b/roswell/coleslaw.ros new file mode 100755 index 0000000..12d9eed --- /dev/null +++ b/roswell/coleslaw.ros @@ -0,0 +1,10 @@ +#!/bin/sh +#|-*- mode:lisp -*-|# +#| +exec ros -Q -L sbcl-bin -m coleslaw -- $0 "$@" +|# +(progn ;;init forms + (ros:ensure-asdf) + #+quicklisp (ql:quickload '(:coleslaw-cli) :silent t)) + +(in-package :coleslaw-cli) From 89db40a55275b2086e873d322013c98c14b68cfe Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 12:10:31 -0400 Subject: [PATCH 5/9] [README] tutorial for CLI / REPL commands --- README.md | 116 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 01a5045..e330f58 100644 --- a/README.md +++ b/README.md @@ -36,48 +36,84 @@ testing is primarily done on [SBCL](http://www.sbcl.org/) and [CCL](http://ccl.c | [Pygments](http://pygments.org/) | [colorize](http://www.cliki.net/colorize) | | -## Installation +## Installation/Tutorial + + + +Step 1: Install this library. + +With [Roswell](https://roswell.github.io/), +``` sh +$ ros install coleslaw +$ export PATH="$HOME/.roswell/bin:$PATH" # If you haven't done this before +``` + +or + +``` lisp +(ql:quickload :coleslaw-cli) +``` -Coleslaw can either be run **manually** on a local machine or -triggered **automatically** on git push to a server. If you want a -server install, run these commands on your server _after_ setting up a -[git bare repo](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server). -Otherwise, run the commands on your local machine. +Step 2: Initialize your blog repository. -1. Install a Common Lisp implementation (we recommend SBCL) and - [Quicklisp](http://quicklisp.org/). -2. Place a config file for coleslaw in your `$HOME` directory. If you - want to run multiple blogs with coleslaw, you can keep each blog's - config file in that blog's repo. Feel free to copy and edit the - [example config][ex_config] or consult the [config docs][conf_docs] - to create one from scratch. -3. This step depends on whether you're setting up a local or server install. - * Server Install: Copy and `chmod +x` the - [example post-receive hook][post_hook] to your blog's bare repo. - * Local Install: Just run the following commands in the - REPL whenever you're ready to regenerate your blog: - ``` - (ql:quickload :coleslaw) - (coleslaw:main "/path/to/my/blog/") - ``` -4. Optionally, point the web server of your liking at your config-specified - `:deploy-dir`. Or "deploy-dir/.curr" if the `versioned` plugin is enabled. -5. If you use Emacs, consider installing - [coleslaw-mode](https://github.com/equwal/coleslaw-mode) to author your - posts. +``` sh +$ mkdir yourblog ; cd yourblog +$ git init +$ coleslaw setup +``` +``` lisp +(coleslaw-cli:setup) +``` -Now just write posts, git commit and build by hand or by push. +Step 3: Write a post file in the current directory. +The file should contain a certain metadata, so use the `coleslaw new` command, +which instantiates a correct file for you. -[ex_config]: https://github.com/redline6561/coleslaw/blob/master/examples/example.coleslawrc -[conf_docs]: https://github.com/redline6561/coleslaw/blob/master/docs/config.md -[post_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive +``` sh +$ coleslaw new +Created a post 2017-11-06.post . +``` +``` lisp +(coleslaw-cli:new "post") +``` + +Step 4: Generate the site from those post files. +The result goes to the `deploy/` subdirectory. + +``` sh +$ coleslaw +``` +``` lisp +(coleslaw-cli:generate) +``` + +Step 5: You can also launch a server... + +``` sh +$ coleslaw preview +``` +``` lisp +(coleslaw-cli:preview) +``` + +Step 6: and watch the file system to automatically regenerate the site! + +``` sh +$ coleslaw watch # or even better, +$ coleslaw watch-preview +``` +``` lisp +(coleslaw-cli:watch) ;; watch-preview does not work on REPL right now +``` + +For further customization, edit the `.coleslawrc` file generated by `coleslaw setup`. +Consult the [config docs](https://github.com/redline6561/coleslaw/blob/master/docs/config.md). ## The Content Format Coleslaw expects content to have a file extension matching the class -of the content. (I.e. `.post` for blog posts, `.page` for static -pages, etc.) +of the content. (I.e. `.post` for blog posts, `.page` for static pages, etc.) There should also be a metadata header on all files starting and ending with the config-specified `:separator`, ";;;;;" by @@ -108,6 +144,20 @@ Two themes are provided: hyde, the default, and readable (based on A guide to creating themes for coleslaw lives [here](https://github.com/redline6561/coleslaw/blob/master/docs/themes.md). +## Deploying on a standalone server + +Coleslaw can deploy to a standalone server. +If you want this server installation, initialize a bare git repo and +set up the post-receive hook on that repo. + +* First initialize a [git bare repo](http://git-scm.com/book/en/Git-on-the-Server-Setting-Up-the-Server) on the server. +* Copy [example post-receive hook][post_hook] to your blog's bare repo and set the executable bit (`chmod +x`). + +* Point the web server at `:deploy-dir` attribute on the config file. + Or "deploy-dir/.curr" if the `versioned` plugin is enabled. + +[post_hook]: https://github.com/redline6561/coleslaw/blob/master/examples/example.post-receive + ## Hacking A core goal of *coleslaw* is to be both pleasant to read and easy to From 17e526317c59911051aac2564319b7d82c814153 Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 13:12:33 -0400 Subject: [PATCH 6/9] absolute link prevents the preview from loading css/js --- cli/cli.lisp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cli/cli.lisp b/cli/cli.lisp index cb1e213..0e719cc 100644 --- a/cli/cli.lisp +++ b/cli/cli.lisp @@ -12,7 +12,7 @@ ;; Required information :author \"~a\" ;; to be placed on post pages and in the copyright/CC-BY-SA notice :deploy-dir \"deploy/\" ;; for Coleslaw's generated HTML to go in - :domain \"https://~a.github.com\" ;; to generate absolute links to the site content + :domain \"\" ;; to generate absolute links to the site content :routing ((:post \"posts/~~a\") ;; to determine the URL scheme of content on the site (:tag-index \"tag/~~a\") (:month-index \"date/~~a\") @@ -46,7 +46,6 @@ user user user - user user))) (defun copy-theme (which &optional (target which)) From 919d047349d72934e02a4c07c8ee920864490bf8 Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 19:13:38 -0400 Subject: [PATCH 7/9] export functions --- cli/cli.lisp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/cli/cli.lisp b/cli/cli.lisp index 0e719cc..d0d4aef 100644 --- a/cli/cli.lisp +++ b/cli/cli.lisp @@ -1,5 +1,14 @@ (defpackage :coleslaw-cli - (:use :cl :trivia)) + (:use :cl :trivia) + (:export + #:copy-theme + #:setup + #:new + #:generate + #:preview + #:watch + #:watch-preview + #:help)) (in-package :coleslaw-cli) From 0e325ee56ea3ab449dcd3fad485303abe2c442cd Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 19:13:50 -0400 Subject: [PATCH 8/9] testing repl --- cli/cli.lisp | 3 ++- coleslaw-test.asd | 5 +++-- tests/cli.lisp | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 tests/cli.lisp diff --git a/cli/cli.lisp b/cli/cli.lisp index d0d4aef..b148beb 100644 --- a/cli/cli.lisp +++ b/cli/cli.lisp @@ -113,7 +113,8 @@ Excerpt separator is `` by default. (string= type "page") name sep) (format *error-output* "~&Created a ~a \"~a\".~%" type name) - (format t "~&~a~%" path))))))) + (format t "~&~a~%" path) + path)))))) (defun generate () (coleslaw:main *default-pathname-defaults*)) diff --git a/coleslaw-test.asd b/coleslaw-test.asd index 5426a07..e077420 100644 --- a/coleslaw-test.asd +++ b/coleslaw-test.asd @@ -4,10 +4,11 @@ :description "A test suite for coleslaw." :license "BSD" :author "Brit Butler " - :depends-on (:coleslaw :prove) + :depends-on (:coleslaw :coleslaw-cli :prove) :defsystem-depends-on (:prove-asdf) :components ((:module "tests" :components - ((:test-file "tests")))) + ((:test-file "tests") + (:test-file "cli")))) :perform (test-op :after (op c) (uiop:symbol-call :prove 'run c))) diff --git a/tests/cli.lisp b/tests/cli.lisp new file mode 100644 index 0000000..2376aec --- /dev/null +++ b/tests/cli.lisp @@ -0,0 +1,23 @@ + +(in-package :coleslaw-tests) + +(plan 2) + +(let ((*default-pathname-defaults* + (pathname + (format nil "~a/" + (uiop:run-program `("mktemp" "-d") + :output `(:string :stripped t)))))) + (coleslaw-cli:setup) + (let ((file (coleslaw-cli:new))) + (ok (probe-file file))) + (coleslaw-cli:generate) + (print (format nil "~adeploy/index.html" *default-pathname-defaults*)) + (ok (probe-file (format nil "~adeploy/index.html" *default-pathname-defaults*)))) + +(finalize) + + + + + From d9fa597d791215307f3a92928cc77e1b976f068f Mon Sep 17 00:00:00 2001 From: Masataro Asai Date: Sat, 26 Oct 2019 19:43:06 -0400 Subject: [PATCH 9/9] fix: DEPLOY method does not respect *default-pathname-defaults* --- src/coleslaw.lisp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coleslaw.lisp b/src/coleslaw.lisp index c5b9bad..895569e 100644 --- a/src/coleslaw.lisp +++ b/src/coleslaw.lisp @@ -43,7 +43,7 @@ in REPO-DIR. Optionally, OLDREV is the revision prior to the last push." (defgeneric deploy (staging) (:documentation "Deploy the STAGING build to the directory specified in the config.") (:method (staging) - (run-program "rsync --delete -avz ~a ~a" staging (deploy-dir *config*)))) + (run-program "rsync --delete -avz ~a ~a" staging (merge-pathnames (deploy-dir *config*))))) (defun update-symlink (path target) "Update the symlink at PATH to point to TARGET."