├── .devcontainer
└── devcontainer.json
├── .env.example
├── .gitignore
├── .sesskey
├── LICENSE
├── README.md
├── assets
├── fonts
│ ├── geist-mono
│ │ ├── GeistMonoVF.woff
│ │ └── GeistMonoVF.woff2
│ └── geist
│ │ ├── Geist-Medium.woff
│ │ ├── Geist-Medium.woff2
│ │ ├── GeistVF.woff
│ │ └── GeistVF.woff2
├── footer-shapes.svg
├── hero-shapes.svg
├── icons
│ ├── accordion-minus-icon.svg
│ ├── accordion-plus-icon.svg
│ ├── arrow-left.svg
│ ├── arrow-right.svg
│ ├── arrow-up-right-white.svg
│ ├── arrow-up-right.svg
│ ├── copy-icon.svg
│ ├── dot.svg
│ ├── github-mark-white.svg
│ ├── github-mark.svg
│ ├── loading-spinner.svg
│ ├── magic-wand.svg
│ ├── minus-icon.svg
│ ├── plus-icon.svg
│ ├── regenerate.svg
│ ├── youtube-icon.svg
│ └── youtube.svg
├── logo.svg
├── og-image.png
├── og-sq.png
└── testimonials
│ ├── daniel-roy-greenfield.png
│ ├── daniel-roy.png
│ ├── giles-thomas.png
│ ├── guillermo-rauch.jpg
│ └── jake-cooper.png
├── content.py
├── css
├── input.css
├── main.css
└── tailwind.css
├── data
└── devcontainers.db
├── dependency_tree.txt
├── favicon-dark.ico
├── favicon.ico
├── helpers
├── __init__.py
├── devcontainer_helpers.py
├── github_helpers.py
├── jinja_helper.py
├── openai_helpers.py
└── token_helpers.py
├── js
└── main.js
├── main.py
├── migrate.py
├── models.py
├── prompts
└── devcontainer.jinja
├── requirements.txt
├── schemas.py
├── schemas
└── devContainer.base.schema.json
├── supabase_client.py
├── tailwind.config.js
└── test.py
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
2 | // README at: https://github.com/devcontainers/templates/tree/main/src/python
3 | {
4 | "name": "FastHTML",
5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
6 | "image": "mcr.microsoft.com/devcontainers/python:3.12-bookworm",
7 |
8 | // Features to add to the dev container. More info: https://containers.dev/features.
9 | // "features": {},
10 |
11 | // Configure tool-specific properties.
12 | "customizations": {
13 | // Configure properties specific to VS Code.
14 | "vscode": {
15 | // Set *default* container specific settings.json values on container create.
16 | "settings": {
17 | "python.defaultInterpreterPath": "/usr/bin/python"
18 | },
19 |
20 | // Add the IDs of extensions you want installed when the container is created.
21 | "extensions": [
22 | "streetsidesoftware.code-spell-checker",
23 | "qwtel.sqlite-viewer",
24 | "GitHub.copilot",
25 | "GitHub.copilot-chat",
26 | "ms-python.python",
27 | "ms-python.vscode-pylance",
28 | "ms-python.isort",
29 | "njpwerner.autodocstring",
30 | "ms-python.pylint", // Pylint extension
31 | "ms-python.flake8", // Flake8 extension
32 | "ms-python.black-formatter", // Black formatter extension
33 | "ms-python.autopep8" // autopep8 extension
34 | ]
35 | }
36 | },
37 |
38 | // Use 'portsAttributes' to set default properties for specific forwarded ports.
39 | // More info: https://containers.dev/implementors/json_reference/#port-attributes
40 | "portsAttributes": {
41 | "5001": {
42 | "label": "FastHTML",
43 | "onAutoForward": "openBrowser"
44 | }
45 | },
46 |
47 | // https://containers.dev/implementors/json_reference/#lifecycle-scripts
48 | "postCreateCommand": "pip3 install -r requirements.txt || true && curl -fsSL cli.new | bash -s -- -y"
49 | // "postAttachCommand": "python main.py"
50 | }
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | AZURE_OPENAI_API_KEY=YOURKEY
2 | AZURE_OPENAI_ENDPOINT=https://YOURENDPOINT.openai.azure.com
3 | AZURE_OPENAI_API_VERSION=2024-02-01
4 | MODEL=gpt-4o-mini
5 | EMBEDDING=text-embedding-3-small
6 | EMBEDDING_MODEL_MAX_TOKENS=8192
7 | GITHUB_TOKEN=YOURTOKEN
8 | SUPABASE_URL=https://YOURLINK.supabase.co
9 | SUPABASE_KEY=your_supabase_key
10 | SUPABASE_DB_URL=your_supabase_db_url # use transaction pooler, example: postgresql://postgres.pnewjmyiuazlkssdmhsc:[YOUR-PASSWORD]@aws-0-us-east-1.pooler.supabase.com:6543/postgres?sslmode=require
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Database
2 | *.db
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | build/
15 | develop-eggs/
16 | dist/
17 | downloads/
18 | eggs/
19 | .eggs/
20 | lib/
21 | lib64/
22 | parts/
23 | sdist/
24 | var/
25 | wheels/
26 | share/python-wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | MANIFEST
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .nox/
46 | .coverage
47 | .coverage.*
48 | .cache
49 | nosetests.xml
50 | coverage.xml
51 | *.cover
52 | *.py,cover
53 | .hypothesis/
54 | .pytest_cache/
55 | cover/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 | db.sqlite3-journal
66 |
67 | # Flask stuff:
68 | instance/
69 | .webassets-cache
70 |
71 | # Scrapy stuff:
72 | .scrapy
73 |
74 | # Sphinx documentation
75 | docs/_build/
76 |
77 | # PyBuilder
78 | .pybuilder/
79 | target/
80 |
81 | # Jupyter Notebook
82 | .ipynb_checkpoints
83 |
84 | # IPython
85 | profile_default/
86 | ipython_config.py
87 |
88 | # pyenv
89 | # For a library or package, you might want to ignore these files since the code is
90 | # intended to run in multiple environments; otherwise, check them in:
91 | # .python-version
92 |
93 | # pipenv
94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
97 | # install all needed dependencies.
98 | #Pipfile.lock
99 |
100 | # poetry
101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
102 | # This is especially recommended for binary packages to ensure reproducibility, and is more
103 | # commonly ignored for libraries.
104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
105 | #poetry.lock
106 |
107 | # pdm
108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
109 | #pdm.lock
110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
111 | # in version control.
112 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
113 | .pdm.toml
114 | .pdm-python
115 | .pdm-build/
116 |
117 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
118 | __pypackages__/
119 |
120 | # Celery stuff
121 | celerybeat-schedule
122 | celerybeat.pid
123 |
124 | # SageMath parsed files
125 | *.sage.py
126 |
127 | # Environments
128 | .env
129 | .venv
130 | env/
131 | venv/
132 | ENV/
133 | env.bak/
134 | venv.bak/
135 |
136 | # Spyder project settings
137 | .spyderproject
138 | .spyproject
139 |
140 | # Rope project settings
141 | .ropeproject
142 |
143 | # mkdocs documentation
144 | /site
145 |
146 | # mypy
147 | .mypy_cache/
148 | .dmypy.json
149 | dmypy.json
150 |
151 | # Pyre type checker
152 | .pyre/
153 |
154 | # pytype static type analyzer
155 | .pytype/
156 |
157 | # Cython debug symbols
158 | cython_debug/
159 |
160 | # PyCharm
161 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
162 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
163 | # and can be added to the global gitignore or merged into this file. For a more nuclear
164 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
165 | #.idea/
166 |
167 | # OS generated files #
168 | ######################
169 | .DS_Store
170 | .DS_Store?
171 | ._*
172 | .Spotlight-V100
173 | .Trashes
174 | ehthumbs.db
175 | Thumbs.db
--------------------------------------------------------------------------------
/.sesskey:
--------------------------------------------------------------------------------
1 | 48023861-a013-4232-a173-dae96c7ec665
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2024 Daytona Platforms, Inc.
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://app.codeanywhere.com/#https://github.com/daytonaio/devcontainer-generator)
2 |
3 | # DevContainer Generator
4 |
5 | Welcome to the **devcontainer-generator** project! This tool helps you automatically generate `devcontainer.json` files for your development environments based on the structure and contents of a given GitHub repository.
6 |
7 | ## Table of Contents
8 |
9 | 1. [Project Structure](#project-structure)
10 | 2. [Installation](#installation)
11 | 3. [Configuration](#configuration)
12 | 4. [Usage](#usage)
13 | 5. [Setting Up Daytona Workspace](#setting-up-daytona-workspace)
14 | 6. [Contributing](#contributing)
15 | 7. [License](#license)
16 |
17 | ## Project Structure
18 |
19 | ```
20 | devcontainer-generator
21 | ├── tests.ipynb
22 | ├── requirements.txt
23 | ├── schemas
24 | │ └── devContainer.base.schema.json
25 | ├── README.md
26 | ├── main.py
27 | └── data
28 | └── devcontainers.db
29 | ```
30 |
31 | - **tests.ipynb**: Jupyter Notebook containing tests and exploratory data analysis.
32 | - **requirements.txt**: List of dependencies needed to run the project.
33 | - **schemas/**: Directory containing the JSON schema for the `devcontainer.json` file.
34 | - **README.md**: This documentation file.
35 | - **main.py**: Main script to generate `devcontainer.json` files.
36 | - **data/**: Directory containing the SQLite database files.
37 |
38 | ## Installation
39 |
40 | To run this project in Daytona, you'll need to have Daytona installed. Follow these steps to set up the project:
41 |
42 | 1. **Install Daytona**:
43 | ```bash
44 | (curl -L https://download.daytona.io/daytona/install.sh | sudo bash) && daytona server stop && daytona server -y && daytona
45 | ```
46 |
47 | 2. **Create new project and run IDE**:
48 | ```bash
49 | daytona create https://github.com/daytonaio/devcontainer-generator.git
50 | ```
51 |
52 | 3. **Set up environment variables**:
53 | Create a `.env` file in the project's root directory and add the following environment variables:
54 | ```dotenv
55 | AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint
56 | AZURE_OPENAI_API_KEY=your_azure_openai_api_key
57 | AZURE_OPENAI_API_VERSION=your_azure_openai_api_version
58 | MODEL=your_model_name
59 | GITHUB_TOKEN=your_github_token
60 | SUPABASE_URL=your_supabase_url
61 | SUPABASE_KEY=your_supabase_api_key
62 | SUPABASE_DB_URL=your_supabase_db_url
63 | ```
64 |
65 | ## Configuration
66 |
67 | ### Azure OpenAI and Instructor Clients
68 |
69 | Ensure the following environment variables are set in your `.env` file:
70 |
71 | ```dotenv
72 | AZURE_OPENAI_ENDPOINT=your_azure_openai_endpoint
73 | AZURE_OPENAI_API_KEY=your_azure_openai_api_key
74 | AZURE_OPENAI_API_VERSION=your_azure_openai_api_version
75 | MODEL=your_model_name
76 | GITHUB_TOKEN=your_github_token
77 |
78 | ```
79 |
80 | ### Supabase Database Setup
81 |
82 | Run the `migrate.py` script to create the `devcontainers` table in Supabase. Here's how you can do it:
83 |
84 | ```bash
85 | python migrate.py
86 | ```
87 |
88 | After creating the table, you can use the Supabase client in your Python code to interact with the database.
89 |
90 | ### JSON Schema
91 |
92 | The JSON schema for the `devcontainer.json` file is located in `schemas/devContainer.base.schema.json`.
93 |
94 | ## Usage
95 |
96 | Run the `main.py` script to start the FastHTML app and generate `devcontainer.json` files. Here's how you can do it:
97 |
98 | ```bash
99 | python main.py
100 | ```
101 |
102 | **Instructions:**
103 |
104 | 1. After starting the app, open your web browser and go to `http://localhost:8000`.
105 | 2. Enter the URL of the GitHub repository for which you want to generate a `devcontainer.json` file.
106 | 3. Click the "Generate devcontainer.json" button.
107 | 4. The generated `devcontainer.json` will be displayed and can be copied to your clipboard.
108 |
109 | ## Setting Up Daytona Workspace
110 |
111 | **Steps to Set Up Daytona Workspace**
112 |
113 | 1. Create [Daytona](https://github.com/daytonaio/daytona) Workspace:
114 |
115 | ```bash
116 | daytona create https://github.com/nkkko/devcontainer-generator
117 | ```
118 |
119 | 2. Select Preferred IDE:
120 |
121 | ```bash
122 | daytona ide
123 | ```
124 |
125 | 3. Open the Workspace:
126 |
127 | ```bash
128 | daytona code
129 | ```
130 |
131 | ## Contributing
132 |
133 | We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute to this project.
134 |
135 | ## License
136 |
137 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details.
138 |
139 | ---
140 |
141 | Thank you for using **devcontainer-generator**! If you have any questions or issues, feel free to open an issue on GitHub.
142 |
--------------------------------------------------------------------------------
/assets/fonts/geist-mono/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/fonts/geist-mono/GeistMonoVF.woff
--------------------------------------------------------------------------------
/assets/fonts/geist-mono/GeistMonoVF.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/fonts/geist-mono/GeistMonoVF.woff2
--------------------------------------------------------------------------------
/assets/fonts/geist/Geist-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/fonts/geist/Geist-Medium.woff
--------------------------------------------------------------------------------
/assets/fonts/geist/Geist-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/fonts/geist/Geist-Medium.woff2
--------------------------------------------------------------------------------
/assets/fonts/geist/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/fonts/geist/GeistVF.woff
--------------------------------------------------------------------------------
/assets/fonts/geist/GeistVF.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/fonts/geist/GeistVF.woff2
--------------------------------------------------------------------------------
/assets/footer-shapes.svg:
--------------------------------------------------------------------------------
1 |
118 |
--------------------------------------------------------------------------------
/assets/hero-shapes.svg:
--------------------------------------------------------------------------------
1 |
2 |
119 |
--------------------------------------------------------------------------------
/assets/icons/accordion-minus-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/assets/icons/accordion-plus-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/assets/icons/arrow-left.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/arrow-right.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/arrow-up-right-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/arrow-up-right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/copy-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/dot.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/github-mark-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/github-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/loading-spinner.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/magic-wand.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/assets/icons/minus-icon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/plus-icon.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/icons/regenerate.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/icons/youtube-icon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/icons/youtube.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/assets/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/og-image.png
--------------------------------------------------------------------------------
/assets/og-sq.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/og-sq.png
--------------------------------------------------------------------------------
/assets/testimonials/daniel-roy-greenfield.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/testimonials/daniel-roy-greenfield.png
--------------------------------------------------------------------------------
/assets/testimonials/daniel-roy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/testimonials/daniel-roy.png
--------------------------------------------------------------------------------
/assets/testimonials/giles-thomas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/testimonials/giles-thomas.png
--------------------------------------------------------------------------------
/assets/testimonials/guillermo-rauch.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/testimonials/guillermo-rauch.jpg
--------------------------------------------------------------------------------
/assets/testimonials/jake-cooper.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/assets/testimonials/jake-cooper.png
--------------------------------------------------------------------------------
/content.py:
--------------------------------------------------------------------------------
1 | from fasthtml.common import *
2 |
3 | description = 'Generate Custom Dev Containers in Seconds with AI'
4 | _blank = dict(target="_blank", rel="noopener noreferrer")
5 |
6 | def caution_section():
7 | return Div(
8 | H3(
9 | "⚠️ Caution: AI Generated Code",
10 | cls="text-2xl font-bold text-center mb-4",
11 | style="padding-left: 20px; padding-top: 10px; padding-bottom: 10px;"
12 | ),
13 | Ul(
14 | Li("Make sure to review the generated devcontainer.json file before running it in your development environment."),
15 | Li("The best way to run AI generated code is inside sandboxed dev environments like those managed by Daytona."),
16 | cls="list-disc list-inside text-lg text-center mt-4"
17 | ),
18 | cls="container mx-auto px-4 py-16 bg-yellow-100",
19 | style="background-color: #ffff66; padding-bottom: 5px; border-radius: 5px; margin-top: 10px; margin-bottom: 10px;"
20 | )
21 |
22 | def hero_section():
23 | return Section(
24 | Div(
25 | H1("AI-Powered Dev Environment Setup", cls="text-3xl font-bold text-center"),
26 | H2("From GitHub to Ready-to-Code in Seconds"),
27 | P("Paste your GitHub URL and get a custom devcontainer.json to simplify your development.", cls="text-lg text-center mt-4"),
28 | cls="container mx-auto px-4 py-16"
29 | ),
30 | cls="bg-gray-100",
31 | )
32 |
33 |
34 | def generator_section():
35 | return Section(
36 | Form(
37 | Group(
38 | Input(type="text", name="repo_url", placeholder="Paste your Github repo URL, or select a repo to get started", cls="form-input", list="repo-list"),
39 | Datalist(
40 | Option(value="https://github.com/devcontainers/templates"),
41 | Option(value="https://github.com/JetBrains/devcontainers-examples"),
42 | Option(value="https://github.com/devcontainers/cli"),
43 | id="repo-list"
44 | ),
45 | Button(
46 | Div(
47 | Img(src="assets/icons/magic-wand.svg", cls="svg-icon"),
48 | Img(src="assets/icons/loading-spinner.svg", cls="htmx-indicator"),
49 | cls="icon-container"
50 | ),
51 | Span("Generate", cls="button-text"),
52 | cls="button",
53 | id="generate-button",
54 | hx_post="/generate",
55 | hx_target="#result",
56 | hx_indicator="#generate-button"
57 | )
58 | ),
59 | Div(id="url-error", cls="error-message"),
60 | id="generate-form",
61 | cls="form",
62 | hx_post="/generate",
63 | hx_target="#result",
64 | hx_indicator=".htmx-indicator"
65 | ),
66 | Div(id="result"),
67 | cls="container"
68 | )
69 |
70 | def benefits_section():
71 | return Section(
72 | Div(
73 | H2("Benefits of Development Containers", cls="text-2xl font-bold text-center mb-8"),
74 | Div(
75 | benefit_card("Consistency", "Ensure every team member uses the same development environment, regardless of their local setup."),
76 | benefit_card("Reproducibility", "Easily recreate a specific development environment for bug reproduction or testing."),
77 | benefit_card("Isolation", "Keep project dependencies contained, preventing conflicts with other projects."),
78 | benefit_card("Onboarding", "Simplify onboarding for new developers by providing a ready-to-go environment."),
79 | cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 container mx-auto",
80 | ),
81 | cls="container mx-auto px-4 py-16",
82 | ),
83 | cls="bg-gray-100"
84 | )
85 |
86 | def benefit_card(title, description):
87 | return Div(
88 | H3(title, cls="text-xl font-bold mb-2"),
89 | P(description, cls="text-gray-700"),
90 | cls="bg-white rounded-md shadow-md p-6"
91 | )
92 |
93 | def setup_section():
94 | return Section(
95 | Div(
96 | H2("Setting Up Dev Containers in Your Repository", cls="text-2xl font-bold text-center mb-8"),
97 | P("Follow these steps:", cls="text-gray-700 container mx-auto mb-4"),
98 | Ul(
99 | Li("Create a `.devcontainer/devcontainer.json` file in the root of your repository."),
100 | Li("Open your repository in VS Code or ",
101 | A("Daytona", href="https://daytona.io", **_blank,
102 | cls="border-b-2 border-b-black/30 hover:border-b-black/80"),
103 | "and it will automatically run your dev container."),
104 | cls="list-decimal list-inside text-gray-700 mb-8"
105 | ),
106 | P("Now you're ready to develop inside your consistent and isolated dev environment!", cls="text-gray-700 container mx-auto"),
107 | cls="container mx-auto px-4 py-16"
108 | ),
109 | )
110 |
111 | def manifesto():
112 | return Section(
113 | Div(
114 | H2("The Open Run Manifesto", cls="text-3xl font-bold text-center mb-8"),
115 | P("Imagine a world where any software, any code, just runs. No more complex setups. No more compatibility issues. Just pure creation.", cls="text-gray-700 container mx-auto mb-4"),
116 | P("Key points:", cls="text-gray-700 container mx-auto mb-4"),
117 | Ul(
118 | Li("Developers waste up to 56% of their time on environment setup and maintenance."),
119 | Li("Code contributions are often hindered by complicated development environments."),
120 | Li("We envision a future where AI handles complex setup tasks."),
121 | Li("Our goal: Make any code instantly runnable by anyone, anywhere."),
122 | cls="list-disc list-inside text-gray-700 mb-8"
123 | ),
124 | P("Join us in making instant-run software a reality for all developers.", cls="text-gray-700 container mx-auto mb-8"),
125 | A("Read the full manifesto",
126 | href="/manifesto",
127 | cls="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded",
128 | **_blank
129 | ),
130 | cls="container mx-auto px-4 py-16"
131 | ),
132 | )
133 |
134 | def examples_section():
135 | return Section(
136 | Div(
137 | H2("Examples from Popular Repositories", cls="text-2xl font-bold text-center mb-8"),
138 | P("Explore how popular projects utilize dev containers for efficient development:", cls="text-gray-700 text-center mb-8 container mx-auto"),
139 | Div(
140 | example_card("Microsoft/vscode", "https://github.com/microsoft/vscode/tree/main/.devcontainer"),
141 | example_card("daytonaio/daytona", "https://github.com/daytonaio/daytona/blob/main/.devcontainer"),
142 | example_card("withastro/astro", "https://github.com/withastro/astro/tree/main/.devcontainer"),
143 | cls="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 container mx-auto",
144 | ),
145 | cls="container mx-auto px-4 py-16"
146 | ),
147 | cls="bg-gray-100"
148 | )
149 |
150 | def example_card(repo_name, url):
151 | return Div(
152 | A(repo_name, href=url, target="_blank", cls="text-lg font-bold hover:underline"),
153 | cls="bg-white rounded-md shadow-md p-6"
154 | )
155 |
156 | def faq_section():
157 | return Section(
158 | Div(
159 | H2("Frequently Asked Questions", cls="text-2xl font-bold text-center mb-8"),
160 | Div(
161 | faq_item("What are the advantages of using a dev container?",
162 | "Dev containers provide a consistent and isolated environment for development, ensuring that all team members have the same setup and dependencies."),
163 | faq_item("How does the dev container generation process work?",
164 | "The process is simple: 1) Enter your GitHub repo URL, 2) Our AI analyzes your project, 3) Get a custom devcontainer.json in seconds, 4) Start coding in your optimized environment."),
165 | faq_item("Which development tools are compatible with the generated dev containers?",
166 | "Our generated dev containers work seamlessly with Daytona, VS Code, GitHub Codespaces, and other popular dev tools."),
167 | faq_item("What are the advantages of using a dev container?",
168 | "Dev containers provide a consistent and isolated environment for development, ensuring that all team members have the same setup and dependencies."),
169 | faq_item("Can I customize the generated dev container?",
170 | "Yes, you can further customize the dev container for example by adding a `Dockerfile` to the `.devcontainer` directory or adding more Dev Container features."),
171 | faq_item("What if my repository already has a dev container?",
172 | "Our generator will detect existing `devcontainer.json` files and try to use them as a starting point for customization. You can also choose to regenerate a new configuration."),
173 | faq_item("How often should I update my dev container configuration?",
174 | "It's a good practice to update your dev container configuration whenever you make significant changes to your project's dependencies or development environment. You can easily regenerate the configuration using our tool."),
175 | faq_item("Does this service work with private GitHub repositories?",
176 | "Currently, our dev container generator only works with public GitHub repositories. This ensures that we can analyze the necessary metadata without requiring additional authentication or permissions."),
177 | cls="accordion container mx-auto",
178 | ),
179 | cls="container mx-auto px-4 py-16",
180 | ),
181 | )
182 |
183 | def faq_item(question, answer):
184 | return Div(
185 | H3(question, cls="text-lg font-medium cursor-pointer"),
186 | Div(P(answer, cls="text-gray-700 mt-2"), cls="hidden"),
187 | cls="mb-4",
188 | **{"_surreal": "on click toggle class:hidden for next-sibling"}
189 | )
190 |
191 | def cta_section():
192 | return Section(
193 | Div(
194 | H2("Ready to Simplify Your Development Environment?", cls="text-2xl font-bold text-center mb-4"),
195 | P("Experience the power of Daytona's flexible and easy development environment platform.", cls="text-lg text-center mb-6"),
196 | A(
197 | Span(
198 | Img(src="assets/icons/github-mark-white.svg", cls="github-icon"),
199 | "Get Daytona Now",
200 | cls="button-content"
201 | ),
202 | role="button",
203 | href="https://github.com/daytonaio/daytona",
204 | target="_blank",
205 | title="Get Daytona",
206 | cls="cta-button"
207 | ),
208 | cls="container mx-auto px-4 py-16 text-center"
209 | ),
210 | cls="bg-gray-100"
211 | )
212 |
213 | def footer_section():
214 | return Footer(
215 | Div(
216 | P("© 2024 ",
217 | A("Daytona Platforms Inc.", href="https://daytona.io", **_blank,
218 | cls="border-b-2 border-b-black/30 hover:border-b-black/80"),
219 | "All rights reserved.", cls="text-center text-gray-600"),
220 | cls="container mx-auto px-4 py-8"
221 | ),
222 | cls="bg-gray-100",
223 | )
224 |
225 | def manifesto_page():
226 | return (
227 | Title("The Open Run Manifesto: Liberating Software for All"),
228 | Main(
229 | Section(
230 | Div(
231 | H1("The Open Run Manifesto", cls="text-4xl font-bold text-center mb-8 text-blue-600"),
232 | P("Liberating Software for All", cls="text-2xl text-gray-700 text-center mb-12"),
233 | Div(
234 | P("Imagine a world where any software, any code, just runs. No more complex setups. No more compatibility issues. Just pure creation.", cls="mb-4"),
235 | P("We're entering an age where coding's power is accessible to all, not just developers.", cls="mb-4"),
236 | P("For too long, we've accepted artificial barriers between code and execution. Developers waste nearly half of their productive time—up to 56%—just setting up and maintaining their development environments. This isn't just inefficiency; it's a creativity killer.", cls="mb-4"),
237 | P("We've seen countless open source contributions and collaborative efforts stifled by complicated development environments, intricate setup processes, and the all-too-familiar \"It works on my machine\" predicament.", cls="mb-4"),
238 | P("This isn't just lost time; it's lost creation.", cls="mb-4"),
239 | P("No more.", cls="font-bold text-blue-600 mb-4"),
240 | P("Imagine if every developer could instantly dive into coding, regardless of the project or technology stack.", cls="mb-4"),
241 | P("Let's build a future where AI handles the complex task of setting up and configuring software.", cls="mb-8"),
242 | cls="text-lg text-gray-700 mb-12"
243 | ),
244 | H3("Imagine:", cls="text-3xl font-bold mt-12 mb-6 text-blue-600"),
245 | Ul(
246 | Li("Checking out a repo and having it run instantly, without any manual intervention"),
247 | Li("No need to read lengthy readme files or follow complex configuration steps"),
248 | Li("A world where the focus is on creation, not on fighting with setup processes"),
249 | cls="list-disc list-inside text-lg text-gray-700 mb-12 pl-8 space-y-2"
250 | ),
251 | P("Today, infrastructure-as-code configuration files offer a solution, but widespread adoption remains limited. Even among those who use them, maintenance can be inconsistent. The goal is to automate these configuration files, which simplify environment setup but are often absent or overly complex. We should make them standard for all projects globally, allowing anyone to run any code instantly.", cls="text-lg text-gray-700 mb-8"),
252 | P("We are taking a first step: automating dev container creation with devcontainer.ai. This open-source initiative paves the way for a future where all code is instantly accessible and runnable by anyone, anywhere.", cls="text-lg text-gray-700 mb-8"),
253 | P("We need your help to achieve this goal. Join us in making instant-run software a reality for all developers.", cls="text-xl text-gray-700 mb-8 font-bold"),
254 | P("Let's build a world where code isn't just open source, but free to be used by anyone with an idea.", cls="text-lg text-gray-700 mb-12"),
255 | A(
256 | Span(
257 | Img(src="assets/icons/github-mark-white.svg", cls="github-icon"),
258 | "Contribute to devcontainer.ai",
259 | cls="button-content"
260 | ),
261 | role="button",
262 | href="https://github.com/daytonaio/devcontainer-generator",
263 | target="_blank",
264 | title="Get Daytona",
265 | cls="cta-button"
266 | ),
267 | Div(
268 | A("Back to Home", href="/", cls="bg-blue-500 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg text-lg"),
269 | cls="mt-16 text-center"
270 | ),
271 | cls="container mx-auto px-8 py-16 max-w-4xl"
272 | ),
273 | cls="bg-gray-50"
274 | ),
275 | footer_section()
276 | )
277 | )
--------------------------------------------------------------------------------
/css/input.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/css/main.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --pico-font-size: 100%;
3 | }
4 |
5 | @font-face {
6 | font-family: "Geist";
7 | src: url("/assets/fonts/geist/GeistVF.woff2") format("woff2"),
8 | url("/assets/fonts/geist/GeistVF.woff") format("woff");
9 | font-weight: normal;
10 | font-style: normal;
11 | font-display: swap;
12 | }
13 |
14 | @font-face {
15 | font-family: "Geist";
16 | src: url("/assets/fonts/geist/Geist-Medium.woff2") format("woff2"),
17 | url("/assets/fonts/geist/Geist-Medium.woff") format("woff");
18 | font-weight: 500;
19 | font-style: normal;
20 | font-display: swap;
21 | }
22 |
23 | @font-face {
24 | font-family: "Geist Mono";
25 | src: url("/assets/fonts/geist-mono/GeistMonoVF.woff2") format("woff2"),
26 | url("/assets/fonts/geist-mono/GeistMonoVF.woff") format("woff");
27 | font-weight: normal;
28 | font-style: normal;
29 | font-display: swap;
30 | }
31 |
32 | body {
33 | padding: 20px;
34 | font-family: Geist, sans-serif;
35 | }
36 |
37 | .container {
38 | max-width: 800px;
39 | margin: 0 auto;
40 | }
41 |
42 | pre {
43 | background-color: #f4f4f4;
44 | padding: 15px;
45 | border-radius: 5px;
46 | position: relative;
47 | }
48 |
49 | .code-container {
50 | position: relative;
51 | overflow: visible;
52 | }
53 |
54 | .button-group {
55 | position: absolute;
56 | top: 0.5rem;
57 | right: 0.5rem;
58 | display: flex;
59 | gap: 0.25rem;
60 | }
61 |
62 | .icon-button {
63 | background-color: rgba(73, 73, 73, 0.5);
64 | border: none;
65 | border-radius: 0.25rem;
66 | padding: 0.25rem;
67 | cursor: pointer;
68 | transition: background-color 0.3s ease;
69 | width: 1.75rem;
70 | height: 1.75rem;
71 | display: flex;
72 | align-items: center;
73 | justify-content: center;
74 | }
75 |
76 | .icon-button:hover {
77 | background-color: rgba(73, 73, 73, 0.25);
78 | }
79 |
80 | .action-text {
81 | position: absolute;
82 | height: 1.75rem;
83 | top: 0rem;
84 | right: 3.75rem;
85 | background-color: #2196F3;
86 | color: rgb(50, 50, 50);
87 | padding: 0.5rem 0.25rem;
88 | border-radius: 0.25rem;
89 | font-size: 0.75rem;
90 | opacity: 0;
91 | transition: opacity 0.3s ease-in-out;
92 | }
93 |
94 | #regenerating-indicator {
95 | display: none;
96 | position: absolute;
97 | top: 0.5rem;
98 | right: 3.75rem;
99 | background-color: #2196F3;
100 | color: rgb(69, 69, 69);
101 | padding: 0.125rem 0.25rem;
102 | border-radius: 0.25rem;
103 | font-size: 0.75rem;
104 | }
105 |
106 | .htmx-request .regenerate-button {
107 | opacity: 0.5;
108 | cursor: not-allowed;
109 | }
110 |
111 | .htmx-request #regenerating-indicator {
112 | display: flex;
113 | }
114 |
115 | .error-message {
116 | color: red;
117 | margin-top: 5px;
118 | font-size: 0.9em;
119 | }
120 |
121 | .button {
122 | display: inline-flex;
123 | align-items: center;
124 | justify-content: center;
125 | }
126 |
127 | .icon-container {
128 | position: relative;
129 | width: 24px;
130 | height: 24px;
131 | margin-right: 8px;
132 | }
133 |
134 | .svg-icon,
135 | .htmx-indicator {
136 | position: absolute;
137 | top: 0;
138 | left: 0;
139 | width: 100%;
140 | height: 100%;
141 | transition: opacity 0.3s ease-in-out;
142 | }
143 |
144 | .htmx-indicator {
145 | opacity: 0;
146 | }
147 |
148 | .button.htmx-request .svg-icon {
149 | opacity: 0;
150 | }
151 |
152 | .button.htmx-request .htmx-indicator {
153 | opacity: 1;
154 | }
155 |
156 | /* Debug styles */
157 | .debug-indicator {
158 | position: fixed;
159 | top: 10px;
160 | right: 10px;
161 | padding: 5px;
162 | background-color: #ff0;
163 | color: #000;
164 | font-weight: bold;
165 | z-index: 9999;
166 | }
167 |
168 | /* Typography */
169 |
170 | .font-details-off {
171 | font-feature-settings: "clig" off, "liga" off;
172 | }
173 |
174 | H1 {
175 | font-family: Geist;
176 | font-size: 2.5rem;
177 | font-style: normal;
178 | font-weight: 500;
179 | line-height: 64px;
180 | /* 114.286% */
181 | letter-spacing: -0.84px;
182 | }
183 |
184 | H2 {
185 | font-size: 2.5rem;
186 | font-style: normal;
187 | font-weight: 500;
188 | line-height: 3rem;
189 | /* 120% */
190 | letter-spacing: -0.0375rem;
191 | }
192 |
193 | .xs-mono-body {
194 | font-family: "Geist Mono", monospace;
195 | font-size: 0.875rem;
196 | font-style: normal;
197 | font-weight: 500;
198 | line-height: 180%;
199 | /* 1.575rem */
200 | }
201 |
202 | .mono-body {
203 | font-family: "Geist Mono", monospace;
204 | font-size: 1rem;
205 | font-style: normal;
206 | font-weight: 500;
207 | line-height: 180%;
208 | /* 1.8rem */
209 | }
210 |
211 | .mono-s {
212 | font-family: "Geist Mono", monospace;
213 | font-size: 16px;
214 | font-style: normal;
215 | font-weight: 500;
216 | line-height: 180%;
217 | /* 28.8px */
218 | }
219 |
220 | .xs-mono-body {
221 | font-family: "Geist Mono";
222 | font-size: 0.875rem;
223 | font-style: normal;
224 | font-weight: 500;
225 | line-height: 180%;
226 | /* 1.575rem */
227 | }
228 |
229 | .regular-body,
230 | .s-body {
231 | font-size: 1rem;
232 | font-style: normal;
233 | font-weight: 400;
234 | line-height: 1.5rem;
235 | /* 150% */
236 | letter-spacing: -0.005rem;
237 | }
238 |
239 | .m-body {
240 | font-family: "Geist", sans-serif;
241 | font-size: 1.25rem;
242 | font-style: normal;
243 | font-weight: 400;
244 | line-height: 1.75rem;
245 | /* 140% */
246 | letter-spacing: -0.0125rem;
247 | }
248 |
249 | .l-body {
250 | font-size: 1.5rem;
251 | font-style: normal;
252 | font-weight: 400;
253 | line-height: 2rem;
254 | /* 133.333% */
255 | letter-spacing: -0.015rem;
256 | }
257 |
258 | .heading-3 {
259 | font-size: 1.5rem;
260 | font-style: normal;
261 | font-weight: 500;
262 | line-height: 2rem;
263 | /* 133.333% */
264 | letter-spacing: -0.015rem;
265 | }
266 |
267 | /* scroll */
268 | .hide-scrollbar::-webkit-scrollbar {
269 | display: none;
270 | }
271 |
272 | .hide-scrollbar {
273 | -ms-overflow-style: none;
274 | /* IE and Edge */
275 | scrollbar-width: none;
276 | /* Firefox */
277 | }
278 |
279 | .cta-button {
280 | display: inline-block;
281 | background-color: #0366d6; /* GitHub blue color */
282 | color: white;
283 | padding: 10px 20px;
284 | border-radius: 6px;
285 | text-decoration: none;
286 | font-weight: bold;
287 | }
288 |
289 | .button-content {
290 | display: flex;
291 | align-items: center;
292 | justify-content: center;
293 | }
294 |
295 | .github-icon {
296 | width: 24px;
297 | height: 24px;
298 | margin-right: 10px;
299 | }
--------------------------------------------------------------------------------
/css/tailwind.css:
--------------------------------------------------------------------------------
1 | /*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1440px){.container{max-width:1440px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.bottom-\[calc\(100\%-300px\)\]{bottom:calc(100% - 300px)}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.top-0{top:0}.z-0{z-index:0}.z-10{z-index:10}.z-50{z-index:50}.z-\[1\]{z-index:1}.col-span-full{grid-column:1/-1}.mx-auto{margin-left:auto;margin-right:auto}.my-11{margin-top:2.75rem;margin-bottom:2.75rem}.-mt-8{margin-top:-2rem}.mb-12{margin-bottom:3rem}.mb-4{margin-bottom:1rem}.mb-8{margin-bottom:2rem}.ml-auto{margin-left:auto}.mr-auto{margin-right:auto}.mt-12{margin-top:3rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.aspect-square{aspect-ratio:1/1}.aspect-video{aspect-ratio:16/9}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-16{height:4rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[2\.75rem\]{height:2.75rem}.h-\[420px\]{height:420px}.h-\[76px\]{height:76px}.h-auto{height:auto}.h-full{height:100%}.h-screen{height:100vh}.max-h-0{max-height:0}.max-h-\[1024px\]{max-height:1024px}.max-h-\[25rem\]{max-height:25rem}.max-h-fit{max-height:-moz-fit-content;max-height:fit-content}.min-h-\[720px\]{min-height:720px}.w-0{width:0}.w-11{width:2.75rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-\[10\.59375rem\]{width:10.59375rem}.w-\[120\%\]{width:120%}.w-\[200\%\]{width:200%}.w-\[4\.5rem\]{width:4.5rem}.w-\[7\.5rem\]{width:7.5rem}.w-fit{width:-moz-fit-content;width:fit-content}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.min-w-\[900px\]{min-width:900px}.min-w-max{min-width:-moz-max-content;min-width:max-content}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-7xl{max-width:80rem}.max-w-\[1440px\]{max-width:1440px}.max-w-\[2048px\]{max-width:2048px}.max-w-\[21rem\]{max-width:21rem}.max-w-\[32rem\]{max-width:32rem}.max-w-\[350px\]{max-width:350px}.max-w-\[36rem\]{max-width:36rem}.max-w-\[400px\]{max-width:400px}.max-w-\[40rem\]{max-width:40rem}.max-w-\[41rem\]{max-width:41rem}.max-w-\[50rem\]{max-width:50rem}.max-w-\[90rem\]{max-width:90rem}.max-w-full{max-width:100%}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.flex-none{flex:none}.flex-grow{flex-grow:1}.origin-center{transform-origin:center}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes negative-unbounce{0%{transform:rotate(-4deg)}to{transform:rotate(0deg)}}.animate-negative-unbounce{animation:negative-unbounce .2s ease-in-out forwards}@keyframes positive-unbounce{0%{transform:rotate(4deg)}to{transform:rotate(0deg)}}.animate-positive-unbounce{animation:positive-unbounce .2s ease-in-out forwards}.cursor-pointer{cursor:pointer}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-rows-1{grid-template-rows:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.content-center{align-content:center}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-8{row-gap:2rem}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-3xl{border-radius:1.5rem}.rounded-\[0\.5rem\]{border-radius:.5rem}.rounded-\[1\.25rem\]{border-radius:1.25rem}.rounded-\[62\.5rem\]{border-radius:62.5rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-l-3xl{border-top-left-radius:1.5rem;border-bottom-left-radius:1.5rem}.rounded-l-\[0\.5rem\]{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-\[0\.5rem\]{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t-3xl{border-top-left-radius:1.5rem;border-top-right-radius:1.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-white\/20{border-color:#fff3}.border-b-black\/30{border-bottom-color:#0000004d}.border-b-transparent{border-bottom-color:#0000}.border-b-white\/50{border-bottom-color:#ffffff80}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-black\/20{background-color:#0003}.bg-blue{--tw-bg-opacity:1;background-color:rgb(117 117 240/var(--tw-bg-opacity))}.bg-green{--tw-bg-opacity:1;background-color:rgb(60 221 140/var(--tw-bg-opacity))}.bg-grey{--tw-bg-opacity:1;background-color:rgb(243 243 243/var(--tw-bg-opacity))}.bg-inherit{background-color:inherit}.bg-pink{--tw-bg-opacity:1;background-color:rgb(230 153 217/var(--tw-bg-opacity))}.bg-purple{--tw-bg-opacity:1;background-color:rgb(58 34 52/var(--tw-bg-opacity))}.bg-purple\/10{background-color:#3a22341a}.bg-soft-blue{--tw-bg-opacity:1;background-color:rgb(232 232 252/var(--tw-bg-opacity))}.bg-soft-green{--tw-bg-opacity:1;background-color:rgb(212 247 230/var(--tw-bg-opacity))}.bg-soft-pink{--tw-bg-opacity:1;background-color:rgb(255 204 247/var(--tw-bg-opacity))}.bg-soft-purple{--tw-bg-opacity:1;background-color:rgb(237 222 233/var(--tw-bg-opacity))}.bg-soft-yellow{--tw-bg-opacity:1;background-color:rgb(255 238 204/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/20{background-color:#fff3}.bg-white\/50{background-color:#ffffff80}.bg-white\/60{background-color:#fff9}.bg-yellow{--tw-bg-opacity:1;background-color:rgb(255 196 53/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.from-transparent{--tw-gradient-from:#0000 var(--tw-gradient-from-position);--tw-gradient-to:#0000 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-\[\#3a2234\]{--tw-gradient-to:#3a2234 var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-4{padding-left:1rem;padding-right:1rem}.px-8{padding-left:2rem;padding-right:2rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-16{padding-bottom:4rem}.pb-24{padding-bottom:6rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-20{padding-right:5rem}.pr-4{padding-right:1rem}.pt-8{padding-top:2rem}.text-center{text-align:center}.text-start{text-align:start}.font-geist{font-family:Geist,sans-serif}.text-2xl{font-size:1.5rem;line-height:2rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-black\/60{color:#0009}.text-black\/80{color:#000c}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-white\/80{color:#fffc}.text-opacity-60{--tw-text-opacity:0.6}.shadow-\[0_2px_2px_rgba\(255\2c 255\2c 255\2c 0\.5\)\2c 0_3px_3px_rgba\(0\2c 0\2c 0\2c 0\.2\)\]{--tw-shadow:0 2px 2px #ffffff80,0 3px 3px #0003;--tw-shadow-colored:0 2px 2px var(--tw-shadow-color),0 3px 3px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-\[inset_0_2px_4px_rgba\(255\2c 255\2c 255\2c 0\.1\)\2c 0_4px_8px_rgba\(0\2c 0\2c 0\2c 0\.5\)\]{--tw-shadow:inset 0 2px 4px #ffffff1a,0 4px 8px #00000080;--tw-shadow-colored:inset 0 2px 4px var(--tw-shadow-color),0 4px 8px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-lg{--tw-backdrop-blur:blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.delay-\[300ms\]{transition-delay:.3s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.even\:bg-purple\/5:nth-child(2n){background-color:#3a22340d}.hover\:border-b-black\/80:hover{border-bottom-color:#000c}.hover\:border-b-white:hover{--tw-border-opacity:1;border-bottom-color:rgb(255 255 255/var(--tw-border-opacity))}.hover\:bg-black\/70:hover{background-color:#000000b3}.hover\:bg-black\/80:hover{background-color:#000c}.hover\:bg-white\/80:hover{background-color:#fffc}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.disabled\:opacity-40:disabled{opacity:.4}.group:hover .group-hover\:translate-x-\[-0\.34rem\]{--tw-translate-x:-0.34rem}.group:hover .group-hover\:translate-x-\[-0\.34rem\],.group:hover .group-hover\:translate-x-\[0\.29rem\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:translate-x-\[0\.29rem\]{--tw-translate-x:0.29rem}.group:hover .group-hover\:translate-y-\[-0\.13rem\]{--tw-translate-y:-0.13rem}.group:hover .group-hover\:translate-y-\[-0\.13rem\],.group:hover .group-hover\:translate-y-\[-0\.1rem\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:translate-y-\[-0\.1rem\]{--tw-translate-y:-0.1rem}.group:hover .group-hover\:translate-y-\[0\.57rem\]{--tw-translate-y:0.57rem}.group:hover .group-hover\:rotate-\[-4deg\],.group:hover .group-hover\:translate-y-\[0\.57rem\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:rotate-\[-4deg\]{--tw-rotate:-4deg}.group:hover .group-hover\:rotate-\[15deg\]{--tw-rotate:15deg}.group:hover .group-hover\:rotate-\[15deg\],.group:hover .group-hover\:rotate-\[20deg\]{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:rotate-\[20deg\]{--tw-rotate:20deg}.group:hover .group-hover\:rotate-\[4deg\]{--tw-rotate:4deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes negative-bounce{0%{transform:rotate(-4deg)}33%{transform:rotate(-3deg)}66%{transform:rotate(-5deg)}to{transform:rotate(-4deg)}}.group:hover .group-hover\:animate-negative-bounce{animation:negative-bounce .6s ease-in-out forwards}@keyframes positive-bounce{0%{transform:rotate(4deg)}33%{transform:rotate(3deg)}66%{transform:rotate(5deg)}to{transform:rotate(4deg)}}.group:hover .group-hover\:animate-positive-bounce{animation:positive-bounce .6s ease-in-out forwards}.peer\/collapsible:checked~.peer-checked\/collapsible\:max-h-\[30rem\]{max-height:30rem}.peer\/collapsible:checked~.peer-checked\/collapsible\:pb-4{padding-bottom:1rem}@media not all and (min-width:1024px){.max-lg\:basis-\[152px\]{flex-basis:152px}.max-lg\:flex-col{flex-direction:column}.max-lg\:items-start{align-items:flex-start}}@media (min-width:1024px){.lg\:-top-\[15\%\]{top:-15%}.lg\:mx-28{margin-left:7rem;margin-right:7rem}.lg\:mx-auto{margin-left:auto;margin-right:auto}.lg\:my-8{margin-top:2rem;margin-bottom:2rem}.lg\:-mr-16{margin-right:-4rem}.lg\:-mt-10{margin-top:-2.5rem}.lg\:-mt-16{margin-top:-4rem}.lg\:mb-8{margin-bottom:2rem}.lg\:mt-6{margin-top:1.5rem}.lg\:flex{display:flex}.lg\:h-14{height:3.5rem}.lg\:h-\[22rem\]{height:22rem}.lg\:h-\[600px\]{height:600px}.lg\:w-14{width:3.5rem}.lg\:w-96{width:24rem}.lg\:w-\[150\%\]{width:150%}.lg\:w-\[26rem\]{width:26rem}.lg\:max-w-7xl{max-width:80rem}.lg\:max-w-\[1440px\]{max-width:1440px}.lg\:max-w-\[43\.375rem\]{max-width:43.375rem}.lg\:max-w-\[45rem\]{max-width:45rem}.lg\:max-w-md{max-width:28rem}.lg\:max-w-xl{max-width:36rem}.lg\:flex-1{flex:1 1 0%}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:flex-row{flex-direction:row}.lg\:items-start{align-items:flex-start}.lg\:justify-between{justify-content:space-between}.lg\:gap-16{gap:4rem}.lg\:gap-6{gap:1.5rem}.lg\:gap-8{gap:2rem}.lg\:gap-x-12{-moz-column-gap:3rem;column-gap:3rem}.lg\:overflow-hidden{overflow:hidden}.lg\:rounded-t-\[2\.5rem\]{border-top-left-radius:2.5rem;border-top-right-radius:2.5rem}.lg\:p-12{padding:3rem}.lg\:p-8{padding:2rem}.lg\:px-16{padding-left:4rem;padding-right:4rem}.lg\:py-6{padding-top:1.5rem;padding-bottom:1.5rem}.lg\:pl-8{padding-left:2rem}.lg\:pr-6{padding-right:1.5rem}.lg\:pt-12{padding-top:3rem}.lg\:pt-16{padding-top:4rem}.peer\/collapsible:checked~.peer-checked\/collapsible\:lg\:pb-6{padding-bottom:1.5rem}}@media (min-width:1440px){.xl\:mx-auto{margin-left:auto;margin-right:auto}.xl\:flex{display:flex}.xl\:items-center{align-items:center}.xl\:overflow-hidden{overflow:hidden}.xl\:rounded-3xl{border-radius:1.5rem}.xl\:p-12{padding:3rem}}
--------------------------------------------------------------------------------
/data/devcontainers.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/data/devcontainers.db
--------------------------------------------------------------------------------
/dependency_tree.txt:
--------------------------------------------------------------------------------
1 | async-timeout==4.0.3
2 | cloudscraper==1.2.71
3 | ├── pyparsing [required: >=2.4.7, installed: 3.1.2]
4 | ├── requests [required: >=2.9.2, installed: 2.32.3]
5 | │ ├── certifi [required: >=2017.4.17, installed: 2024.7.4]
6 | │ ├── charset-normalizer [required: >=2,<4, installed: 2.1.1]
7 | │ ├── idna [required: >=2.5,<4, installed: 3.7]
8 | │ └── urllib3 [required: >=1.21.1,<3, installed: 2.2.2]
9 | └── requests-toolbelt [required: >=0.9.1, installed: 1.0.0]
10 | └── requests [required: >=2.0.1,<3.0.0, installed: 2.32.3]
11 | ├── certifi [required: >=2017.4.17, installed: 2024.7.4]
12 | ├── charset-normalizer [required: >=2,<4, installed: 2.1.1]
13 | ├── idna [required: >=2.5,<4, installed: 3.7]
14 | └── urllib3 [required: >=1.21.1,<3, installed: 2.2.2]
15 | exceptiongroup==1.2.2
16 | GitPython==3.1.41
17 | └── gitdb [required: >=4.0.1,<5, installed: 4.0.11]
18 | └── smmap [required: >=3.0.1,<6, installed: 5.0.1]
19 | httptools==0.6.1
20 | instructor==1.3.7
21 | ├── aiohttp [required: >=3.9.1,<4.0.0, installed: 3.10.2]
22 | │ ├── aiohappyeyeballs [required: >=2.3.0, installed: 2.3.5]
23 | │ ├── aiosignal [required: >=1.1.2, installed: 1.3.1]
24 | │ │ └── frozenlist [required: >=1.1.0, installed: 1.4.1]
25 | │ ├── attrs [required: >=17.3.0, installed: 24.2.0]
26 | │ ├── frozenlist [required: >=1.1.1, installed: 1.4.1]
27 | │ ├── multidict [required: >=4.5,<7.0, installed: 6.0.5]
28 | │ └── yarl [required: >=1.0,<2.0, installed: 1.9.4]
29 | │ ├── idna [required: >=2.0, installed: 3.7]
30 | │ └── multidict [required: >=4.0, installed: 6.0.5]
31 | ├── docstring_parser [required: >=0.16,<0.17, installed: 0.16]
32 | ├── jiter [required: >=0.4.1,<0.5.0, installed: 0.4.2]
33 | ├── openai [required: >=1.1.0,<2.0.0, installed: 1.40.2]
34 | │ ├── anyio [required: >=3.5.0,<5, installed: 4.4.0]
35 | │ │ ├── idna [required: >=2.8, installed: 3.7]
36 | │ │ └── sniffio [required: >=1.1, installed: 1.3.1]
37 | │ ├── distro [required: >=1.7.0,<2, installed: 1.9.0]
38 | │ ├── httpx [required: >=0.23.0,<1, installed: 0.27.0]
39 | │ │ ├── anyio [required: Any, installed: 4.4.0]
40 | │ │ │ ├── idna [required: >=2.8, installed: 3.7]
41 | │ │ │ └── sniffio [required: >=1.1, installed: 1.3.1]
42 | │ │ ├── certifi [required: Any, installed: 2024.7.4]
43 | │ │ ├── httpcore [required: ==1.*, installed: 1.0.5]
44 | │ │ │ ├── certifi [required: Any, installed: 2024.7.4]
45 | │ │ │ └── h11 [required: >=0.13,<0.15, installed: 0.14.0]
46 | │ │ ├── idna [required: Any, installed: 3.7]
47 | │ │ └── sniffio [required: Any, installed: 1.3.1]
48 | │ ├── jiter [required: >=0.4.0,<1, installed: 0.4.2]
49 | │ ├── pydantic [required: >=1.9.0,<3, installed: 2.8.2]
50 | │ │ ├── annotated-types [required: >=0.4.0, installed: 0.7.0]
51 | │ │ ├── pydantic_core [required: ==2.20.1, installed: 2.20.1]
52 | │ │ │ └── typing_extensions [required: >=4.6.0,!=4.7.0, installed: 4.12.2]
53 | │ │ └── typing_extensions [required: >=4.6.1, installed: 4.12.2]
54 | │ ├── sniffio [required: Any, installed: 1.3.1]
55 | │ ├── tqdm [required: >4, installed: 4.66.5]
56 | │ └── typing_extensions [required: >=4.11,<5, installed: 4.12.2]
57 | ├── pydantic [required: >=2.8.0,<3.0.0, installed: 2.8.2]
58 | │ ├── annotated-types [required: >=0.4.0, installed: 0.7.0]
59 | │ ├── pydantic_core [required: ==2.20.1, installed: 2.20.1]
60 | │ │ └── typing_extensions [required: >=4.6.0,!=4.7.0, installed: 4.12.2]
61 | │ └── typing_extensions [required: >=4.6.1, installed: 4.12.2]
62 | ├── pydantic_core [required: >=2.18.0,<3.0.0, installed: 2.20.1]
63 | │ └── typing_extensions [required: >=4.6.0,!=4.7.0, installed: 4.12.2]
64 | ├── rich [required: >=13.7.0,<14.0.0, installed: 13.7.1]
65 | │ ├── markdown-it-py [required: >=2.2.0, installed: 3.0.0]
66 | │ │ └── mdurl [required: ~=0.1, installed: 0.1.2]
67 | │ └── Pygments [required: >=2.13.0,<3.0.0, installed: 2.18.0]
68 | ├── tenacity [required: >=8.4.1,<9.0.0, installed: 8.5.0]
69 | └── typer [required: >=0.9.0,<1.0.0, installed: 0.12.3]
70 | ├── click [required: >=8.0.0, installed: 8.1.7]
71 | ├── rich [required: >=10.11.0, installed: 13.7.1]
72 | │ ├── markdown-it-py [required: >=2.2.0, installed: 3.0.0]
73 | │ │ └── mdurl [required: ~=0.1, installed: 0.1.2]
74 | │ └── Pygments [required: >=2.13.0,<3.0.0, installed: 2.18.0]
75 | ├── shellingham [required: >=1.3.0, installed: 1.5.4]
76 | └── typing_extensions [required: >=3.7.4.3, installed: 4.12.2]
77 | ipykernel==6.29.5
78 | ├── comm [required: >=0.1.1, installed: 0.2.2]
79 | │ └── traitlets [required: >=4, installed: 5.14.3]
80 | ├── debugpy [required: >=1.6.5, installed: 1.8.5]
81 | ├── ipython [required: >=7.23.1, installed: 8.26.0]
82 | │ ├── decorator [required: Any, installed: 5.1.1]
83 | │ ├── jedi [required: >=0.16, installed: 0.19.1]
84 | │ │ └── parso [required: >=0.8.3,<0.9.0, installed: 0.8.4]
85 | │ ├── matplotlib-inline [required: Any, installed: 0.1.7]
86 | │ │ └── traitlets [required: Any, installed: 5.14.3]
87 | │ ├── pexpect [required: >4.3, installed: 4.9.0]
88 | │ │ └── ptyprocess [required: >=0.5, installed: 0.7.0]
89 | │ ├── prompt_toolkit [required: >=3.0.41,<3.1.0, installed: 3.0.47]
90 | │ │ └── wcwidth [required: Any, installed: 0.2.13]
91 | │ ├── Pygments [required: >=2.4.0, installed: 2.18.0]
92 | │ ├── stack-data [required: Any, installed: 0.6.3]
93 | │ │ ├── asttokens [required: >=2.1.0, installed: 2.4.1]
94 | │ │ │ └── six [required: >=1.12.0, installed: 1.16.0]
95 | │ │ ├── executing [required: >=1.2.0, installed: 2.0.1]
96 | │ │ └── pure_eval [required: Any, installed: 0.2.3]
97 | │ └── traitlets [required: >=5.13.0, installed: 5.14.3]
98 | ├── jupyter_client [required: >=6.1.12, installed: 8.6.2]
99 | │ ├── jupyter_core [required: >=4.12,!=5.0.*, installed: 5.7.2]
100 | │ │ ├── platformdirs [required: >=2.5, installed: 4.2.2]
101 | │ │ └── traitlets [required: >=5.3, installed: 5.14.3]
102 | │ ├── python-dateutil [required: >=2.8.2, installed: 2.9.0.post0]
103 | │ │ └── six [required: >=1.5, installed: 1.16.0]
104 | │ ├── pyzmq [required: >=23.0, installed: 26.1.0]
105 | │ ├── tornado [required: >=6.2, installed: 6.4.1]
106 | │ └── traitlets [required: >=5.3, installed: 5.14.3]
107 | ├── jupyter_core [required: >=4.12,!=5.0.*, installed: 5.7.2]
108 | │ ├── platformdirs [required: >=2.5, installed: 4.2.2]
109 | │ └── traitlets [required: >=5.3, installed: 5.14.3]
110 | ├── matplotlib-inline [required: >=0.1, installed: 0.1.7]
111 | │ └── traitlets [required: Any, installed: 5.14.3]
112 | ├── nest-asyncio [required: Any, installed: 1.6.0]
113 | ├── packaging [required: Any, installed: 24.1]
114 | ├── psutil [required: Any, installed: 6.0.0]
115 | ├── pyzmq [required: >=24, installed: 26.1.0]
116 | ├── tornado [required: >=6.1, installed: 6.4.1]
117 | └── traitlets [required: >=5.4.0, installed: 5.14.3]
118 | Jinja2==3.1.4
119 | └── MarkupSafe [required: >=2.0, installed: 2.1.5]
120 | jsonschema==4.23.0
121 | ├── attrs [required: >=22.2.0, installed: 24.2.0]
122 | ├── jsonschema-specifications [required: >=2023.03.6, installed: 2023.12.1]
123 | │ └── referencing [required: >=0.31.0, installed: 0.35.1]
124 | │ ├── attrs [required: >=22.2.0, installed: 24.2.0]
125 | │ └── rpds-py [required: >=0.7.0, installed: 0.20.0]
126 | ├── referencing [required: >=0.28.4, installed: 0.35.1]
127 | │ ├── attrs [required: >=22.2.0, installed: 24.2.0]
128 | │ └── rpds-py [required: >=0.7.0, installed: 0.20.0]
129 | └── rpds-py [required: >=0.7.1, installed: 0.20.0]
130 | pipdeptree==2.23.1
131 | ├── packaging [required: >=23.1, installed: 24.1]
132 | └── pip [required: >=23.1.2, installed: 24.2]
133 | python-dotenv==1.0.1
134 | python-fasthtml==0.2.4
135 | ├── beautifulsoup4 [required: Any, installed: 4.12.3]
136 | │ └── soupsieve [required: >1.2, installed: 2.5]
137 | ├── fastcore [required: >=1.6.3, installed: 1.6.3]
138 | │ └── packaging [required: Any, installed: 24.1]
139 | ├── fastlite [required: >=0.0.6, installed: 0.0.7]
140 | │ ├── fastcore [required: >=1.5.41, installed: 1.6.3]
141 | │ │ └── packaging [required: Any, installed: 24.1]
142 | │ └── sqlite_minutils [required: Any, installed: 3.36.0.post4]
143 | │ └── fastcore [required: Any, installed: 1.6.3]
144 | │ └── packaging [required: Any, installed: 24.1]
145 | ├── httpx [required: Any, installed: 0.27.0]
146 | │ ├── anyio [required: Any, installed: 4.4.0]
147 | │ │ ├── idna [required: >=2.8, installed: 3.7]
148 | │ │ └── sniffio [required: >=1.1, installed: 1.3.1]
149 | │ ├── certifi [required: Any, installed: 2024.7.4]
150 | │ ├── httpcore [required: ==1.*, installed: 1.0.5]
151 | │ │ ├── certifi [required: Any, installed: 2024.7.4]
152 | │ │ └── h11 [required: >=0.13,<0.15, installed: 0.14.0]
153 | │ ├── idna [required: Any, installed: 3.7]
154 | │ └── sniffio [required: Any, installed: 1.3.1]
155 | ├── itsdangerous [required: Any, installed: 2.2.0]
156 | ├── oauthlib [required: Any, installed: 3.2.2]
157 | ├── python-dateutil [required: Any, installed: 2.9.0.post0]
158 | │ └── six [required: >=1.5, installed: 1.16.0]
159 | ├── python-multipart [required: Any, installed: 0.0.9]
160 | ├── starlette [required: >0.33, installed: 0.38.2]
161 | │ └── anyio [required: >=3.4.0,<5, installed: 4.4.0]
162 | │ ├── idna [required: >=2.8, installed: 3.7]
163 | │ └── sniffio [required: >=1.1, installed: 1.3.1]
164 | └── uvicorn [required: >=0.30, installed: 0.30.5]
165 | ├── click [required: >=7.0, installed: 8.1.7]
166 | └── h11 [required: >=0.8, installed: 0.14.0]
167 | PyYAML==6.0.2
168 | replicate==0.31.0
169 | ├── httpx [required: >=0.21.0,<1, installed: 0.27.0]
170 | │ ├── anyio [required: Any, installed: 4.4.0]
171 | │ │ ├── idna [required: >=2.8, installed: 3.7]
172 | │ │ └── sniffio [required: >=1.1, installed: 1.3.1]
173 | │ ├── certifi [required: Any, installed: 2024.7.4]
174 | │ ├── httpcore [required: ==1.*, installed: 1.0.5]
175 | │ │ ├── certifi [required: Any, installed: 2024.7.4]
176 | │ │ └── h11 [required: >=0.13,<0.15, installed: 0.14.0]
177 | │ ├── idna [required: Any, installed: 3.7]
178 | │ └── sniffio [required: Any, installed: 1.3.1]
179 | ├── packaging [required: Any, installed: 24.1]
180 | ├── pydantic [required: >1.10.7, installed: 2.8.2]
181 | │ ├── annotated-types [required: >=0.4.0, installed: 0.7.0]
182 | │ ├── pydantic_core [required: ==2.20.1, installed: 2.20.1]
183 | │ │ └── typing_extensions [required: >=4.6.0,!=4.7.0, installed: 4.12.2]
184 | │ └── typing_extensions [required: >=4.6.1, installed: 4.12.2]
185 | └── typing_extensions [required: >=4.5.0, installed: 4.12.2]
186 | setuptools==69.0.3
187 | SQLAlchemy==2.0.32
188 | ├── greenlet [required: !=0.4.17, installed: 3.0.3]
189 | └── typing_extensions [required: >=4.6.0, installed: 4.12.2]
190 | sqlite-vec==0.1.1
191 | tiktoken==0.7.0
192 | ├── regex [required: >=2022.1.18, installed: 2024.7.24]
193 | └── requests [required: >=2.26.0, installed: 2.32.3]
194 | ├── certifi [required: >=2017.4.17, installed: 2024.7.4]
195 | ├── charset-normalizer [required: >=2,<4, installed: 2.1.1]
196 | ├── idna [required: >=2.5,<4, installed: 3.7]
197 | └── urllib3 [required: >=1.21.1,<3, installed: 2.2.2]
198 | uvloop==0.19.0
199 | watchfiles==0.23.0
200 | └── anyio [required: >=3.0.0, installed: 4.4.0]
201 | ├── idna [required: >=2.8, installed: 3.7]
202 | └── sniffio [required: >=1.1, installed: 1.3.1]
203 | websockets==12.0
204 | wheel==0.44.0
205 |
--------------------------------------------------------------------------------
/favicon-dark.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/favicon-dark.ico
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/daytonaio/devcontainer-generator/0e00603b4163c0dd97e2ae450e5bf44576338ecd/favicon.ico
--------------------------------------------------------------------------------
/helpers/__init__.py:
--------------------------------------------------------------------------------
1 | # This file can be left empty or you can add imports if you want to make certain functions easily accessible
--------------------------------------------------------------------------------
/helpers/devcontainer_helpers.py:
--------------------------------------------------------------------------------
1 | # helpers/devcontainer_helpers.py
2 |
3 | import json
4 | import logging
5 | import os
6 | import jsonschema
7 | import tiktoken
8 | from helpers.jinja_helper import process_template
9 | from schemas import DevContainerModel
10 | from supabase_client import supabase
11 | from models import DevContainer
12 |
13 |
14 | import logging
15 | import tiktoken
16 |
17 | def truncate_context(context, max_tokens=120000):
18 | logging.info(f"Starting truncate_context with max_tokens={max_tokens}")
19 | logging.debug(f"Initial context length: {len(context)} characters")
20 |
21 | encoding = tiktoken.encoding_for_model("gpt-4o-mini")
22 | tokens = encoding.encode(context)
23 |
24 | logging.info(f"Initial token count: {len(tokens)}")
25 |
26 | if len(tokens) <= max_tokens:
27 | logging.info("Context is already within token limit. No truncation needed.")
28 | return context
29 |
30 | logging.info(f"Context size is {len(tokens)} tokens. Truncation needed.")
31 |
32 | # Prioritize keeping the repository structure and languages
33 | structure_end = context.find("<>")
34 | languages_end = context.find("<>")
35 |
36 | logging.debug(f"Structure end position: {structure_end}")
37 | logging.debug(f"Languages end position: {languages_end}")
38 |
39 | important_content = context[:languages_end] + "<>\n\n"
40 | remaining_content = context[languages_end + len("<>\n\n"):]
41 |
42 | important_tokens = encoding.encode(important_content)
43 | logging.debug(f"Important content token count: {len(important_tokens)}")
44 |
45 | if len(important_tokens) > max_tokens:
46 | logging.warning("Important content alone exceeds max_tokens. Truncating important content.")
47 | important_content = encoding.decode(important_tokens[:max_tokens])
48 | return important_content
49 |
50 | remaining_tokens = max_tokens - len(important_tokens)
51 | logging.info(f"Tokens available for remaining content: {remaining_tokens}")
52 |
53 | truncated_remaining = encoding.decode(encoding.encode(remaining_content)[:remaining_tokens])
54 |
55 | final_context = important_content + truncated_remaining
56 | final_tokens = encoding.encode(final_context)
57 |
58 | logging.info(f"Final token count: {len(final_tokens)}")
59 | logging.debug(f"Final context length: {len(final_context)} characters")
60 |
61 | return final_context
62 |
63 | def generate_devcontainer_json(instructor_client, repo_url, repo_context, devcontainer_url=None, max_retries=2, regenerate=False):
64 | existing_devcontainer = None
65 | if "<>" in repo_context:
66 | logging.info("Existing devcontainer.json found in the repository.")
67 | existing_devcontainer = (
68 | repo_context.split("<>")[1]
69 | .split("<>")[0]
70 | .strip()
71 | )
72 | if not regenerate and devcontainer_url:
73 | logging.info(f"Using existing devcontainer.json from URL: {devcontainer_url}")
74 | return existing_devcontainer, devcontainer_url
75 |
76 | logging.info("Generating devcontainer.json...")
77 |
78 | # Truncate the context to fit within token limits
79 | truncated_context = truncate_context(repo_context, max_tokens=126000)
80 |
81 | template_data = {
82 | "repo_url": repo_url,
83 | "repo_context": truncated_context,
84 | "existing_devcontainer": existing_devcontainer
85 | }
86 |
87 | prompt = process_template("prompts/devcontainer.jinja", template_data)
88 |
89 | for attempt in range(max_retries + 1):
90 | try:
91 | logging.debug(f"Attempt {attempt + 1} to generate devcontainer.json")
92 | response = instructor_client.chat.completions.create(
93 | model=os.getenv("MODEL"),
94 | response_model=DevContainerModel,
95 | messages=[
96 | {"role": "system", "content": "You are a helpful assistant that generates devcontainer.json files."},
97 | {"role": "user", "content": prompt},
98 | ],
99 | )
100 | devcontainer_json = json.dumps(response.dict(exclude_none=True), indent=2)
101 |
102 | if validate_devcontainer_json(devcontainer_json):
103 | logging.info("Successfully generated and validated devcontainer.json")
104 | if existing_devcontainer and not regenerate:
105 | return existing_devcontainer, devcontainer_url
106 | else:
107 | return devcontainer_json, None # Return None as URL for generated content
108 | else:
109 | logging.warning(f"Generated JSON failed validation on attempt {attempt + 1}")
110 | if attempt == max_retries:
111 | raise ValueError("Failed to generate valid devcontainer.json after maximum retries")
112 | except Exception as e:
113 | logging.error(f"Error on attempt {attempt + 1}: {str(e)}")
114 | if attempt == max_retries:
115 | raise
116 |
117 | raise ValueError("Failed to generate valid devcontainer.json after maximum retries")
118 |
119 | def validate_devcontainer_json(devcontainer_json):
120 | logging.info("Validating devcontainer.json...")
121 | schema_path = os.path.join(os.path.dirname(__file__), "..", "schemas", "devContainer.base.schema.json")
122 | with open(schema_path, "r") as schema_file:
123 | schema = json.load(schema_file)
124 | try:
125 | logging.debug("Running validation...")
126 | jsonschema.validate(instance=json.loads(devcontainer_json), schema=schema)
127 | logging.info("Validation successful.")
128 | return True
129 | except jsonschema.exceptions.ValidationError as e:
130 | logging.error(f"Validation failed: {e}")
131 | return False
132 |
133 | def save_devcontainer(new_devcontainer):
134 | try:
135 | result = supabase.table("devcontainers").insert(new_devcontainer.dict()).execute()
136 | return result.data[0] if result.data else None
137 | except Exception as e:
138 | logging.error(f"Error saving devcontainer to Supabase: {str(e)}")
139 | raise
--------------------------------------------------------------------------------
/helpers/github_helpers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import logging
4 | import requests
5 | from helpers.token_helpers import count_tokens
6 | from models import DevContainer
7 | from supabase_client import supabase
8 |
9 | def is_valid_github_url(url):
10 | pattern = r"^https?://github\.com/[\w-]+/[\w.-]+/?$"
11 | return re.match(pattern, url) is not None
12 |
13 | def fetch_repo_context(repo_url, max_depth=1):
14 | # First, check if the URL is valid
15 | if not is_valid_github_url(repo_url):
16 | logging.error(f"Invalid GitHub repository URL: {repo_url}")
17 | raise ValueError("Invalid GitHub repository URL")
18 |
19 | logging.info(f"Fetching context from GitHub repository: {repo_url}")
20 | token = os.getenv("GITHUB_TOKEN")
21 |
22 | parts = repo_url.split("/")
23 | owner = parts[-2]
24 | repo = parts[-1]
25 |
26 | contents_api_url = f"https://api.github.com/repos/{owner}/{repo}/contents"
27 | languages_api_url = f"https://api.github.com/repos/{owner}/{repo}/languages"
28 |
29 | headers = {
30 | "Accept": "application/vnd.github.v3+json",
31 | "Authorization": f"token {token}",
32 | }
33 |
34 | existing_devcontainer = None
35 | devcontainer_url = None
36 |
37 | root_devcontainer_url = f"{contents_api_url}/.devcontainer.json"
38 | response = requests.get(root_devcontainer_url, headers=headers)
39 | if response.status_code == 200:
40 | existing_devcontainer = requests.get(response.json()["download_url"]).text
41 | devcontainer_url = response.json()["download_url"]
42 |
43 | if not existing_devcontainer:
44 | devcontainer_dir_url = f"{contents_api_url}/.devcontainer"
45 | response = requests.get(devcontainer_dir_url, headers=headers)
46 | if response.status_code == 200:
47 | for item in response.json():
48 | if item["name"] == "devcontainer.json":
49 | existing_devcontainer = requests.get(item["download_url"]).text
50 | devcontainer_url = item["download_url"]
51 | break
52 |
53 | context = []
54 | total_tokens = 0
55 |
56 | def traverse_dir(api_url, depth=0, prefix=""):
57 | if depth > max_depth:
58 | return []
59 |
60 | logging.info(f"Traversing directory: {api_url}")
61 | response = requests.get(api_url, headers=headers)
62 | response.raise_for_status()
63 |
64 | structure = []
65 | important_files = [
66 | "requirements.txt", "Dockerfile", ".gitignore", "package.json",
67 | "Gemfile", "README.md", ".env.example", "Pipfile", "setup.py",
68 | "Pipfile.lock", "pyproject.toml", "CMakeLists.txt", "Makefile",
69 | "go.mod", "go.sum", "pom.xml", "build.gradle", "Cargo.toml",
70 | "Cargo.lock", "composer.json", "phpunit.xml", "mix.exs",
71 | "pubspec.yaml", "stack.yaml", "DESCRIPTION", "NAMESPACE", "Rakefile",
72 | ]
73 | large_dirs_to_skip = ["node_modules", "vendor"]
74 |
75 | for item in response.json():
76 | logging.debug(f"Processing item: {item['name']}")
77 | if item["type"] == "dir":
78 | if item["name"] in large_dirs_to_skip:
79 | continue
80 | structure.append(f"{prefix}{item['name']}/")
81 | structure.extend(
82 | traverse_dir(item["url"], depth + 1, prefix=prefix + " ")
83 | )
84 | else:
85 | structure.append(f"{prefix}{item['name']}")
86 |
87 | if item["type"] == "file" and item["name"] in important_files:
88 | logging.debug(f"Fetching content of {item['name']}")
89 | file_content = requests.get(item["download_url"]).text
90 | content_text = (
91 | f"<>\n{file_content}"
92 | + f"\n<>"
93 | )
94 | context.append(content_text)
95 |
96 | file_tokens = count_tokens(content_text)
97 | nonlocal total_tokens
98 | total_tokens += file_tokens
99 |
100 | return structure
101 |
102 | logging.info("Building repository structure...")
103 | repo_structure = traverse_dir(contents_api_url)
104 |
105 | logging.info("Adding repository structure to context...")
106 | repo_structure_text = (
107 | "<>\n"
108 | + "\n".join(repo_structure)
109 | + "\n<>"
110 | )
111 | context.insert(0, repo_structure_text)
112 |
113 | repo_structure_tokens = count_tokens(repo_structure_text)
114 | total_tokens += repo_structure_tokens
115 |
116 | logging.info("Fetching repository languages...")
117 | languages_response = requests.get(languages_api_url, headers=headers)
118 | languages_response.raise_for_status()
119 |
120 | logging.info("Adding repository languages to context...")
121 | languages_data = languages_response.json()
122 | languages_context = (
123 | "<>\n"
124 | + "\n".join(
125 | [f"{lang}: {count} lines" for lang, count in languages_data.items()]
126 | )
127 | + "\n<>"
128 | )
129 | context.append(languages_context)
130 |
131 | languages_tokens = count_tokens(languages_context)
132 | total_tokens += languages_tokens
133 |
134 | logging.debug(f"Total tokens: {total_tokens}")
135 |
136 | if existing_devcontainer:
137 | devcontainer_context = (
138 | "<>\n"
139 | f"{existing_devcontainer}\n"
140 | "<>"
141 | )
142 | context.append(devcontainer_context)
143 | total_tokens += count_tokens(devcontainer_context)
144 |
145 | return "\n\n".join(context), existing_devcontainer, devcontainer_url
146 |
147 | def check_url_exists(url):
148 | existing = supabase.table("devcontainers").select("*").eq("url", url).order("created_at", desc=True).limit(1).execute()
149 | existing_record = existing.data[0] if existing.data else None
150 | return existing_record is not None, existing_record
--------------------------------------------------------------------------------
/helpers/jinja_helper.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | from jinja2 import Environment, FileSystemLoader, select_autoescape
4 |
5 |
6 | def process_template(template_file: str, data: dict[str, Any]) -> str:
7 | jinja_env = Environment(
8 | loader=FileSystemLoader(searchpath="./"), autoescape=select_autoescape()
9 | )
10 | template = jinja_env.get_template(template_file)
11 | return template.render(**data)
--------------------------------------------------------------------------------
/helpers/openai_helpers.py:
--------------------------------------------------------------------------------
1 | import os
2 | import logging
3 | from openai import AzureOpenAI
4 | import instructor
5 |
6 | def setup_azure_openai():
7 | logging.info("Setting up Azure OpenAI client...")
8 | return AzureOpenAI(
9 | api_key=os.getenv("AZURE_OPENAI_API_KEY"),
10 | azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
11 | api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
12 | )
13 |
14 | def setup_instructor(openai_client):
15 | logging.info("Setting up Instructor client...")
16 | return instructor.patch(openai_client)
17 |
18 | def check_env_vars():
19 | required_vars = [
20 | "AZURE_OPENAI_ENDPOINT",
21 | "AZURE_OPENAI_API_KEY",
22 | "AZURE_OPENAI_API_VERSION",
23 | "MODEL",
24 | "GITHUB_TOKEN",
25 | ]
26 | missing_vars = [var for var in required_vars if not os.environ.get(var)]
27 | if missing_vars:
28 | print(
29 | f"Missing environment variables: {', '.join(missing_vars)}. "
30 | "Please configure the env vars file properly."
31 | )
32 | return False
33 | return True
--------------------------------------------------------------------------------
/helpers/token_helpers.py:
--------------------------------------------------------------------------------
1 | import tiktoken
2 |
3 | def count_tokens(text):
4 | encoder = tiktoken.encoding_for_model("gpt-4o")
5 | tokens = encoder.encode(text)
6 | return len(tokens)
7 |
8 | def truncate_to_token_limit(text, model_name, max_tokens):
9 | encoding = tiktoken.encoding_for_model(model_name)
10 | tokens = encoding.encode(text)
11 | if len(tokens) > max_tokens:
12 | truncated_tokens = tokens[:max_tokens]
13 | return encoding.decode(truncated_tokens)
14 | return text
--------------------------------------------------------------------------------
/js/main.js:
--------------------------------------------------------------------------------
1 | console.log("main.js loaded");
2 |
3 | document.addEventListener('htmx:beforeRequest', function(event) {
4 | console.log('htmx:beforeRequest triggered');
5 |
6 | if (event.detail.elt.id === 'generate-button') {
7 | console.log('Disabling generate button');
8 | event.detail.elt.disabled = true;
9 | }
10 | });
11 |
12 | document.addEventListener('htmx:afterRequest', function(event) {
13 | console.log('htmx:afterRequest triggered');
14 |
15 | if (event.detail.elt.id === 'generate-button') {
16 | console.log('Enabling generate button');
17 | event.detail.elt.disabled = false;
18 | }
19 | });
20 |
21 | document.addEventListener('DOMContentLoaded', function() {
22 | console.log('DOM fully loaded and parsed');
23 | const generateButton = document.getElementById('generate-button');
24 | if (generateButton) {
25 | console.log('Generate button found');
26 | generateButton.addEventListener('click', function() {
27 | console.log('Generate button clicked');
28 | });
29 | } else {
30 | console.log('Generate button not found');
31 | }
32 | });
33 |
34 | // Remove the initializeButtons function and related code since we're not using it anymore
35 |
36 | function handleCopyClick(event) {
37 | const button = event.target.closest('.icon-button.copy-button');
38 | if (!button) return;
39 |
40 | console.log("Copy button clicked");
41 |
42 | const codeContainer = button.closest(".code-container");
43 | if (!codeContainer) {
44 | console.error("Code container not found");
45 | return;
46 | }
47 |
48 | const codeElement = codeContainer.querySelector("code");
49 | if (!codeElement) {
50 | console.error("Code element not found");
51 | return;
52 | }
53 |
54 | const codeContents = codeElement.textContent;
55 | const copiedText = codeContainer.querySelector("#copied-text");
56 |
57 | navigator.clipboard.writeText(codeContents)
58 | .then(() => {
59 | console.log("Copy successful");
60 | showActionText("Copied!");
61 | })
62 | .catch(error => {
63 | console.error("Copy failed:", error);
64 | });
65 | }
66 |
67 | function handleRegenerateClick(event) {
68 | const button = event.target.closest('.icon-button.regenerate-button');
69 | if (!button) return;
70 |
71 | console.log("Regenerate button clicked");
72 | showActionText("Regenerating...");
73 | // The actual regeneration is handled by HTMX
74 | }
75 |
76 | function showActionText(text) {
77 | const actionText = document.getElementById('action-text');
78 | if (actionText) {
79 | actionText.textContent = text;
80 | actionText.style.opacity = "1";
81 | if (text !== "Regenerating...") {
82 | setTimeout(() => {
83 | actionText.style.opacity = "0";
84 | }, 2000);
85 | }
86 | }
87 | }
88 |
89 | function initializeButtons() {
90 | const copyButtons = document.querySelectorAll(".icon-button.copy-button");
91 | const regenerateButtons = document.querySelectorAll(".icon-button.regenerate-button");
92 |
93 | console.log("Copy buttons found:", copyButtons.length);
94 | console.log("Regenerate buttons found:", regenerateButtons.length);
95 |
96 | copyButtons.forEach((button, index) => {
97 | console.log(`Adding listener to copy button ${index}`);
98 | button.removeEventListener("click", handleCopyClick);
99 | button.addEventListener("click", handleCopyClick);
100 | });
101 |
102 | regenerateButtons.forEach((button, index) => {
103 | console.log(`Adding listener to regenerate button ${index}`);
104 | button.removeEventListener("click", handleRegenerateClick);
105 | button.addEventListener("click", handleRegenerateClick);
106 | });
107 | }
108 |
109 | function setupObserver() {
110 | const observer = new MutationObserver((mutations) => {
111 | mutations.forEach((mutation) => {
112 | if (mutation.type === 'childList') {
113 | initializeButtons();
114 | }
115 | });
116 | });
117 |
118 | observer.observe(document.body, { childList: true, subtree: true });
119 | }
120 |
121 | function isValidGithubUrl(url) {
122 | const pattern = /^https?:\/\/github\.com\/[\w-]+\/[\w.-]+\/?$/;
123 | return pattern.test(url);
124 | }
125 |
126 | function validateRepoUrl() {
127 | const input = document.querySelector('input[name="repo_url"]');
128 | const errorDiv = document.getElementById('url-error');
129 | const generateButton = document.getElementById('generate-button');
130 |
131 | if (input.value.trim() === '') {
132 | errorDiv.textContent = 'Please enter a GitHub repository URL.';
133 | generateButton.disabled = true;
134 | return false;
135 | }
136 |
137 | if (!isValidGithubUrl(input.value)) {
138 | errorDiv.textContent = 'Please enter a valid GitHub repository URL.';
139 | generateButton.disabled = true;
140 | return false;
141 | }
142 |
143 | errorDiv.textContent = '';
144 | generateButton.disabled = false;
145 | return true;
146 | }
147 |
148 | document.addEventListener('DOMContentLoaded', function() {
149 | const repoUrlInput = document.querySelector('input[name="repo_url"]');
150 | repoUrlInput.addEventListener('input', validateRepoUrl);
151 |
152 | const generateForm = document.getElementById('generate-form');
153 | generateForm.addEventListener('submit', function(event) {
154 | if (!validateRepoUrl()) {
155 | event.preventDefault();
156 | }
157 | });
158 |
159 | document.body.addEventListener('click', (event) => {
160 | handleCopyClick(event);
161 | handleRegenerateClick(event);
162 | });
163 |
164 | initializeButtons();
165 | setupObserver();
166 | });
167 |
168 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 | import json
4 | from datetime import datetime
5 | from fasthtml.common import *
6 | from dotenv import load_dotenv
7 | from supabase_client import supabase
8 |
9 | from helpers.openai_helpers import setup_azure_openai, setup_instructor
10 | from helpers.github_helpers import fetch_repo_context, check_url_exists
11 | from helpers.devcontainer_helpers import generate_devcontainer_json, validate_devcontainer_json
12 | from helpers.token_helpers import count_tokens, truncate_to_token_limit
13 | from models import DevContainer
14 | from schemas import DevContainerModel
15 | from content import *
16 |
17 | # Set up logging
18 | logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s")
19 |
20 | # Load environment variables
21 | load_dotenv()
22 |
23 | def check_env_vars():
24 | required_vars = [
25 | "AZURE_OPENAI_ENDPOINT",
26 | "AZURE_OPENAI_API_KEY",
27 | "AZURE_OPENAI_API_VERSION",
28 | "MODEL",
29 | "GITHUB_TOKEN",
30 | "SUPABASE_URL",
31 | "SUPABASE_KEY",
32 | ]
33 | missing_vars = [var for var in required_vars if not os.environ.get(var)]
34 | if missing_vars:
35 | print(f"Missing environment variables: {', '.join(missing_vars)}. Please configure the env vars file properly.")
36 | return False
37 | return True
38 |
39 | hdrs = [
40 | Script(src="https://www.googletagmanager.com/gtag/js?id=G-Q22LCTCW8Y", aync=True),
41 | Script("""
42 | window.dataLayer = window.dataLayer || [];
43 | function gtag(){dataLayer.push(arguments);}
44 | gtag('js', new Date());
45 | gtag('config', 'G-Q22LCTCW8Y');
46 | """),
47 | Script("""
48 | (function(c,l,a,r,i,t,y){
49 | c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
50 | t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
51 | y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
52 | })(window, document, "clarity", "script", "o5om7ajkg6");
53 | """),
54 | picolink,
55 | Meta(charset='UTF-8'),
56 | Meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0'),
57 | Meta(name='description', content=description),
58 | *Favicon('favicon.ico', 'favicon-dark.ico'),
59 | *Socials(title='DevContainer.ai',
60 | description=description,
61 | site_name='devcontainer.ai',
62 | twitter_site='@daytonaio',
63 | image=f'/assets/og-sq.png',
64 | url=''),
65 | Script(src='https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js'),
66 | scopesrc,
67 | Link(rel="stylesheet", href="/css/main.css"),
68 | ]
69 |
70 | # Initialize FastHTML app
71 | app, rt = fast_app(
72 | hdrs=hdrs,
73 | live=True,
74 | debug=True
75 | )
76 |
77 | scripts = (
78 | Script(src="/js/main.js"),
79 | )
80 |
81 | from fastcore.xtras import timed_cache
82 |
83 | # Main page composition
84 | @timed_cache(seconds=60)
85 | def home():
86 | return (Title(f"DevContainer.ai - {description}"),
87 | Main(
88 | hero_section(),
89 | generator_section(),
90 | setup_section(),
91 | manifesto(),
92 | benefits_section(),
93 | examples_section(),
94 | faq_section(),
95 | cta_section(),
96 | footer_section()),
97 | *scripts)
98 |
99 | # Define routes
100 | @rt("/")
101 | async def get():
102 | return home()
103 |
104 | @rt("/generate", methods=["post"])
105 | async def post(repo_url: str, regenerate: bool = False):
106 | logging.info(f"Generating devcontainer.json for: {repo_url}")
107 |
108 | # Normalize the repo_url by stripping trailing slashes
109 | repo_url = repo_url.rstrip('/')
110 |
111 | try:
112 | exists, existing_record = check_url_exists(repo_url)
113 | logging.info(f"URL check result: exists={exists}, existing_record={existing_record}")
114 |
115 | repo_context, existing_devcontainer, devcontainer_url = fetch_repo_context(repo_url)
116 | logging.info(f"Fetched repo context. Existing devcontainer: {'Yes' if existing_devcontainer else 'No'}")
117 | logging.info(f"Devcontainer URL: {devcontainer_url}")
118 |
119 | if exists and not regenerate:
120 | logging.info(f"URL already exists in database. Returning existing devcontainer_json for: {repo_url}")
121 | devcontainer_json = existing_record['devcontainer_json']
122 | generated = existing_record['generated']
123 | source = "database"
124 | url = existing_record['devcontainer_url']
125 | else:
126 | devcontainer_json, url = generate_devcontainer_json(instructor_client, repo_url, repo_context, devcontainer_url, regenerate=regenerate)
127 | generated = True
128 | source = "generated" if url is None else "repository"
129 |
130 |
131 | if not exists or regenerate:
132 | logging.info("Saving to database...")
133 | try:
134 | if hasattr(openai_client.embeddings, "create"):
135 | embedding_model = os.getenv("EMBEDDING", "text-embedding-ada-002")
136 | max_tokens = int(os.getenv("EMBEDDING_MODEL_MAX_TOKENS", 8192))
137 |
138 | truncated_context = truncate_to_token_limit(repo_context, embedding_model, max_tokens)
139 |
140 | embedding = openai_client.embeddings.create(input=truncated_context, model=embedding_model).data[0].embedding
141 | embedding_json = json.dumps(embedding)
142 | else:
143 | embedding_json = None
144 |
145 | new_devcontainer = DevContainer(
146 | url=repo_url,
147 | devcontainer_json=devcontainer_json,
148 | devcontainer_url=devcontainer_url,
149 | repo_context=repo_context,
150 | tokens=count_tokens(repo_context),
151 | model=os.getenv("MODEL"),
152 | embedding=embedding_json,
153 | generated=generated,
154 | created_at=datetime.utcnow().isoformat() # Ensure this is a string
155 | )
156 |
157 | # Convert the Pydantic model to a dictionary and handle datetime serialization
158 | devcontainer_dict = json.loads(new_devcontainer.json(exclude_unset=True))
159 |
160 | result = supabase.table("devcontainers").insert(devcontainer_dict).execute()
161 | logging.info(f"Successfully saved to database with devcontainer_url: {devcontainer_url}")
162 | except Exception as e:
163 | logging.error(f"Error while saving to database: {str(e)}")
164 | raise
165 |
166 | return Div(
167 | Article(f"Devcontainer.json {'found in ' + source if source in ['database', 'repository'] else 'generated'}"),
168 | caution_section(),
169 | Pre(
170 | Code(devcontainer_json, id="devcontainer-code", cls="overflow-auto"),
171 | Div(
172 | Button(
173 | Img(cls="w-4 h-4", src="assets/icons/copy-icon.svg", alt="Copy"),
174 | cls="icon-button copy-button",
175 | title="Copy to clipboard",
176 | ),
177 | Button(
178 | Img(cls="w-4 h-4", src="assets/icons/regenerate.svg", alt="Regenerate"),
179 | cls="icon-button regenerate-button",
180 | hx_post=f"/generate?regenerate=true&repo_url={repo_url}",
181 | hx_target="#result",
182 | hx_indicator="#action-text",
183 | title="Regenerate",
184 | ),
185 | Span(cls="action-text", id="action-text"),
186 | cls="button-group"
187 | ),
188 | cls="code-container relative"
189 | )
190 | )
191 | except Exception as e:
192 | logging.error(f"An error occurred: {str(e)}", exc_info=True)
193 | return Div(H2("Error"), P(f"An error occurred: {str(e)}"))
194 |
195 | @rt("/manifesto")
196 | async def get():
197 | return manifesto_page()
198 |
199 | # Serve static files
200 | @rt("/{fname:path}.{ext:static}")
201 | async def get(fname:str, ext:str):
202 | return FileResponse(f'{fname}.{ext}')
203 |
204 | # Initialize clients
205 | if check_env_vars():
206 | openai_client = setup_azure_openai()
207 | instructor_client = setup_instructor(openai_client)
208 |
209 | if __name__ == "__main__":
210 | logging.info("Starting FastHTML app...")
211 | serve()
--------------------------------------------------------------------------------
/migrate.py:
--------------------------------------------------------------------------------
1 | import psycopg2
2 | from dotenv import load_dotenv
3 | import os
4 | import sys
5 | import logging
6 |
7 | # Configure logging
8 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
9 |
10 |
11 | # Load environment variables from .env
12 | load_dotenv()
13 |
14 | # Fetch the SUPABASE_DB_URL from environment variables
15 | DATABASE_URL = os.getenv("SUPABASE_DB_URL")
16 |
17 |
18 | # Debugging: Ensure DATABASE_URL is loaded (avoid printing sensitive information)
19 | if DATABASE_URL:
20 | logging.info("SUPABASE_DB_URL loaded successfully.")
21 | else:
22 | logging.error("SUPABASE_DB_URL is not set. Please check your .env file.")
23 | sys.exit(1) # Exit the script with a non-zero status
24 |
25 | # Define the migration SQL
26 | CREATE_TABLE_SQL = """
27 | CREATE TABLE IF NOT EXISTS devcontainers (
28 | id INTEGER NOT NULL,
29 | url VARCHAR,
30 | devcontainer_json TEXT,
31 | devcontainer_url VARCHAR,
32 | repo_context TEXT,
33 | tokens INTEGER,
34 | model TEXT,
35 | embedding TEXT,
36 | generated BOOLEAN,
37 | created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
38 | PRIMARY KEY (id)
39 | );
40 | """
41 |
42 | def main():
43 | connection = None # Initialize connection variable
44 |
45 | try:
46 | # Connect to the PostgreSQL database using the connection string
47 | connection = psycopg2.connect(DATABASE_URL)
48 | connection.autocommit = True # Enable autocommit mode
49 | logging.info("Connection to the database was successful.")
50 |
51 | # Create a cursor to execute SQL queries
52 | cursor = connection.cursor()
53 |
54 | # Execute the CREATE TABLE statement
55 | cursor.execute(CREATE_TABLE_SQL)
56 | logging.info("devcontainers table created (if it didn't exist already).")
57 |
58 | # Optionally, verify the table creation
59 | cursor.execute("""
60 | SELECT table_name
61 | FROM information_schema.tables
62 | WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
63 | """)
64 | tables = cursor.fetchall()
65 | logging.info(f"Current tables in 'public' schema: {tables}")
66 |
67 | # Close the cursor
68 | cursor.close()
69 |
70 | except psycopg2.OperationalError as e:
71 | logging.error("OperationalError: Could not connect to the database.")
72 | logging.error("Please check your SUPABASE_DB_URL and ensure the database server is running.")
73 | logging.error(f"Details: {e}")
74 | sys.exit(1) # Exit the script with a non-zero status
75 |
76 | except Exception as e:
77 | logging.error(f"Error running migration: {e}")
78 | sys.exit(1) # Exit the script with a non-zero status
79 |
80 | finally:
81 | if connection:
82 | connection.close() # Close the connection if it was established
83 | logging.info("Database connection closed.")
84 |
85 | if __name__ == "__main__":
86 | main()
87 |
--------------------------------------------------------------------------------
/models.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 | from datetime import datetime
3 | from typing import Optional
4 |
5 | class DevContainer(BaseModel):
6 | id: Optional[int] = None
7 | url: str
8 | devcontainer_json: str
9 | devcontainer_url: Optional[str]
10 | repo_context: str
11 | tokens: int
12 | model: str
13 | embedding: Optional[str]
14 | generated: bool
15 | created_at: str = datetime.utcnow().isoformat()
--------------------------------------------------------------------------------
/prompts/devcontainer.jinja:
--------------------------------------------------------------------------------
1 | Given the following context from a GitHub repository:
2 |
3 | {{ repo_context }}
4 |
5 | {% if existing_devcontainer %}
6 | An existing devcontainer.json file was found in the repository:
7 |
8 | {{ existing_devcontainer }}
9 |
10 | Please use this as a reference and improve upon it, incorporating any new requirements or best practices.
11 | {% endif %}
12 |
13 | Begin by applying Chain of Thought (CoT) reasoning to decompose the context and task into logical, manageable components. Think slowly and pay attention to all important facts in the context such as the ports used by the application and the ports used for testing.
14 |
15 | Generate a devcontainer.json file for this project. The file should include appropriate settings for the development environment based on the project's requirements and structure. The 'features' field is essential and should include a dictionary of features to enable within the container.
16 |
17 | Always add comments (like in the provided example) to explain what each line or block of code does. This will help you and others who come after you understand what each line of code is doing, why it's there and how it works.
18 |
19 | Here's an example of a devcontainer.json:
20 | ```json
21 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the
22 | // README at: https://github.com/devcontainers/templates/tree/main/src/python
23 | {
24 | "name": "Python 3 with Streamlit: Groq MoA",
25 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
26 | "image": "mcr.microsoft.com/devcontainers/python:3.12-bookworm",
27 |
28 | "containerEnv": {
29 | "GROQ_API_KEY": "${localEnv:GROQ_API_KEY}"
30 | },
31 |
32 | // Features to add to the dev container. More info: https://containers.dev/features.
33 | // "features": {},
34 |
35 | // Configure tool-specific properties.
36 | "customizations": {
37 | // Configure properties specific to VS Code.
38 | "vscode": {
39 | // Set *default* container specific settings.json values on container create.
40 | "settings": {
41 | "python.defaultInterpreterPath": "/usr/local/bin/python",
42 | "python.linting.enabled": true,
43 | "python.linting.pylintEnabled": true,
44 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
45 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
46 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
47 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
48 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
49 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
50 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
51 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
52 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint"
53 | },
54 |
55 | // Add the IDs of extensions you want installed when the container is created.
56 | "extensions": [
57 | "streetsidesoftware.code-spell-checker",
58 | "ms-python.python",
59 | "ms-python.vscode-pylance",
60 | "ms-python.isort",
61 | "njpwerner.autodocstring"
62 | ]
63 | }
64 | },
65 |
66 | // Use 'portsAttributes' to set default properties for specific forwarded ports.
67 | // More info: https://containers.dev/implementors/json_reference/#port-attributes
68 | "portsAttributes": {
69 | "8501": {
70 | "label": "Streamlit - Groq MoA",
71 | "onAutoForward": "openBrowser"
72 | }
73 | },
74 |
75 | // https://containers.dev/implementors/json_reference/#lifecycle-scripts
76 | "postCreateCommand": "pip3 install -r requirements.txt",
77 | "postAttachCommand": "streamlit run app.py"
78 |
79 | }
80 | ```
81 | Your goal is to deliver the most logical, secure, efficient, and well-documented devcontainer.json file.
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohappyeyeballs==2.3.5
2 | aiohttp==3.10.2
3 | aiosignal==1.3.1
4 | annotated-types==0.7.0
5 | anyio==4.4.0
6 | appnope==0.1.4
7 | asttokens==2.4.1
8 | async-timeout==4.0.3
9 | attrs==24.2.0
10 | beautifulsoup4==4.12.3
11 | black==24.8.0
12 | certifi==2024.7.4
13 | charset-normalizer==2.1.1
14 | click==8.1.7
15 | cloudscraper==1.2.71
16 | comm==0.2.2
17 | debugpy==1.8.5
18 | decorator==5.1.1
19 | deprecation==2.1.0
20 | distro==1.9.0
21 | docstring_parser==0.16
22 | exceptiongroup==1.2.2
23 | executing==2.0.1
24 | fastcore==1.6.3
25 | fastlite==0.0.7
26 | frozenlist==1.4.1
27 | gitdb==4.0.11
28 | GitPython==3.1.41
29 | gotrue==2.8.0
30 | greenlet==3.0.3
31 | h11==0.14.0
32 | h2==4.1.0
33 | hpack==4.0.0
34 | httpcore==1.0.5
35 | httptools==0.6.1
36 | httpx==0.27.0
37 | hyperframe==6.0.1
38 | idna==3.7
39 | instructor==1.4.0
40 | ipykernel==6.29.5
41 | ipython==8.26.0
42 | isort==5.13.2
43 | itsdangerous==2.2.0
44 | jedi==0.19.1
45 | Jinja2==3.1.4
46 | jiter==0.4.2
47 | jsonschema==4.23.0
48 | jsonschema-specifications==2023.12.1
49 | jupyter_client==8.6.2
50 | jupyter_core==5.7.2
51 | kaleido==0.2.1
52 | markdown-it-py==3.0.0
53 | MarkupSafe==2.1.5
54 | matplotlib-inline==0.1.7
55 | mdurl==0.1.2
56 | multidict==6.0.5
57 | mypy-extensions==1.0.0
58 | nest-asyncio==1.6.0
59 | numpy==2.0.1
60 | oauthlib==3.2.2
61 | openai==1.40.2
62 | packaging==24.1
63 | pandas==2.2.2
64 | parso==0.8.4
65 | pathspec==0.12.1
66 | pexpect==4.9.0
67 | platformdirs==4.2.2
68 | plotly==5.23.0
69 | postgrest==0.16.11
70 | prompt_toolkit==3.0.47
71 | psutil==6.0.0
72 | ptyprocess==0.7.0
73 | pure_eval==0.2.3
74 | pydantic==2.8.2
75 | pydantic_core==2.20.1
76 | Pygments==2.18.0
77 | pyparsing==3.1.2
78 | python-dateutil==2.9.0.post0
79 | python-dotenv==1.0.1
80 | python-fasthtml==0.2.4
81 | python-multipart==0.0.9
82 | pytz==2024.1
83 | PyYAML==6.0.2
84 | pyzmq==26.1.0
85 | realtime==2.0.2
86 | referencing==0.35.1
87 | regex==2024.7.24
88 | replicate==0.31.0
89 | requests==2.32.3
90 | requests-toolbelt==1.0.0
91 | rich==13.7.1
92 | rpds-py==0.20.0
93 | setuptools==69.0.3
94 | shellingham==1.5.4
95 | six==1.16.0
96 | smmap==5.0.1
97 | sniffio==1.3.1
98 | soupsieve==2.5
99 | SQLAlchemy==2.0.32
100 | sqlite-vec==0.1.6
101 | sqlite_minutils==3.36.0.post4
102 | stack-data==0.6.3
103 | starlette==0.38.2
104 | storage3==0.7.7
105 | StrEnum==0.4.15
106 | supabase==2.8.0
107 | supafunc==0.5.1
108 | tenacity==8.5.0
109 | tiktoken==0.7.0
110 | tornado==6.4.1
111 | tqdm==4.66.5
112 | traitlets==5.14.3
113 | typer==0.12.3
114 | typing_extensions==4.12.2
115 | tzdata==2024.1
116 | urllib3==2.2.2
117 | uvicorn==0.30.5
118 | uvloop==0.19.0
119 | watchfiles==0.23.0
120 | wcwidth==0.2.13
121 | websockets==12.0
122 | wheel==0.44.0
123 | yarl==1.9.4
124 | psycopg2-binary==2.9.10
125 |
--------------------------------------------------------------------------------
/schemas.py:
--------------------------------------------------------------------------------
1 | # schemas.py
2 | from pydantic import BaseModel, Field
3 | from typing import Optional
4 |
5 | class DevContainerModel(BaseModel):
6 | name: str = Field(description="Name of the dev container")
7 | image: str = Field(description="Docker image to use")
8 | forwardPorts: Optional[list[int]] = Field(
9 | description="Ports to forward from the container to the local machine"
10 | )
11 | customizations: Optional[dict] = Field(
12 | None, description="Tool-specific configuration"
13 | )
14 | settings: Optional[dict] = Field(
15 | None, description="VS Code settings to configure the development environment"
16 | )
17 | postCreateCommand: Optional[str] = Field(
18 | description="Command to run after creating the container"
19 | )
--------------------------------------------------------------------------------
/schemas/devContainer.base.schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json-schema.org/draft/2019-09/schema",
3 | "description": "Defines a dev container",
4 | "allowComments": true,
5 | "allowTrailingCommas": false,
6 | "definitions": {
7 | "devContainerCommon": {
8 | "type": "object",
9 | "properties": {
10 | "$schema": {
11 | "type": "string",
12 | "format": "uri",
13 | "description": "The JSON schema of the `devcontainer.json` file."
14 | },
15 | "name": {
16 | "type": "string",
17 | "description": "A name for the dev container which can be displayed to the user."
18 | },
19 | "features": {
20 | "type": "object",
21 | "description": "Features to add to the dev container.",
22 | "properties": {
23 | "fish": {
24 | "deprecated": true,
25 | "deprecationMessage": "Legacy feature not supported. Please check https://containers.dev/features for replacements."
26 | },
27 | "maven": {
28 | "deprecated": true,
29 | "deprecationMessage": "Legacy feature will be removed in the future. Please check https://containers.dev/features for replacements. E.g., `ghcr.io/devcontainers/features/java` has an option to install Maven."
30 | },
31 | "gradle": {
32 | "deprecated": true,
33 | "deprecationMessage": "Legacy feature will be removed in the future. Please check https://containers.dev/features for replacements. E.g., `ghcr.io/devcontainers/features/java` has an option to install Gradle."
34 | },
35 | "homebrew": {
36 | "deprecated": true,
37 | "deprecationMessage": "Legacy feature not supported. Please check https://containers.dev/features for replacements."
38 | },
39 | "jupyterlab": {
40 | "deprecated": true,
41 | "deprecationMessage": "Legacy feature will be removed in the future. Please check https://containers.dev/features for replacements. E.g., `ghcr.io/devcontainers/features/python` has an option to install JupyterLab."
42 | }
43 | },
44 | "additionalProperties": true
45 | },
46 | "overrideFeatureInstallOrder": {
47 | "type": "array",
48 | "description": "Array consisting of the Feature id (without the semantic version) of Features in the order the user wants them to be installed.",
49 | "items": {
50 | "type": "string"
51 | }
52 | },
53 | "secrets": {
54 | "type": "object",
55 | "description": "Recommended secrets for this dev container. Recommendations are provided as environment variable keys with optional metadata.",
56 | "patternProperties": {
57 | "^[a-zA-Z_][a-zA-Z0-9_]*$": {
58 | "type": "object",
59 | "description": "Environment variable keys following unix-style naming conventions. eg: ^[a-zA-Z_][a-zA-Z0-9_]*$",
60 | "properties": {
61 | "description": {
62 | "type": "string",
63 | "description": "A description of the secret."
64 | },
65 | "documentationUrl": {
66 | "type": "string",
67 | "format": "uri",
68 | "description": "A URL to documentation about the secret."
69 | }
70 | },
71 | "additionalProperties": false
72 | },
73 | "additionalProperties": false
74 | },
75 | "additionalProperties": false
76 | },
77 | "forwardPorts": {
78 | "type": "array",
79 | "description": "Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format \"host:port_number\".",
80 | "items": {
81 | "oneOf": [
82 | {
83 | "type": "integer",
84 | "maximum": 65535,
85 | "minimum": 0
86 | },
87 | {
88 | "type": "string",
89 | "pattern": "^([a-z0-9-]+):(\\d{1,5})$"
90 | }
91 | ]
92 | }
93 | },
94 | "portsAttributes": {
95 | "type": "object",
96 | "patternProperties": {
97 | "(^\\d+(-\\d+)?$)|(.+)": {
98 | "type": "object",
99 | "description": "A port, range of ports (ex. \"40000-55000\"), or regular expression (ex. \".+\\\\/server.js\"). For a port number or range, the attributes will apply to that port number or range of port numbers. Attributes which use a regular expression will apply to ports whose associated process command line matches the expression.",
100 | "properties": {
101 | "onAutoForward": {
102 | "type": "string",
103 | "enum": [
104 | "notify",
105 | "openBrowser",
106 | "openBrowserOnce",
107 | "openPreview",
108 | "silent",
109 | "ignore"
110 | ],
111 | "enumDescriptions": [
112 | "Shows a notification when a port is automatically forwarded.",
113 | "Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.",
114 | "Opens the browser when the port is automatically forwarded, but only the first time the port is forward during a session. Depending on your settings, this could open an embedded browser.",
115 | "Opens a preview in the same window when the port is automatically forwarded.",
116 | "Shows no notification and takes no action when this port is automatically forwarded.",
117 | "This port will not be automatically forwarded."
118 | ],
119 | "description": "Defines the action that occurs when the port is discovered for automatic forwarding",
120 | "default": "notify"
121 | },
122 | "elevateIfNeeded": {
123 | "type": "boolean",
124 | "description": "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.",
125 | "default": false
126 | },
127 | "label": {
128 | "type": "string",
129 | "description": "Label that will be shown in the UI for this port.",
130 | "default": "Application"
131 | },
132 | "requireLocalPort": {
133 | "type": "boolean",
134 | "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.",
135 | "default": false
136 | },
137 | "protocol": {
138 | "type": "string",
139 | "enum": [
140 | "http",
141 | "https"
142 | ],
143 | "description": "The protocol to use when forwarding this port."
144 | }
145 | },
146 | "default": {
147 | "label": "Application",
148 | "onAutoForward": "notify"
149 | }
150 | }
151 | },
152 | "markdownDescription": "Set default properties that are applied when a specific port number is forwarded. For example:\n\n```\n\"3000\": {\n \"label\": \"Application\"\n},\n\"40000-55000\": {\n \"onAutoForward\": \"ignore\"\n},\n\".+\\\\/server.js\": {\n \"onAutoForward\": \"openPreview\"\n}\n```",
153 | "defaultSnippets": [
154 | {
155 | "body": {
156 | "${1:3000}": {
157 | "label": "${2:Application}",
158 | "onAutoForward": "notify"
159 | }
160 | }
161 | }
162 | ],
163 | "additionalProperties": false
164 | },
165 | "otherPortsAttributes": {
166 | "type": "object",
167 | "properties": {
168 | "onAutoForward": {
169 | "type": "string",
170 | "enum": [
171 | "notify",
172 | "openBrowser",
173 | "openPreview",
174 | "silent",
175 | "ignore"
176 | ],
177 | "enumDescriptions": [
178 | "Shows a notification when a port is automatically forwarded.",
179 | "Opens the browser when the port is automatically forwarded. Depending on your settings, this could open an embedded browser.",
180 | "Opens a preview in the same window when the port is automatically forwarded.",
181 | "Shows no notification and takes no action when this port is automatically forwarded.",
182 | "This port will not be automatically forwarded."
183 | ],
184 | "description": "Defines the action that occurs when the port is discovered for automatic forwarding",
185 | "default": "notify"
186 | },
187 | "elevateIfNeeded": {
188 | "type": "boolean",
189 | "description": "Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.",
190 | "default": false
191 | },
192 | "label": {
193 | "type": "string",
194 | "description": "Label that will be shown in the UI for this port.",
195 | "default": "Application"
196 | },
197 | "requireLocalPort": {
198 | "type": "boolean",
199 | "markdownDescription": "When true, a modal dialog will show if the chosen local port isn't used for forwarding.",
200 | "default": false
201 | },
202 | "protocol": {
203 | "type": "string",
204 | "enum": [
205 | "http",
206 | "https"
207 | ],
208 | "description": "The protocol to use when forwarding this port."
209 | }
210 | },
211 | "defaultSnippets": [
212 | {
213 | "body": {
214 | "onAutoForward": "ignore"
215 | }
216 | }
217 | ],
218 | "markdownDescription": "Set default properties that are applied to all ports that don't get properties from the setting `remote.portsAttributes`. For example:\n\n```\n{\n \"onAutoForward\": \"ignore\"\n}\n```",
219 | "additionalProperties": false
220 | },
221 | "updateRemoteUserUID": {
222 | "type": "boolean",
223 | "description": "Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder."
224 | },
225 | "containerEnv": {
226 | "type": "object",
227 | "additionalProperties": {
228 | "type": "string"
229 | },
230 | "description": "Container environment variables."
231 | },
232 | "containerUser": {
233 | "type": "string",
234 | "description": "The user the container will be started with. The default is the user on the Docker image."
235 | },
236 | "mounts": {
237 | "type": "array",
238 | "description": "Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax.",
239 | "items": {
240 | "anyOf": [
241 | {
242 | "$ref": "#/definitions/Mount"
243 | },
244 | {
245 | "type": "string"
246 | }
247 | ]
248 | }
249 | },
250 | "init": {
251 | "type": "boolean",
252 | "description": "Passes the --init flag when creating the dev container."
253 | },
254 | "privileged": {
255 | "type": "boolean",
256 | "description": "Passes the --privileged flag when creating the dev container."
257 | },
258 | "capAdd": {
259 | "type": "array",
260 | "description": "Passes docker capabilities to include when creating the dev container.",
261 | "examples": [
262 | "SYS_PTRACE"
263 | ],
264 | "items": {
265 | "type": "string"
266 | }
267 | },
268 | "securityOpt": {
269 | "type": "array",
270 | "description": "Passes docker security options to include when creating the dev container.",
271 | "examples": [
272 | "seccomp=unconfined"
273 | ],
274 | "items": {
275 | "type": "string"
276 | }
277 | },
278 | "remoteEnv": {
279 | "type": "object",
280 | "additionalProperties": {
281 | "type": [
282 | "string",
283 | "null"
284 | ]
285 | },
286 | "description": "Remote environment variables to set for processes spawned in the container including lifecycle scripts and any remote editor/IDE server process."
287 | },
288 | "remoteUser": {
289 | "type": "string",
290 | "description": "The username to use for spawning processes in the container including lifecycle scripts and any remote editor/IDE server process. The default is the same user as the container."
291 | },
292 | "initializeCommand": {
293 | "type": [
294 | "string",
295 | "array",
296 | "object"
297 | ],
298 | "description": "A command to run locally (i.e Your host machine, cloud VM) before anything else. This command is run before \"onCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
299 | "items": {
300 | "type": "string"
301 | },
302 | "additionalProperties": {
303 | "type": [
304 | "string",
305 | "array"
306 | ],
307 | "items": {
308 | "type": "string"
309 | }
310 | }
311 | },
312 | "onCreateCommand": {
313 | "type": [
314 | "string",
315 | "array",
316 | "object"
317 | ],
318 | "description": "A command to run when creating the container. This command is run after \"initializeCommand\" and before \"updateContentCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
319 | "items": {
320 | "type": "string"
321 | },
322 | "additionalProperties": {
323 | "type": [
324 | "string",
325 | "array"
326 | ],
327 | "items": {
328 | "type": "string"
329 | }
330 | }
331 | },
332 | "updateContentCommand": {
333 | "type": [
334 | "string",
335 | "array",
336 | "object"
337 | ],
338 | "description": "A command to run when creating the container and rerun when the workspace content was updated while creating the container. This command is run after \"onCreateCommand\" and before \"postCreateCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
339 | "items": {
340 | "type": "string"
341 | },
342 | "additionalProperties": {
343 | "type": [
344 | "string",
345 | "array"
346 | ],
347 | "items": {
348 | "type": "string"
349 | }
350 | }
351 | },
352 | "postCreateCommand": {
353 | "type": [
354 | "string",
355 | "array",
356 | "object"
357 | ],
358 | "description": "A command to run after creating the container. This command is run after \"updateContentCommand\" and before \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
359 | "items": {
360 | "type": "string"
361 | },
362 | "additionalProperties": {
363 | "type": [
364 | "string",
365 | "array"
366 | ],
367 | "items": {
368 | "type": "string"
369 | }
370 | }
371 | },
372 | "postStartCommand": {
373 | "type": [
374 | "string",
375 | "array",
376 | "object"
377 | ],
378 | "description": "A command to run after starting the container. This command is run after \"postCreateCommand\" and before \"postAttachCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
379 | "items": {
380 | "type": "string"
381 | },
382 | "additionalProperties": {
383 | "type": [
384 | "string",
385 | "array"
386 | ],
387 | "items": {
388 | "type": "string"
389 | }
390 | }
391 | },
392 | "postAttachCommand": {
393 | "type": [
394 | "string",
395 | "array",
396 | "object"
397 | ],
398 | "description": "A command to run when attaching to the container. This command is run after \"postStartCommand\". If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell. If this is an object, each provided command will be run in parallel.",
399 | "items": {
400 | "type": "string"
401 | },
402 | "additionalProperties": {
403 | "type": [
404 | "string",
405 | "array"
406 | ],
407 | "items": {
408 | "type": "string"
409 | }
410 | }
411 | },
412 | "waitFor": {
413 | "type": "string",
414 | "enum": [
415 | "initializeCommand",
416 | "onCreateCommand",
417 | "updateContentCommand",
418 | "postCreateCommand",
419 | "postStartCommand"
420 | ],
421 | "description": "The user command to wait for before continuing execution in the background while the UI is starting up. The default is \"updateContentCommand\"."
422 | },
423 | "userEnvProbe": {
424 | "type": "string",
425 | "enum": [
426 | "none",
427 | "loginShell",
428 | "loginInteractiveShell",
429 | "interactiveShell"
430 | ],
431 | "description": "User environment probe to run. The default is \"loginInteractiveShell\"."
432 | },
433 | "hostRequirements": {
434 | "type": "object",
435 | "description": "Host hardware requirements.",
436 | "properties": {
437 | "cpus": {
438 | "type": "integer",
439 | "minimum": 1,
440 | "description": "Number of required CPUs."
441 | },
442 | "memory": {
443 | "type": "string",
444 | "pattern": "^\\d+([tgmk]b)?$",
445 | "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb."
446 | },
447 | "storage": {
448 | "type": "string",
449 | "pattern": "^\\d+([tgmk]b)?$",
450 | "description": "Amount of required disk space in bytes. Supports units tb, gb, mb and kb."
451 | },
452 | "gpu": {
453 | "oneOf": [
454 | {
455 | "type": [
456 | "boolean",
457 | "string"
458 | ],
459 | "enum": [
460 | true,
461 | false,
462 | "optional"
463 | ],
464 | "description": "Indicates whether a GPU is required. The string \"optional\" indicates that a GPU is optional. An object value can be used to configure more detailed requirements."
465 | },
466 | {
467 | "type": "object",
468 | "properties": {
469 | "cores": {
470 | "type": "integer",
471 | "minimum": 1,
472 | "description": "Number of required cores."
473 | },
474 | "memory": {
475 | "type": "string",
476 | "pattern": "^\\d+([tgmk]b)?$",
477 | "description": "Amount of required RAM in bytes. Supports units tb, gb, mb and kb."
478 | }
479 | },
480 | "description": "Indicates whether a GPU is required. The string \"optional\" indicates that a GPU is optional. An object value can be used to configure more detailed requirements.",
481 | "additionalProperties": false
482 | }
483 | ]
484 | }
485 | },
486 | "unevaluatedProperties": false
487 | },
488 | "customizations": {
489 | "type": "object",
490 | "description": "Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations."
491 | },
492 | "additionalProperties": {
493 | "type": "object",
494 | "additionalProperties": true
495 | }
496 | }
497 | },
498 | "nonComposeBase": {
499 | "type": "object",
500 | "properties": {
501 | "appPort": {
502 | "type": [
503 | "integer",
504 | "string",
505 | "array"
506 | ],
507 | "description": "Application ports that are exposed by the container. This can be a single port or an array of ports. Each port can be a number or a string. A number is mapped to the same port on the host. A string is passed to Docker unchanged and can be used to map ports differently, e.g. \"8000:8010\".",
508 | "items": {
509 | "type": [
510 | "integer",
511 | "string"
512 | ]
513 | }
514 | },
515 | "runArgs": {
516 | "type": "array",
517 | "description": "The arguments required when starting in the container.",
518 | "items": {
519 | "type": "string"
520 | }
521 | },
522 | "shutdownAction": {
523 | "type": "string",
524 | "enum": [
525 | "none",
526 | "stopContainer"
527 | ],
528 | "description": "Action to take when the user disconnects from the container in their editor. The default is to stop the container."
529 | },
530 | "overrideCommand": {
531 | "type": "boolean",
532 | "description": "Whether to overwrite the command specified in the image. The default is true."
533 | },
534 | "workspaceFolder": {
535 | "type": "string",
536 | "description": "The path of the workspace folder inside the container."
537 | },
538 | "workspaceMount": {
539 | "type": "string",
540 | "description": "The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project."
541 | }
542 | }
543 | },
544 | "dockerfileContainer": {
545 | "oneOf": [
546 | {
547 | "type": "object",
548 | "properties": {
549 | "build": {
550 | "type": "object",
551 | "description": "Docker build-related options.",
552 | "allOf": [
553 | {
554 | "type": "object",
555 | "properties": {
556 | "dockerfile": {
557 | "type": "string",
558 | "description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file."
559 | },
560 | "context": {
561 | "type": "string",
562 | "description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file."
563 | }
564 | },
565 | "required": [
566 | "dockerfile"
567 | ]
568 | },
569 | {
570 | "$ref": "#/definitions/buildOptions"
571 | }
572 | ],
573 | "unevaluatedProperties": false
574 | }
575 | },
576 | "required": [
577 | "build"
578 | ]
579 | },
580 | {
581 | "allOf": [
582 | {
583 | "type": "object",
584 | "properties": {
585 | "dockerFile": {
586 | "type": "string",
587 | "description": "The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file."
588 | },
589 | "context": {
590 | "type": "string",
591 | "description": "The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file."
592 | }
593 | },
594 | "required": [
595 | "dockerFile"
596 | ]
597 | },
598 | {
599 | "type": "object",
600 | "properties": {
601 | "build": {
602 | "description": "Docker build-related options.",
603 | "$ref": "#/definitions/buildOptions"
604 | }
605 | }
606 | }
607 | ]
608 | }
609 | ]
610 | },
611 | "buildOptions": {
612 | "type": "object",
613 | "properties": {
614 | "target": {
615 | "type": "string",
616 | "description": "Target stage in a multi-stage build."
617 | },
618 | "args": {
619 | "type": "object",
620 | "additionalProperties": {
621 | "type": [
622 | "string"
623 | ]
624 | },
625 | "description": "Build arguments."
626 | },
627 | "cacheFrom": {
628 | "type": [
629 | "string",
630 | "array"
631 | ],
632 | "description": "The image to consider as a cache. Use an array to specify multiple images.",
633 | "items": {
634 | "type": "string"
635 | }
636 | },
637 | "options": {
638 | "type": "array",
639 | "description": "Additional arguments passed to the build command.",
640 | "items": {
641 | "type": "string"
642 | }
643 | }
644 | }
645 | },
646 | "imageContainer": {
647 | "type": "object",
648 | "properties": {
649 | "image": {
650 | "type": "string",
651 | "description": "The docker image that will be used to create the container."
652 | }
653 | },
654 | "required": [
655 | "image"
656 | ]
657 | },
658 | "composeContainer": {
659 | "type": "object",
660 | "properties": {
661 | "dockerComposeFile": {
662 | "type": [
663 | "string",
664 | "array"
665 | ],
666 | "description": "The name of the docker-compose file(s) used to start the services.",
667 | "items": {
668 | "type": "string"
669 | }
670 | },
671 | "service": {
672 | "type": "string",
673 | "description": "The service you want to work on. This is considered the primary container for your dev environment which your editor will connect to."
674 | },
675 | "runServices": {
676 | "type": "array",
677 | "description": "An array of services that should be started and stopped.",
678 | "items": {
679 | "type": "string"
680 | }
681 | },
682 | "workspaceFolder": {
683 | "type": "string",
684 | "description": "The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml."
685 | },
686 | "shutdownAction": {
687 | "type": "string",
688 | "enum": [
689 | "none",
690 | "stopCompose"
691 | ],
692 | "description": "Action to take when the user disconnects from the primary container in their editor. The default is to stop all of the compose containers."
693 | },
694 | "overrideCommand": {
695 | "type": "boolean",
696 | "description": "Whether to overwrite the command specified in the image. The default is false."
697 | }
698 | },
699 | "required": [
700 | "dockerComposeFile",
701 | "service",
702 | "workspaceFolder"
703 | ]
704 | },
705 | "Mount": {
706 | "type": "object",
707 | "properties": {
708 | "type": {
709 | "type": "string",
710 | "enum": [
711 | "bind",
712 | "volume"
713 | ],
714 | "description": "Mount type."
715 | },
716 | "source": {
717 | "type": "string",
718 | "description": "Mount source."
719 | },
720 | "target": {
721 | "type": "string",
722 | "description": "Mount target."
723 | }
724 | },
725 | "required": [
726 | "type",
727 | "target"
728 | ],
729 | "additionalProperties": false
730 | }
731 | },
732 | "oneOf": [
733 | {
734 | "allOf": [
735 | {
736 | "oneOf": [
737 | {
738 | "allOf": [
739 | {
740 | "oneOf": [
741 | {
742 | "$ref": "#/definitions/dockerfileContainer"
743 | },
744 | {
745 | "$ref": "#/definitions/imageContainer"
746 | }
747 | ]
748 | },
749 | {
750 | "$ref": "#/definitions/nonComposeBase"
751 | }
752 | ]
753 | },
754 | {
755 | "$ref": "#/definitions/composeContainer"
756 | }
757 | ]
758 | },
759 | {
760 | "$ref": "#/definitions/devContainerCommon"
761 | }
762 | ]
763 | },
764 | {
765 | "type": "object",
766 | "$ref": "#/definitions/devContainerCommon",
767 | "additionalProperties": false
768 | }
769 | ],
770 | "unevaluatedProperties": false
771 | }
772 |
--------------------------------------------------------------------------------
/supabase_client.py:
--------------------------------------------------------------------------------
1 | import os
2 | from supabase import create_client, Client
3 | from dotenv import load_dotenv
4 |
5 | load_dotenv()
6 |
7 | url: str = os.environ.get("SUPABASE_URL")
8 | key: str = os.environ.get("SUPABASE_KEY")
9 | supabase: Client = create_client(url, key)
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["*.py"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | import os
2 | import requests
3 | from dotenv import load_dotenv
4 |
5 | def main():
6 | # Unset any existing GITHUB_TOKEN environment variable
7 | if 'GITHUB_TOKEN' in os.environ:
8 | del os.environ['GITHUB_TOKEN']
9 |
10 | # Load environment variables from .env file
11 | load_dotenv()
12 |
13 | token = os.getenv("GITHUB_TOKEN")
14 | if not token:
15 | print("GITHUB_TOKEN environment variable is not set.")
16 | return
17 |
18 | # Print all environment variables starting with GITHUB to debug
19 | print("Environment Variables:")
20 | for key, value in os.environ.items():
21 | if 'GITHUB' in key:
22 | print(f"{key}: {value[:5]}...")
23 |
24 | contents_api_url = "https://api.github.com/repos/ckeditor/ckeditor5/contents"
25 |
26 | headers = {
27 | "Accept": "application/vnd.github.v3+json",
28 | "Authorization": f"token {token}",
29 | }
30 |
31 | print(f"GITHUB_TOKEN: {token[:5]}...")
32 | print(f"Headers: {headers}")
33 | print(f"Contents API URL: {contents_api_url}")
34 |
35 | response = requests.get(contents_api_url, headers=headers)
36 | print(f"Response (standard): {response.status_code} - {response.text}")
37 |
38 | http_proxy = os.getenv('HTTP_PROXY', '')
39 | https_proxy = os.getenv('HTTPS_PROXY', '')
40 | print(f"HTTP_PROXY: {http_proxy}")
41 | print(f"HTTPS_PROXY: {https_proxy}")
42 |
43 | # Try turning off SSL verify just to debug if it's causing issues
44 | response_no_ssl = requests.get(contents_api_url, headers=headers, verify=False)
45 | print(f"Response (no SSL verify): {response_no_ssl.status_code} - {response_no_ssl.text}")
46 |
47 | if __name__ == "__main__":
48 | main()
--------------------------------------------------------------------------------