1# Soft Serve
  2
  3<p>
  4    <img style="width: 451px" src="https://stuff.charm.sh/soft-serve/soft-serve-header.png?0" alt="A nice rendering of some melting ice cream with the words āCharm Soft Serveā next to it"><br>
  5    <a href="https://github.com/charmbracelet/soft-serve/releases"><img src="https://img.shields.io/github/release/charmbracelet/soft-serve.svg" alt="Latest Release"></a>
  6    <a href="https://pkg.go.dev/github.com/charmbracelet/soft-serve?tab=doc"><img src="https://godoc.org/github.com/golang/gddo?status.svg" alt="GoDoc"></a>
  7    <a href="https://github.com/charmbracelet/soft-serve/actions"><img src="https://github.com/charmbracelet/soft-serve/workflows/build/badge.svg" alt="Build Status"></a>
  8    <a href="https://nightly.link/charmbracelet/soft-serve/workflows/nightly/main"><img src="https://shields.io/badge/-Nightly%20Builds-orange?logo=hackthebox&logoColor=fff&style=appveyor"/></a>
  9</p>
 10
 11A tasty, self-hostable Git server for the command line. š¦
 12
 13<picture>
 14  <source media="(max-width: 750px)" srcset="https://github.com/charmbracelet/soft-serve/assets/42545625/c754c746-dc4c-44a6-9c39-28649264cbf2">
 15  <source media="(min-width: 750px)" width="750" srcset="https://github.com/charmbracelet/soft-serve/assets/42545625/c754c746-dc4c-44a6-9c39-28649264cbf2">
 16  <img src="https://github.com/charmbracelet/soft-serve/assets/42545625/c754c746-dc4c-44a6-9c39-28649264cbf2" alt="Soft Serve screencast">
 17</picture>
 18
 19- Easy to navigate TUI available over SSH
 20- Clone repos over SSH, HTTP, or Git protocol
 21- Git LFS support with both HTTP and SSH backends
 22- Manage repos with SSH
 23- Create repos on demand with SSH or `git push`
 24- Browse repos, files and commits with SSH-accessible UI
 25- Print files over SSH with or without syntax highlighting and line numbers
 26- Easy access control
 27  - SSH authentication using public keys
 28  - Allow/disallow anonymous access
 29  - Add collaborators with SSH public keys
 30  - Repos can be public or private
 31  - User access tokens
 32
 33## Where can I see it?
 34
 35Just run `ssh git.charm.sh` for an example. You can also try some of the following commands:
 36
 37```bash
 38# Jump directly to a repo in the TUI
 39ssh git.charm.sh -t soft-serve
 40
 41# Print out a directory tree for a repo
 42ssh git.charm.sh repo tree soft-serve
 43
 44# Print a specific file
 45ssh git.charm.sh repo blob soft-serve cmd/soft/root.go
 46
 47# Print a file with syntax highlighting and line numbers
 48ssh git.charm.sh repo blob soft-serve cmd/soft/root.go -c -l
 49```
 50
 51## Installation
 52
 53Soft Serve is a single binary called `soft`. You can get it from a package
 54manager:
 55
 56```bash
 57# macOS or Linux
 58brew tap charmbracelet/tap && brew install charmbracelet/tap/soft-serve
 59
 60# Arch Linux
 61pacman -S soft-serve
 62
 63# Nix
 64nix-env -iA nixpkgs.soft-serve
 65
 66# Debian/Ubuntu
 67sudo mkdir -p /etc/apt/keyrings
 68curl -fsSL https://repo.charm.sh/apt/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/charm.gpg
 69echo "deb [signed-by=/etc/apt/keyrings/charm.gpg] https://repo.charm.sh/apt/ * *" | sudo tee /etc/apt/sources.list.d/charm.list
 70sudo apt update && sudo apt install soft-serve
 71
 72# Fedora/RHEL
 73echo '[charm]
 74name=Charm
 75baseurl=https://repo.charm.sh/yum/
 76enabled=1
 77gpgcheck=1
 78gpgkey=https://repo.charm.sh/yum/gpg.key' | sudo tee /etc/yum.repos.d/charm.repo
 79sudo yum install soft-serve
 80```
 81
 82You can also download a binary from the [releases][releases] page. Packages are
 83available in Alpine, Debian, and RPM formats. Binaries are available for Linux,
 84macOS, and Windows.
 85
 86[releases]: https://github.com/charmbracelet/soft-serve/releases
 87
 88Or just install it with `go`:
 89
 90```bash
 91go install github.com/charmbracelet/soft-serve/cmd/soft@latest
 92```
 93
 94A [Docker image][docker] is also available.
 95
 96[docker]: https://github.com/charmbracelet/soft-serve/blob/main/docker.md
 97
 98## Setting up a server
 99
