diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0ef9f17
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+flist
+flist.exe
+test/
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..72dd69f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2024 ThreeFold
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..130875c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,27 @@
+build:
+ v fmt -w flist.v
+ v -o flist .
+ sudo ./flist install
+
+rebuild:
+ sudo flist uninstall
+ v fmt -w flist.v
+ v -o flist .
+ sudo ./flist install
+
+delete:
+ sudo flist uninstall
+
+build-win:
+ v fmt -w flist.v
+ v -o flist .
+ ./flist.exe install
+
+rebuild-win:
+ ./flist.exe uninstall
+ v fmt -w flist.v
+ v -o flist .
+ ./flist.exe install
+
+delete-win:
+ ./flist.exe uninstall
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..33021b3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,209 @@
+
Flist CLI in Vlang
+
+Table of Contents
+
+- [Introduction](#introduction)
+- [Prerequisites](#prerequisites)
+- [Makefile Installation](#makefile-installation)
+ - [Building and Installing with Makefile](#building-and-installing-with-makefile)
+ - [Rebuilding and Uninstalling with Makefile](#rebuilding-and-uninstalling-with-makefile)
+- [Manual Installation](#manual-installation)
+- [Available Commands](#available-commands)
+- [Usage](#usage)
+- [OS-Specific Instructions](#os-specific-instructions)
+ - [Linux](#linux)
+ - [macOS](#macos)
+ - [Windows](#windows)
+- [Troubleshooting](#troubleshooting)
+- [Development](#development)
+- [Contributing](#contributing)
+- [License](#license)
+
+---
+
+## Introduction
+
+Flist CLI is a tool that turns Dockerfiles and Docker images directly into Flist on the TF Flist Hub, passing through Docker Hub.
+
+## Prerequisites
+
+- [V programming language](https://vlang.io/) (latest version) installed on your system
+- [Docker Engine](https://docs.docker.com/engine/install/) installed and running (Linux)
+- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running (MacOS+Windows)
+- [Docker Hub](https://hub.docker.com/) account
+- [TF Hub](https://hub.grid.tf/) account and token
+- Makefile (optional)
+
+Read more on the TF Hub and Flist on the ThreeFold Manual [here](https://manual.grid.tf/documentation/developers/flist/flist.html).
+
+## Makefile Installation
+
+### Building and Installing with Makefile
+
+- To clone this repository, build the project, and install the CLI:
+ - MacOS and Linux
+ ```
+ git clone https://github.com/threefoldfoundation/flist_cli
+ cd flist_cli
+ make build
+ ```
+ - Windows
+ ```
+ git clone https://github.com/threefoldfoundation/flist_cli
+ cd flist_cli
+ make build-win
+ ```
+
+This will build the executable and install it to the appropriate system location.
+
+### Rebuilding and Uninstalling with Makefile
+
+You can use the following Makefile commands:
+
+- To rebuild and reinstall:
+ - MacOS and Linux
+ ```
+ make rebuild
+ ```
+ - Windows
+ ```
+ make rebuild-win
+ ```
+
+- To uninstall and remove the binary:
+ - MacOS and Linux
+ ```
+ make delete
+ ```
+ - Windows
+ ```
+ make delete-win
+ ```
+
+## Manual Installation
+
+You can install the Flist with the following commands. You do not need Makefile to use the Flist CLI.
+
+- Linux and MacOS
+ - Build
+ ```
+ v fmt -w flist.v
+ v -o flist .
+ sudo ./flist install
+ ```
+ - Rebuild
+ ```
+ sudo flist uninstall
+ v fmt -w flist.v
+ v -o flist .
+ sudo ./flist install
+ ```
+ - Delete
+ ```
+ sudo flist uninstall
+ ```
+- Windows
+ - Build
+ ```
+ v fmt -w flist.v
+ v -o flist .
+ ./flist.exe install
+ ```
+ - Rebuild
+ ```
+ ./flist.exe uninstall
+ v fmt -w flist.v
+ v -o flist .
+ ./flist.exe install
+ ```
+ - Delete
+ ```
+ ./flist.exe uninstall
+ ```
+
+## Available Commands
+
+After installation, you can use the `flist` command followed by various subcommands:
+
+```
+flist [arguments]
+```
+
+Run `flist` or `flist help` to see all available commands for your specific OS.
+
+- `install` - Install the Flist CLI
+- `uninstall` - Uninstall the Flist CLI
+- `login` - Log in to Docker Hub and save the Flist Hub token
+- `logout` - Log out of Docker Hub and remove the Flist Hub token
+- `push` - Build and push a Docker image to Docker Hub, then convert and push it as an Flist to Flist Hub
+- `delete` - Delete an Flist from Flist Hub
+- `rename` - Rename an Flist in Flist Hub
+- `ls` - List all Flists of the current user
+- `ls url` - List all Flists of the current user with full URLs
+- `help` - Display this help message
+
+## Usage
+
+A Linux user would use the following commands:
+
+```
+sudo ./flist install
+sudo flist uninstall
+flist login
+flist logout
+flist push :
+flist delete
+flist rename
+flist ls
+flist ls url
+flist help
+```
+
+## OS-Specific Instructions
+
+### Linux
+
+1. Ensure Docker Engine is installed and running.
+2. The `flist` executable will be installed to:
+ ```
+ /usr/local/bin/flist
+ ```
+
+### macOS
+
+1. Ensure Docker Desktop is installed and running.
+2. The `flist` executable will be installed to:
+ ```
+ /usr/local/bin/flist
+ ```
+
+### Windows
+
+1. Ensure Docker Desktop is installed and running.
+2. Run the program and installer in an admin PowerShell.
+3. The `flist.exe` executable will be installed to:
+ ```
+ C:\\Program Files\\flist\\flist.exe
+ ```
+
+## Troubleshooting
+
+- If you encounter permission issues, ensure you're running the command with appropriate privileges (e.g., as administrator on Windows or with `sudo` on Unix-like systems).
+- If you face issues with Docker commands, try logging out and logging back in to refresh your Docker credentials.
+- If you encounter compilation errors, ensure you have the latest version of V installed. To update v, run `v up`.
+
+## Development
+
+To modify the Flist CLI:
+
+1. Make your changes to the `flist.v` file.
+2. Rebuild the project using using the appropriate Make command.
+3. Test your changes thoroughly across different operating systems if possible.
+
+## Contributing
+
+Contributions are welcome! Please feel free to submit a Pull Request.
+
+## License
+
+[Apache 2.0 License](LICENSE)
\ No newline at end of file
diff --git a/flist.v b/flist.v
new file mode 100644
index 0000000..4849cf8
--- /dev/null
+++ b/flist.v
@@ -0,0 +1,577 @@
+import os
+import net.http
+import term
+import json
+
+const token_file = os.join_path(os.home_dir(), '.config', 'tfhubtoken')
+const docker_username_file = os.join_path(os.home_dir(), '.config', 'dockerusername')
+const config_dir = os.join_path(os.home_dir(), '.config')
+
+const binary_location = $if windows {
+ 'C:\\Program Files\\flist\\flist.exe'
+} $else {
+ '/usr/local/bin/flist'
+}
+
+const docker_cmd = $if windows {
+ 'docker'
+} $else {
+ 'sudo docker'
+}
+
+const info_msg = $if windows {
+ 'Note: Docker Desktop must be running to use the push function.\nInstall and uninstall functions require PowerShell with administrator privileges.\nOther functions require PowerShell (without admin privileges).\n'
+} $else $if linux {
+ 'Note: Docker Engine must be running to use the push function.\n'
+} $else $if macos {
+ 'Note: Docker Desktop must be running to use the push function.\n'
+}
+
+const flist_repo_folder = (' # Run this line in the Flist CLI repo folder')
+
+struct FlistItem {
+ name string
+}
+
+struct Payload {
+ username string
+}
+
+struct Response {
+ payload Payload
+}
+
+fn add_path_windows() {
+ // Define the new directory path to add
+ new_path := r'C:\Program Files\flist'
+
+ // Get the current PATH environment variable
+ current_path := os.getenv('PATH')
+
+ // Check if the new_path is already in the current PATH
+ if current_path.contains(new_path) {
+ println('The directory is already in the PATH.')
+ return
+ }
+
+ // Construct the new PATH by appending the new_path
+ new_env_path := '${current_path};${new_path}'
+
+ // Prepare the command to set the new PATH
+ cmd := r'setx PATH "' + new_env_path + '"'
+
+ // Execute the command to update the PATH
+ exit_code := os.system(cmd)
+
+ if exit_code == 0 {
+ success_message('\nFlist CLI directory added to the path. \nMake sure to load a new admin PowerShell to use the CLI.')
+ } else {
+ error_message('\nFailed to add the Flist CLI to the PATH. Please try running the script as an administrator.')
+ }
+}
+
+fn remove_path_windows() {
+ // Define the directory path to remove
+ path_to_remove := r'C:\Program Files\flist'
+
+ // Get the current PATH environment variable
+ current_path := os.getenv('PATH')
+
+ // Check if the path_to_remove is in the current PATH
+ if !current_path.contains(path_to_remove) {
+ println('The directory is not in the PATH.')
+ return
+ }
+
+ // Remove specified directory from the PATH
+ new_env_path := current_path.split(';').filter(it != path_to_remove).join(';')
+
+ // Prepare the command to set the new PATH
+ cmd := r'setx PATH "' + new_env_path + '"'
+
+ // Execute the command to update the PATH
+ exit_code := os.system(cmd)
+
+ if exit_code == 0 {
+ success_message('\nThe Flist CLI directory has been removed from the path.')
+ } else {
+ error_message('\nFailed to remove the Flist CLI from the PATH. Please try running the script as an administrator.')
+ }
+}
+
+fn error_message(msg string) {
+ println(term.red('\nError: ') + msg)
+ println(term.yellow("Run 'flist help' for usage information.\n"))
+}
+
+fn success_message(msg string) {
+ println(term.green('\n' + msg + '\n'))
+}
+
+fn info_message(msg string) {
+ println(term.cyan('\n' + msg + '\n'))
+}
+
+fn create_box(content []string, padding int) string {
+ mut max_width := 0
+ for line in content {
+ clean_line := term.strip_ansi(line)
+ if clean_line.len > max_width {
+ max_width = clean_line.len
+ }
+ }
+ max_width += padding * 2
+
+ separator := '━'.repeat(max_width + 2) // +2 for left and right borders
+ mut box_content := term.cyan('┏${separator}┓') + '\n'
+
+ for line in content {
+ clean_line := term.strip_ansi(line)
+ padding_left := ' '.repeat(padding)
+ padding_right := ' '.repeat(max_width - clean_line.len)
+ box_content += term.cyan('┃') + padding_left + line + padding_right + term.cyan('┃') +
+ '\n'
+ }
+
+ box_content += term.cyan('┗${separator}┛')
+ return box_content
+}
+
+fn install() {
+ info_message('Installing Flist CLI...')
+ current_exe := os.executable()
+ if os.exists(current_exe) {
+ os.mkdir_all(os.dir(binary_location)) or {
+ error_message('Failed to create directory for binary: ${err}')
+ exit(1)
+ }
+ os.cp(current_exe, binary_location) or {
+ error_message('Failed to copy binary to path: ${err}')
+ exit(1)
+ }
+ os.chmod(binary_location, 0o755) or {
+ error_message('Failed to change permissions to binary at path: ${err}')
+ exit(1)
+ }
+ $if windows {
+ add_path_windows()
+ }
+ success_message('Flist CLI has been installed to ' + binary_location)
+ info_message("Run 'flist help' to see all commands.")
+ } else {
+ error_message('Cannot find the executable file')
+ exit(1)
+ }
+}
+
+fn uninstall() {
+ info_message('Uninstalling Flist CLI...')
+
+ if os.exists(binary_location) {
+ // Remove the binary file
+ os.rm(binary_location) or {
+ error_message('Failed to remove the binary at path: ${err}')
+ exit(1)
+ }
+ success_message('Flist CLI has been removed from ' + binary_location)
+ } else {
+ info_message('Flist CLI is not installed at ' + binary_location)
+ }
+
+ $if windows {
+ remove_path_windows()
+ }
+}
+
+fn login() {
+ mut token_exists := os.exists(token_file)
+ os.mkdir_all(config_dir) or {
+ error_message('Failed to create config folder for token and Docker username files: ${err}')
+ exit(1)
+ }
+ if !token_exists {
+ tfhub_token := os.input('Please enter your TF Hub token: ')
+ os.write_file(token_file, tfhub_token) or {
+ error_message('Failed to write TF Hub token to file: ${err}')
+ exit(1)
+ }
+ success_message('TF Hub token saved in ' + token_file)
+ } else {
+ info_message('Your TF Hub token is already saved.')
+ }
+
+ mut dockername_exists := os.exists(docker_username_file)
+ mut docker_username := ''
+
+ if !dockername_exists {
+ docker_username = os.input('Please enter your Docker username: ')
+ os.write_file(docker_username_file, docker_username) or {
+ error_message('Failed to write Docker username to file: ${err}')
+ exit(1)
+ }
+ success_message('Docker username saved in ' + docker_username_file)
+ }
+
+ docker_username = os.read_file(docker_username_file) or {
+ error_message('Failed to read the Docker username from file: ${err}')
+ exit(1)
+ }
+
+ info_message('Enter your Docker password')
+ os.system('${docker_cmd} login -u ${docker_username}')
+
+ success_message('TF Hub and Docker Hub login process completed.')
+}
+
+fn logout() {
+ if os.exists(token_file) {
+ os.rm(token_file) or {
+ error_message('Failed to remove TF Hub token file at config directory: ${err}')
+ exit(1)
+ }
+ success_message('Your TF Hub token has been removed')
+ } else {
+ info_message('Your TF Hub token was already not present.')
+ }
+
+ if os.exists(docker_username_file) {
+ os.rm(docker_username_file) or {
+ error_message('Failed to remove Docker username file in config folder: ${err}')
+ exit(1)
+ }
+ success_message('Your Docker username has been removed from the config folder.')
+ } else {
+ info_message('Your Docker username was already not present in the config folder.')
+ }
+
+ exit_code := os.system('${docker_cmd} logout')
+ if exit_code != 0 {
+ error_message('Failed to log out from Docker Hub.')
+ }
+
+ success_message('You are now logged out of Docker Hub and your TF Hub token has been removed.')
+}
+
+fn push(tag string) {
+ docker_user := os.read_file(docker_username_file) or {
+ error_message("No Docker username found. Please run 'flist login' first.")
+ exit(1)
+ }
+
+ info_message('Docker username: ${docker_user}')
+
+ full_tag := '${docker_user}/${tag}'
+
+ tfhub_token := os.read_file(token_file) or {
+ error_message("No TF Hub token found. Please run 'flist login' first.")
+ exit(1)
+ }
+
+ info_message('Starting Docker build')
+ if os.system('${docker_cmd} buildx build -t ${full_tag} .') != 0 {
+ error_message('Docker build failed')
+ exit(1)
+ }
+
+ info_message('Finished local Docker build, now pushing to Docker Hub')
+ if os.system('${docker_cmd} push ${full_tag}') != 0 {
+ error_message('Docker push failed')
+ exit(1)
+ }
+
+ info_message('Converting Docker image to Flist now...')
+
+ url := 'https://hub.grid.tf/api/flist/me/docker'
+ data := 'image=${full_tag}'
+
+ mut config := http.FetchConfig{
+ url: url
+ method: .post
+ data: data
+ header: http.new_header(
+ key: .authorization
+ value: 'bearer ${tfhub_token}'
+ )
+ }
+
+ config.header.add_custom('Content-Type', 'application/x-www-form-urlencoded') or {
+ error_message('Add custom failed: ${err}')
+ exit(1)
+ }
+
+ response := http.fetch(config) or {
+ error_message('HTTP POST request failed: ${err}')
+ exit(1)
+ }
+
+ if response.status_code == 200 {
+ hub_user := get_hub_username(tfhub_token) or {
+ error_message('Failed to get TF Hub username')
+ exit(1)
+ }
+
+ flist_name := full_tag.replace_each([':', '-', '/', '-']) + '.flist'
+ flist_url := 'https://hub.grid.tf/${hub_user}/${flist_name}'
+
+ success_content := [
+ term.bold(term.green('Success!') +
+ ' Your Flist has been created and pushed to the TF Hub.'),
+ '',
+ term.bold('Flist Details:'),
+ term.yellow('Name: ') + flist_name,
+ term.yellow('User: ') + hub_user,
+ term.yellow('URL: ') + flist_url,
+ '',
+ 'You can access your Flist using the URL above.',
+ 'To manage your Flists, use the following commands:',
+ term.yellow(' flist ls ') + ' - List all your Flists',
+ term.yellow(' flist delete') + ' - Delete an Flist',
+ term.yellow(' flist rename') + ' - Rename an Flist',
+ ]
+
+ println(create_box(success_content, 2))
+ } else {
+ error_message('Request failed with status code: ${response.status_code}')
+ println('Response body:')
+ println(response.body)
+ exit(1)
+ }
+}
+
+fn delete(flist_name string) {
+ tfhub_token := os.read_file(token_file) or {
+ error_message("No TF Hub token found. Please run 'flist login' first.")
+ exit(1)
+ }
+
+ info_message('Deleting Flist: ' + flist_name)
+ url := 'https://hub.grid.tf/api/flist/me/' + flist_name
+ config := http.FetchConfig{
+ url: url
+ method: .delete
+ header: http.new_header(key: .authorization, value: 'bearer ' + tfhub_token)
+ }
+
+ response := http.fetch(config) or {
+ error_message('Failed to send delete request: ' + err.msg())
+ exit(1)
+ }
+
+ if response.status_code == 200 {
+ success_message('Deletion request sent successfully.')
+ } else {
+ error_message('Deletion request failed with status code: ' + response.status_code.str())
+ }
+}
+
+fn rename(flist_name string, new_flist_name string) {
+ tfhub_token := os.read_file(token_file) or {
+ error_message("No TF Hub token found. Please run 'flist login' first.")
+ exit(1)
+ }
+
+ info_message('Renaming Flist: ' + flist_name + ' to ' + new_flist_name)
+ url := 'https://hub.grid.tf/api/flist/me/' + flist_name + '/rename/' + new_flist_name
+ config := http.FetchConfig{
+ url: url
+ method: .get
+ header: http.new_header(key: .authorization, value: 'bearer ' + tfhub_token)
+ }
+
+ response := http.fetch(config) or {
+ error_message('Failed to send rename request: ' + err.msg())
+ exit(1)
+ }
+
+ if response.status_code == 200 {
+ success_message('Rename request sent successfully.')
+ } else {
+ error_message('Rename request failed with status code: ' + response.status_code.str())
+ }
+}
+
+fn get_hub_username(tfhub_token string) ?string {
+ url := 'https://hub.grid.tf/api/flist/me'
+
+ config := http.FetchConfig{
+ url: url
+ method: .get
+ header: http.new_header(
+ key: .authorization
+ value: 'bearer ${tfhub_token}'
+ )
+ }
+
+ response := http.fetch(config) or {
+ error_message('Failed to fetch hub username: ${err}')
+ return none
+ }
+
+ if response.status_code != 200 {
+ error_message('Failed to fetch hub username. Status code: ${response.status_code}')
+ return none
+ }
+
+ parsed_response := json.decode(Response, response.body) or {
+ error_message('Failed to parse JSON response: ${err}')
+ return none
+ }
+
+ if parsed_response.payload.username != '' {
+ return parsed_response.payload.username
+ }
+
+ error_message('Username not found in response')
+ return none
+}
+
+fn ls(show_url bool) {
+ tfhub_token := os.read_file(token_file) or {
+ error_message("No TF Hub token found. Please run 'flist login' first.")
+ exit(1)
+ }
+
+ hub_user := get_hub_username(tfhub_token) or {
+ error_message('Failed to get hub username')
+ exit(1)
+ }
+
+ url := 'https://hub.grid.tf/api/flist/${hub_user}'
+
+ config := http.FetchConfig{
+ url: url
+ method: .get
+ header: http.new_header(
+ key: .authorization
+ value: 'bearer ${tfhub_token}'
+ )
+ }
+
+ response := http.fetch(config) or {
+ error_message('Failed to fetch data: ${err}')
+ exit(1)
+ }
+
+ if response.status_code != 200 {
+ error_message('Failed to fetch data. Status code: ${response.status_code}')
+ exit(1)
+ }
+
+ data := json.decode([]FlistItem, response.body) or {
+ error_message('Failed to parse JSON: ${err}')
+ exit(1)
+ }
+
+ mut content := [term.bold('Flists for user ' + term.green(hub_user) + ':')]
+ for item in data {
+ if show_url {
+ content << term.yellow('> ') + 'https://hub.grid.tf/' + hub_user + '/' + item.name
+ } else {
+ content << term.yellow('> ') + item.name
+ }
+ }
+
+ println(create_box(content, 2))
+}
+
+fn help() {
+ welcome_msg := term.bold(term.green('Welcome to the Flist CLI!'))
+ println(create_box([welcome_msg], 2))
+
+ println('This tool turns Dockerfiles and Docker images directly into Flists on the TF Flist Hub, passing by the Docker Hub.\n')
+ println(term.cyan(info_msg))
+ println(term.bold('Available commands:'))
+ println(term.cyan(' install ') + ' - Install the Flist CLI')
+ println(term.cyan(' uninstall') + ' - Uninstall the Flist CLI')
+ println(term.cyan(' login ') + ' - Log in to Docker Hub and save the Flist Hub token')
+ println(term.cyan(' logout ') + ' - Log out of Docker Hub and remove the Flist Hub token')
+ println(term.cyan(' push ') +
+ ' - Build and push a Docker image to Docker Hub, then convert and push it as an Flist to Flist Hub')
+ println(term.cyan(' delete ') + ' - Delete an Flist from Flist Hub')
+ println(term.cyan(' rename ') + ' - Rename an Flist in Flist Hub')
+ println(term.cyan(' ls ') + ' - List all Flists of the current user')
+ println(term.cyan(' ls url ') + ' - List all Flists of the current user with full URLs')
+ println(term.cyan(' help ') + ' - Display this help message\n')
+ println(term.bold('Usage:'))
+ $if linux {
+ println(term.yellow(' sudo ./flist install') + term.cyan(flist_repo_folder))
+ println(term.yellow(' sudo flist uninstall'))
+ } $else $if macos {
+ println(term.yellow(' sudo ./flist install') + term.cyan(flist_repo_folder))
+ println(term.yellow(' flist uninstall'))
+ } $else $if windows {
+ println(term.yellow(' ./flist.exe install') + term.cyan(flist_repo_folder))
+ println(term.yellow(' ./flist.exe uninstall') + term.cyan(flist_repo_folder))
+ }
+ println(term.yellow(' flist login'))
+ println(term.yellow(' flist logout'))
+ println(term.yellow(' flist push :'))
+ println(term.yellow(' flist delete '))
+ println(term.yellow(' flist rename '))
+ println(term.yellow(' flist ls'))
+ println(term.yellow(' flist ls url'))
+ println(term.yellow(' flist help\n'))
+}
+
+fn main() {
+ if os.args.len == 1 {
+ help()
+ return
+ }
+
+ match os.args[1] {
+ 'install' {
+ install()
+ }
+ 'uninstall' {
+ uninstall()
+ }
+ 'push' {
+ if os.args.len == 3 {
+ push(os.args[2])
+ } else {
+ error_message("Incorrect number of arguments for 'push'.")
+ exit(1)
+ }
+ }
+ 'login' {
+ login()
+ }
+ 'logout' {
+ logout()
+ }
+ 'delete' {
+ if os.args.len == 3 {
+ delete(os.args[2])
+ } else {
+ error_message("Incorrect number of arguments for 'delete'.")
+ exit(1)
+ }
+ }
+ 'rename' {
+ if os.args.len == 4 {
+ rename(os.args[2], os.args[3])
+ } else {
+ error_message("Incorrect number of arguments for 'rename'.")
+ exit(1)
+ }
+ }
+ 'ls' {
+ if os.args.len == 2 {
+ ls(false)
+ } else if os.args.len == 3 && os.args[2] == 'url' {
+ ls(true)
+ } else {
+ error_message("Incorrect usage of 'ls'. Use 'ls' or 'ls url'.")
+ exit(1)
+ }
+ }
+ 'help' {
+ help()
+ }
+ else {
+ error_message('Unknown command: ' + os.args[1])
+ exit(1)
+ }
+ }
+}