100Make sure `git` is installed, then run `soft serve`. Thatās it.
101
102This will create a `data` directory that will store all the repos, ssh keys,
103and database.
104
105To change the default data path use `SOFT_SERVE_DATA_PATH` environment variable.
106
107```sh
108SOFT_SERVE_DATA_PATH=/var/lib/soft-serve soft serve
109```
110
111When you run Soft Serve for the first time, make sure you have the
112`SOFT_SERVE_INITIAL_ADMIN_KEYS` environment variable is set to your ssh
113authorized key. Any added key to this variable will be treated as admin with
114full privileges.
115
116Using this environment variable, Soft Serve will create a new `admin` user that
117has full privileges. You can rename and change the user settings later.
118
119Check out [Systemd][systemd] on how to run Soft Serve as a service using
120Systemd. Soft Serve packages in our Apt/Yum repositories come with Systemd
121service units.
122
123[systemd]: https://github.com/charmbracelet/soft-serve/blob/main/systemd.md
124
125### Server Configuration
126
127Once you start the server for the first time, the settings will be in
128`config.yaml` under your data directory. The default `config.yaml` is
129self-explanatory and will look like this:
130
131```yaml
132# Soft Serve Server configurations
133
134# The name of the server.
135# This is the name that will be displayed in the UI.
136name: "Soft Serve"
137
138# Log format to use. Valid values are "json", "logfmt", and "text".
139log_format: "text"
140
141# The SSH server configuration.
142ssh:
143  # The address on which the SSH server will listen.
144  listen_addr: ":23231"
145
146  # The public URL of the SSH server.
147  # This is the address that will be used to clone repositories.
148  public_url: "ssh://localhost:23231"
149
150  # The path to the SSH server's private key.
151  key_path: "ssh/soft_serve_host"
152
153  # The path to the SSH server's client private key.
154  # This key will be used to authenticate the server to make git requests to
155  # ssh remotes.
156  client_key_path: "ssh/soft_serve_client"
157
158  # The maximum number of seconds a connection can take.
159  # A value of 0 means no timeout.
160  max_timeout: 0
161
162  # The number of seconds a connection can be idle before it is closed.
163  idle_timeout: 120
164
165# The Git daemon configuration.
166git:
167  # The address on which the Git daemon will listen.
168  listen_addr: ":9418"
169
170  # The maximum number of seconds a connection can take.
171  # A value of 0 means no timeout.
172  max_timeout: 0
173
174  # The number of seconds a connection can be idle before it is closed.
175  idle_timeout: 3
176
177  # The maximum number of concurrent connections.
178  max_connections: 32
179
180# The HTTP server configuration.
181http:
182  # The address on which the HTTP server will listen.
183  listen_addr: ":23232"
184
185  # The path to the TLS private key.
186  tls_key_path: ""
187
188  # The path to the TLS certificate.
189  tls_cert_path: ""
190
191  # The public URL of the HTTP server.
192  # This is the address that will be used to clone repositories.
193  # Make sure to use https:// if you are using TLS.
194  public_url: "http://localhost:23232"
195
196# The database configuration.
197db:
198  # The database driver to use.
199  # Valid values are "sqlite" and "postgres".
200  driver: "sqlite"
201  # The database data source name.
202  # This is driver specific and can be a file path or connection string.
203  # Make sure foreign key support is enabled when using SQLite.
204  data_source: "soft-serve.db?_pragma=busy_timeout(5000)&_pragma=foreign_keys(1)"
205
206# Git LFS configuration.
207lfs:
208  # Enable Git LFS.
209  enabled: true
210  # Enable Git SSH transfer.
211  ssh_enabled: false
212
213# Cron job configuration
214jobs:
215  mirror_pull: "@every 10m"
216
217# The stats server configuration.
218stats:
219  # The address on which the stats server will listen.
220  listen_addr: ":23233"
221# Additional admin keys.
222#initial_admin_keys:
223#  - "ssh-rsa AAAAB3NzaC1yc2..."
224```
225
226You can also use environment variables, to override these settings. All server
227settings environment variables start with `SOFT_SERVE_` followed by the setting
228name all in uppercase. Here are some examples:
229
230- `SOFT_SERVE_NAME`: The name of the server that will appear in the TUI
231- `SOFT_SERVE_SSH_LISTEN_ADDR`: SSH listen address
232- `SOFT_SERVE_SSH_KEY_PATH`: SSH host key-pair path
233- `SOFT_SERVE_HTTP_LISTEN_ADDR`: HTTP listen address
234- `SOFT_SERVE_HTTP_PUBLIC_URL`: HTTP public URL used for cloning
235- `SOFT_SERVE_GIT_MAX_CONNECTIONS`: The number of simultaneous connections to git daemon
236
237#### Database Configuration
238
239Soft Serve supports both SQLite and Postgres for its database. Like all other Soft Serve settings, you can change the database _driver_ and _data source_ using either `config.yaml` or environment variables. The default config uses SQLite as the default database driver.
240
241To use Postgres as your database, first create a Soft Serve database:
242
243```sh
244psql -h<hostname> -p<port> -U<user> -c 'CREATE DATABASE soft_serve'
245```
246
247Then set the database _data source_ to point to your Postgres database. For instance, if you're running Postgres locally, using the default user `postgres` and using a database name `soft_serve`, you would have this config in your config file or environment variable:
248
249```
250db:
251  driver: "postgres"
252  data_source: "postgres://postgres@localhost:5432/soft_serve?sslmode=disable"
253```
254
255Environment variables equivalent:
256
257```sh
258SOFT_SERVE_DB_DRIVER=postgres \
259SOFT_SERVE_DB_DATA_SOURCE="postgres://postgres@localhost:5432/soft_serve?sslmode=disable" \
260soft serve
261```
262
263You can specify a database connection password in the _data source_ url. For example, `postgres://myuser:dbpass@localhost:5432/my_soft_serve_db`.
264
265#### LFS Configuration
266
267Soft Serve supports both Git LFS [HTTP](https://github.com/git-lfs/git-lfs/blob/main/docs/api/README.md) and [SSH](https://github.com/git-lfs/git-lfs/blob/main/docs/proposals/ssh_adapter.md) protocols out of the box, there is no need to do any extra set up.
268
269Use the `lfs` config section to customize your Git LFS server.
270
271> **Note**: The pure-SSH transfer is disabled by default.
272
273## Server Access
274
275Soft Serve at its core manages your server authentication and authorization. Authentication verifies the identity of a user, while authorization determines their access rights to a repository.
276
277To manage the server users, access, and repos, you can use the SSH command line interface.
278
279Try `ssh localhost -i ~/.ssh/id_ed25519 -p 23231 help` for more info. Make sure
280you use your key here.
281
282For ease of use, instead of specifying the key, port, and hostname every time
283you SSH into Soft Serve, add your own Soft Serve instance entry to your SSH
284config. For instance, to use `ssh soft` instead of typing `ssh localhost -i
285~/.ssh/id_ed25519 -p 23231`, we can define a `soft` entry in our SSH config
286file `~/.ssh/config`.
287
288```conf
289Host soft
290  HostName localhost
291  Port 23231
292  IdentityFile ~/.ssh/id_ed25519
293```
294
295Now, we can do `ssh soft` to SSH into Soft Serve. Since `git` is also aware of
296this config, you can use `soft` as the hostname for your clone commands.
297
298```sh
299git clone ssh://soft/dotfiles
300# make changes
301# add & commit
302git push origin main
303```
304
305> **Note** The `-i` part will be omitted in the examples below for brevity. You
306> can add your server settings to your sshconfig for quicker access.
307
308### Authentication
309
310Everything that needs authentication is done using SSH. Make sure you have
311added an entry for your Soft Serve instance in your `~/.ssh/config` file.
312
313By default, Soft Serve gives ready-only permission to anonymous connections to
314any of the above protocols. This is controlled by two settings `anon-access`
315and `allow-keyless`.
316
317- `anon-access`: Defines the access level for anonymous users. Available
318  options are `no-access`, `read-only`, `read-write`, and `admin-access`.
319  Default is `read-only`.
320- `allow-keyless`: Whether to allow connections that doesn't use keys to pass.
321  Setting this to `false` would disable access to SSH keyboard-interactive,
322  HTTP, and Git protocol connections. Default is `true`.
323
324```sh
325$ ssh -p 23231 localhost settings
326Manage server settings
327
328Usage:
329  ssh -p 23231 localhost settings [command]
330
331Available Commands:
332  allow-keyless Set or get allow keyless access to repositories
333  anon-access   Set or get the default access level for anonymous users
334
335Flags:
336  -h, --help   help for settings
337
338Use "ssh -p 23231 localhost settings [command] --help" for more information about a command.
339```
340
341> **Note** These settings can only be changed by admins.
342
343When `allow-keyless` is disabled, connections that don't use SSH Public Key
344authentication will get denied. This means cloning repos over HTTP(s) or git://
345will get denied.
346
347Meanwhile, `anon-access` controls the access level granted to connections that
348use SSH Public Key authentication but are not registered users. The default
349setting for this is `read-only`. This will grant anonymous connections that use
350SSH Public Key authentication `read-only` access to public repos.
351
352`anon-access` is also used in combination with `allow-keyless` to determine the
353access level for HTTP(s) and git:// clone requests.
354
355#### SSH
356
357Soft Serve doesn't allow duplicate SSH public keys for users. A public key can be associated with one user only. This makes SSH authentication simple and straight forward, add your public key to your Soft Serve user to be able to access Soft Serve.
358
359#### HTTP
360
361You can generate user access tokens through the SSH command line interface. Access tokens can have an optional expiration date. Use your access token as the basic auth user to access your Soft Serve repos through HTTP.
362
363```sh
364# Create a user token
365ssh -p 23231 localhost token create 'my new token'
366ss_1234abc56789012345678901234de246d798fghi
367
368# Or with an expiry date
369ssh -p 23231 localhost token create --expires-in 1y 'my other token'
370ss_98fghi1234abc56789012345678901234de246d7
371```
372
373Now you can access to repos that require `read-write` access.
374
375```sh
376git clone http://ss_98fghi1234abc56789012345678901234de246d7@localhost:23232/my-private-repo.git my-private-repo
377# Make changes and push
378```
379
380### Authorization
381
382Soft Serve offers a simple access control. There are four access levels,
383no-access, read-only, read-write, and admin-access.
384
385`admin-access` has full control of the server and can make changes to users and repos.
386
387`read-write` access gets full control of repos.
388
389`read-only` can read public repos.
390
391`no-access` denies access to all repos.
392
393## User Management
394
395Admins can manage users and their keys using the `user` command. Once a user is
396created and has access to the server, they can manage their own keys and
397settings.
398
399To create a new user simply use `user create`:
400
401```sh
402# Create a new user
403ssh -p 23231 localhost user create beatrice
404
405# Add user keys
406ssh -p 23231 localhost user add-pubkey beatrice ssh-rsa AAAAB3Nz...
407ssh -p 23231 localhost user add-pubkey beatrice ssh-ed25519 AAAA...
408
409# Create another user with public key
410ssh -p 23231 localhost user create frankie '-k "ssh-ed25519 AAAATzN..."'
411
412# Need help?
413ssh -p 23231 localhost user help
414```
415
416Once a user is created, they get `read-only` access to public repositories.
417They can also create new repositories on the server.
418
419Users can manage their keys using the `pubkey` command:
420
421```sh
422# List user keys
423ssh -p 23231 localhost pubkey list
424
425# Add key
426ssh -p 23231 localhost pubkey add ssh-ed25519 AAAA...
427
428# Wanna change your username?
429ssh -p 23231 localhost set-username yolo
430
431# To display user info
432ssh -p 23231 localhost info
433```
434
435## Repositories
436
437You can manage repositories using the `repo` command.
438
439```sh
440# Run repo help
441$ ssh -p 23231 localhost repo help
442Manage repositories
443
444Usage:
445  ssh -p 23231 localhost repo [command]
446
447Aliases:
448  repo, repos, repository, repositories
449
450Available Commands:
451  blob         Print out the contents of file at path
452  branch       Manage repository branches
453  collab       Manage collaborators
454  create       Create a new repository
455  delete       Delete a repository
456  description  Set or get the description for a repository
457  hide         Hide or unhide a repository
458  import       Import a new repository from remote
459  info         Get information about a repository
460  is-mirror    Whether a repository is a mirror
461  list         List repositories
462  private      Set or get a repository private property
463  project-name Set or get the project name for a repository
464  rename       Rename an existing repository
465  tag          Manage repository tags
466  tree         Print repository tree at path
467
468Flags:
469  -h, --help   help for repo
470
471Use "ssh -p 23231 localhost repo [command] --help" for more information about a command.
472```
473
474To use any of the above `repo` commands, a user must be a collaborator in the repository. More on this below.
475
476### Creating Repositories
477
478To create a repository, first make sure you are a registered user. Use the
479`repo create <repo>` command to create a new repository:
480
481```sh
482# Create a new repository
483ssh -p 23231 localhost repo create icecream
484
485# Create a repo with description
486ssh -p 23231 localhost repo create icecream '-d "This is an Ice Cream description"'
487
488# ... and project name
489ssh -p 23231 localhost repo create icecream '-d "This is an Ice Cream description"' '-n "Ice Cream"'
490
491# I need my repository private!
492ssh -p 23231 localhost repo create icecream -p '-d "This is an Ice Cream description"' '-n "Ice Cream"'
493
494# Help?
495ssh -p 23231 localhost repo create -h
496```
497
498Or you can add your Soft Serve server as a remote to any existing repo, given
499you have write access, and push to remote:
500
501```
502git remote add origin ssh://localhost:23231/icecream
503```
504
505After youāve added the remote just go ahead and push. If the repo doesnāt exist
506on the server itāll be created.
507
508```
509git push origin main
510```
511
512Repositories can be nested too:
513
514```sh
515# Create a new nested repository
516ssh -p 23231 localhost repo create charmbracelet/icecream
517
518# Or ...
519git remote add charm ssh://localhost:23231/charmbracelet/icecream
520git push charm main
521```
522
523### Deleting Repositories
524
525You can delete repositories using the `repo delete <repo>` command.
526
527```sh
528ssh -p 23231 localhost repo delete icecream
529```
530
531### Renaming Repositories
532
533Use the `repo rename <old> <new>` command to rename existing repositories.
534
535```sh
536ssh -p 23231 localhost repo rename icecream vanilla
537```
538
539### Repository Collaborators
540
541Sometimes you want to restrict write access to certain repositories. This can
542be achieved by adding a collaborator to your repository.
543
544Use the `repo collab <command> <repo>` command to manage repo collaborators.
545
546```sh
547# Add collaborator to soft-serve
548ssh -p 23231 localhost repo collab add soft-serve frankie
549
550# Add collaborator with a specific access level
551ssh -p 23231 localhost repo collab add soft-serve beatrice read-only
552
553# Remove collaborator
554ssh -p 23231 localhost repo collab remove soft-serve beatrice
555
556# List collaborators
557ssh -p 23231 localhost repo collab list soft-serve
558```
559
560### Repository Metadata
561
562You can also change the repo's description, project name, whether it's private,
563etc using the `repo <command>` command.
564
565```sh
566# Set description for repo
567ssh -p 23231 localhost repo description icecream "This is a new description"
568
569# Hide repo from listing
570ssh -p 23231 localhost repo hidden icecream true
571
572# List repository info (branches, tags, description, etc)
573ssh -p 23231 localhost repo icecream info
574```
575
576To make a repository private, use `repo private <repo> [true|false]`. Private
577repos can only be accessed by admins and collaborators.
578
579```sh
580ssh -p 23231 localhost repo icecream private true
581```
582
583### Repository Branches & Tags
584
585Use `repo branch` and `repo tag` to list, and delete branches or tags. You can
586also use `repo branch default` to set or get the repository default branch.
587
588### Repository Tree
589
590To print a file tree for the project, just use the `repo tree` command along with
591the repo name as the SSH command to your Soft Serve server:
592
593```sh
594ssh -p 23231 localhost repo tree soft-serve
595```
596
597You can also specify the sub-path and a specific reference or branch.
598
599```sh
600ssh -p 23231 localhost repo tree soft-serve server/config
601ssh -p 23231 localhost repo tree soft-serve main server/config
602```
603
604From there, you can print individual files using the `repo blob` command:
605
606```sh
607ssh -p 23231 localhost repo blob soft-serve cmd/soft/root.go
608```
609
610You can add the `-c` flag to enable syntax coloring and `-l` to print line
611numbers:
612
613```sh
614ssh -p 23231 localhost repo blob soft-serve cmd/soft/root.go -c -l
615
616```
617
618Use `--raw` to print raw file contents. This is useful for dumping binary data.
619
620## The Soft Serve TUI
621
622<img src="https://stuff.charm.sh/soft-serve/soft-serve-demo-commit.png" width="750" alt="TUI example showing a diff">
623
624Soft Serve serves a TUI over SSH for browsing repos, viewing files and commits,
625and grabbing clone commands:
626
627```sh
628ssh localhost -p 23231
629```
630
631It's also possible to ālinkā to a specific repo:
632
633```sh
634ssh -p 23231 localhost -t soft-serve
635```
636
637You can copy text to your clipboard over SSH. For instance, you can press
638<kbd>c</kbd> on the highlighted repo in the menu to copy the clone command
639[^osc52].
640
641[^osc52]:
642    Copying over SSH depends on your terminal support of OSC52. Refer to
643    [go-osc52](https://github.com/aymanbagabas/go-osc52) for more information.
644
645## Hooks
646
647Soft Serve supports git server-side hooks `pre-receive`, `update`,
648`post-update`, and `post-receive`. This means you can define your own hooks to
649run on repository push events. Hooks can be defined as a per-repository hook,
650and/or global hooks that run for all repositories.
651
652You can find per-repository hooks under the repository `hooks` directory.
653
654Globs hooks can be found in your `SOFT_SERVE_DATA_PATH` directory under
655`hooks`. Defining global hooks is useful if you want to run CI/CD for example.
656
657Here's an example of sending a message after receiving a push event. Create an
658executable file `<data path>/hooks/update`:
659
660```sh
661#!/bin/sh
662#
663# An example hook script to echo information about the push
664# and send it to the client.
665
666refname="$1"
667oldrev="$2"
668newrev="$3"
669
670# Safety check
671if [ -z "$GIT_DIR" ]; then
672        echo "Don't run this script from the command line." >&2
673        echo " (if you want, you could supply GIT_DIR then run" >&2
674        echo "  $0 <ref> <oldrev> <newrev>)" >&2
675        exit 1
676fi
677
678if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then
679        echo "usage: $0 <ref> <oldrev> <newrev>" >&2
680        exit 1
681fi
682
683# Check types
684# if $newrev is 0000...0000, it's a commit to delete a ref.
685zero=$(git hash-object --stdin </dev/null | tr '[0-9a-f]' '0')
686if [ "$newrev" = "$zero" ]; then
687        newrev_type=delete
688else
689        newrev_type=$(git cat-file -t $newrev)
690fi
691
692echo "Hi from Soft Serve update hook!"
693echo
694echo "RefName: $refname"
695echo "Change Type: $newrev_type"
696echo "Old SHA1: $oldrev"
697echo "New SHA1: $newrev"
698
699exit 0
700```
701
702Now, you should get a message after pushing changes to any repository.
703
704## A note about RSA keys
705
706Unfortunately, due to a shortcoming in Goās `x/crypto/ssh` package, Soft Serve
707does not currently support access via new SSH RSA keys: only the old SHA-1
708ones will work.
709
710Until we sort this out youāll either need an SHA-1 RSA key or a key with
711another algorithm, e.g. Ed25519. Not sure what type of keys you have?
712You can check with the following:
713
714```sh
715$ find ~/.ssh/id_*.pub -exec ssh-keygen -l -f {} \;
716```
717
718If youāre curious about the inner workings of this problem have a look at:
719
720- https://github.com/golang/go/issues/37278
721- https://go-review.googlesource.com/c/crypto/+/220037
722- https://github.com/golang/crypto/pull/197
723
724## Feedback
725
726Weād love to hear your thoughts on this project. Feel free to drop us a note!
727
728- [Twitter](https://twitter.com/charmcli)
729- [The Fediverse](https://mastodon.social/@charmcli)
730- [Discord](https://charm.sh/chat)
731
732## License
733
734[MIT](https://github.com/charmbracelet/soft-serve/raw/main/LICENSE)
735
736---
737
738Part of [Charm](https://charm.sh).
739
740<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
741
742Charmēē±å¼ęŗ ⢠Charm loves open source