create-caspian-app 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- create_caspian_app-0.0.1/.gitignore +8 -0
- create_caspian_app-0.0.1/CHANGELOG.md +12 -0
- create_caspian_app-0.0.1/CONTRIBUTING.md +62 -0
- create_caspian_app-0.0.1/LICENSE +21 -0
- create_caspian_app-0.0.1/PKG-INFO +116 -0
- create_caspian_app-0.0.1/README.md +94 -0
- create_caspian_app-0.0.1/pyproject.toml +55 -0
- create_caspian_app-0.0.1/scripts/sync_template.py +272 -0
- create_caspian_app-0.0.1/src/create_caspian_app/__init__.py +5 -0
- create_caspian_app-0.0.1/src/create_caspian_app/__main__.py +5 -0
- create_caspian_app-0.0.1/src/create_caspian_app/casp.py +74 -0
- create_caspian_app-0.0.1/src/create_caspian_app/cli.py +192 -0
- create_caspian_app-0.0.1/src/create_caspian_app/envfile.py +185 -0
- create_caspian_app-0.0.1/src/create_caspian_app/features.py +87 -0
- create_caspian_app-0.0.1/src/create_caspian_app/installer.py +106 -0
- create_caspian_app-0.0.1/src/create_caspian_app/scaffold.py +319 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/.claude/settings.json +3 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/.github/copilot-instructions.md +169 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/.github/instructions/database-generation.instructions.md +47 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/.github/instructions/tailwind-build.instructions.md +20 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/.python-version +1 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/AGENTS.md +455 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/CLAUDE.md +1 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/README.md +63 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/_gitignore +32 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/README.md +24 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/agent-development.md +22 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/ai-validation-checklist.md +116 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/auth.md +793 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/authorization.md +127 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/background-jobs.md +139 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/cache.md +71 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/commands.md +86 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/components.md +820 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/core-runtime-map.md +192 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/database.md +427 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/environment.md +205 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/error-handling.md +127 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/fetch-data.md +536 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/file-conventions.md +315 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/file-uploads.md +219 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/i18n.md +134 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/index.md +161 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/installation.md +29 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/maintenance.md +88 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/mcp.md +60 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/metadata.md +319 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/observability.md +116 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/prepared-pages.md +115 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/project-structure.md +418 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/pulsepoint-runtime-map.md +207 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/pulsepoint.md +2019 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/request-pipeline.md +108 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/routing.md +597 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/state.md +278 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/static-export.md +112 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/task-scheduling.md +80 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/testing.md +324 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/validation.md +370 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/docs/caspian/websockets.md +226 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/main.py +2198 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/prisma/migrations/20260930065337/migration.sql +31 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/prisma/migrations/migration_lock.toml +3 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/prisma/schema.prisma +26 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/prisma/seed.py +40 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/public/css/styles.css +964 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/public/favicon.ico +0 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/public/js/bundle-mjs.mjs.map +1 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/public/js/main.js +16 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/public/js/pp-reactive-v2.min.js +49 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/public/js/tailwind-merge.mjs +3368 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/pyproject.toml +73 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/_component_imports.py +71 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/_markup_equivalence.py +867 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/browser_log.py +526 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/build-static.py +307 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/build.py +49 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/build_pages.py +26 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/check_templates.py +497 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/dev_log_bridge.py +387 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/discovery.py +58 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/fix.py +113 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/format.py +719 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/page_cache.py +62 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/prepare.py +195 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/prepared_pages.py +360 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/python_server.py +159 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/serve-static.py +187 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/serve_mcp.py +30 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/tailwind-tool.json +13 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/tailwind.py +298 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/settings/test.py +419 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/app/error.py +94 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/app/globals.css +34 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/app/index.py +99 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/app/layout.py +43 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/app/not_found.py +28 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/auth/auth_config.py +46 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/mcp/fastmcp.json +15 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/mcp/mcp_server.py +93 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/prisma/__init__.py +43 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/prisma/db.py +10289 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/prisma/models.py +585 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/src/lib/websocket/sockets.py +873 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/tests/README.md +63 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/tests/conftest.py +156 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/tests/test_app.py +48 -0
- create_caspian_app-0.0.1/src/create_caspian_app/template/uv.lock +1896 -0
- create_caspian_app-0.0.1/src/create_caspian_app/ui.py +101 -0
- create_caspian_app-0.0.1/template_overrides/docs/caspian/testing.md +324 -0
- create_caspian_app-0.0.1/template_overrides/tests/README.md +63 -0
- create_caspian_app-0.0.1/template_overrides/tests/conftest.py +156 -0
- create_caspian_app-0.0.1/template_overrides/tests/test_app.py +48 -0
- create_caspian_app-0.0.1/tests/test_scaffold.py +306 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.0.1 — Alpha
|
|
4
|
+
|
|
5
|
+
Initial release of the Caspian Python app generator.
|
|
6
|
+
|
|
7
|
+
- Create apps with the `create-caspian-app` command and a bundled starter template.
|
|
8
|
+
- Choose backend-only apps, Tailwind CSS, MCP, Prisma, and WebSocket support.
|
|
9
|
+
- Generate environment files with fresh secrets for each project.
|
|
10
|
+
- Update existing apps with `casp update project`, preserving managed user files
|
|
11
|
+
and configured exclusions.
|
|
12
|
+
- Include starter app tests and development guides.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Maintaining create-caspian-app
|
|
2
|
+
|
|
3
|
+
Use Python 3.14 for package development and release checks.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
The app template comes from the Caspian starter repository
|
|
7
|
+
(`create-caspian-app-python`) and is bundled inside the wheel, so creating an app
|
|
8
|
+
works offline and each release always installs the same files.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv run python scripts/sync_template.py # copy git-tracked files from ../create-caspian-app-python
|
|
12
|
+
uv run python scripts/sync_template.py --check # fail if the bundled template is stale
|
|
13
|
+
uv run pytest # installer tests
|
|
14
|
+
uv run ruff check src/create_caspian_app tests scripts
|
|
15
|
+
uv build # dist/*.whl + dist/*.tar.gz
|
|
16
|
+
uvx twine check --strict dist/create_caspian_app-0.0.1-py3-none-any.whl dist/create_caspian_app-0.0.1.tar.gz
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Publish 0.0.1
|
|
20
|
+
|
|
21
|
+
The version comes from `src/create_caspian_app/__init__.py`; the package metadata
|
|
22
|
+
marks its development status as Alpha. The exact version is `0.0.1`, so package
|
|
23
|
+
installers treat it as a normal release. A version such as `0.0.1a1` would instead
|
|
24
|
+
be an alpha prerelease under Python's versioning rules.
|
|
25
|
+
|
|
26
|
+
After the checks above pass, upload the two verified artifacts:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv publish dist/create_caspian_app-0.0.1-py3-none-any.whl dist/create_caspian_app-0.0.1.tar.gz
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Provide a PyPI API token through `UV_PUBLISH_TOKEN` in your shell or CI secret
|
|
33
|
+
store. Never commit the token. PyPI does not allow replacing an uploaded release
|
|
34
|
+
file; use a new version for subsequent changes.
|
|
35
|
+
|
|
36
|
+
To rehearse on TestPyPI, use a separate TestPyPI token and upload endpoint:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv publish --publish-url https://test.pypi.org/legacy/ dist/create_caspian_app-0.0.1-py3-none-any.whl dist/create_caspian_app-0.0.1.tar.gz
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Only files tracked by git in the source repo get copied. `.venv/`, `caspian.config.json`,
|
|
43
|
+
`.env`, and bytecode are always left out. The source repo is also the framework's
|
|
44
|
+
development workspace, so the sync also leaves out its demo pages, benchmarks,
|
|
45
|
+
and the framework's own test suite (`DEVELOPMENT_ONLY` in
|
|
46
|
+
`scripts/sync_template.py`), and applies small checked text edits (`EDITS`). When the
|
|
47
|
+
source changes so that one of those edits no longer applies, the sync stops with an error,
|
|
48
|
+
so development content never ships silently.
|
|
49
|
+
|
|
50
|
+
`template_overrides/` holds the files this package owns, which are added to (or replace
|
|
51
|
+
files in) the template:
|
|
52
|
+
|
|
53
|
+
- `tests/conftest.py`: the app test fixtures (`client`, `rpc`, `csrf_headers`, `sign_in`,
|
|
54
|
+
`config`) and the `feature(...)` / `frontend` markers.
|
|
55
|
+
- `tests/test_app.py`: starter tests for the app shell.
|
|
56
|
+
- `tests/README.md`: the gate, formatting, and test commands.
|
|
57
|
+
- `docs/caspian/testing.md`: the testing guide AI agents follow ("what to test for each
|
|
58
|
+
change", with a recipe for each surface).
|
|
59
|
+
|
|
60
|
+
Edit those files here, not in the source repo, then run the sync. Every code block in the
|
|
61
|
+
Recipes section of `testing.md` was run against a generated app. If you change a recipe,
|
|
62
|
+
check it the same way.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jefferson Abraham Omier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: create-caspian-app
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Create Caspian apps: a high-performance Python web framework (FastAPI + PulsePoint).
|
|
5
|
+
Project-URL: Homepage, https://caspian.tsnc.tech/
|
|
6
|
+
Project-URL: Repository, https://github.com/TheSteelNinjaCode/create-caspian-app
|
|
7
|
+
Author-email: Jefferson Abraham Omier <thesteelninjacode@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: boilerplate,caspian,create-app,fastapi,mcp,python,scaffold,tailwindcss
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Requires-Python: >=3.14
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# create-caspian-app
|
|
24
|
+
|
|
25
|
+
Create a new [Caspian](https://caspian.tsnc.tech/) app, a high-performance Python web
|
|
26
|
+
framework built on FastAPI and the PulsePoint browser runtime.
|
|
27
|
+
|
|
28
|
+
The installer and application tooling use Python.
|
|
29
|
+
|
|
30
|
+
Version **0.0.1** is the initial **alpha** release. APIs and generated project
|
|
31
|
+
structure may change as Caspian develops.
|
|
32
|
+
|
|
33
|
+
## Create an app
|
|
34
|
+
|
|
35
|
+
With [uv](https://docs.astral.sh/uv/) (recommended, nothing to install first):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uvx create-caspian-app my-app
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or install the command once:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uv tool install create-caspian-app # or: pipx install create-caspian-app
|
|
45
|
+
create-caspian-app my-app
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You'll be asked which features you want:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
? What is your project named? (my-app)
|
|
52
|
+
? Would you like to use Tailwind CSS? y/N
|
|
53
|
+
? Would you like to use MCP (Model Context Protocol)? y/N
|
|
54
|
+
? Would you like to use Prisma ORM (Caspian DB)? y/N
|
|
55
|
+
? Would you like to use WebSocket? y/N
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The installer writes the project, generates a `.env` with fresh secrets, runs
|
|
59
|
+
`uv sync --group dev`, and builds the CSS. When Tailwind is on, it also downloads the
|
|
60
|
+
pinned, checksum-verified Tailwind standalone compiler. Then:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cd my-app
|
|
64
|
+
uv run python main.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Skip the questions
|
|
68
|
+
|
|
69
|
+
Pass flags and `-y`. Only the flags you pass are turned on:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
uvx create-caspian-app my-app --tailwindcss --mcp --prisma --websocket -y
|
|
73
|
+
uvx create-caspian-app my-app --prisma --websocket -y
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Flag | Feature |
|
|
77
|
+
| ---------------- | -------------------------------------------------------------- |
|
|
78
|
+
| `--tailwindcss` | Tailwind CSS v4 via the standalone compiler |
|
|
79
|
+
| `--mcp` | MCP server mounted in FastAPI (`src/lib/mcp`) |
|
|
80
|
+
| `--prisma` | Caspian DB: `prisma/schema.prisma` + generated Python client |
|
|
81
|
+
| `--websocket` | Named sockets (`src/lib/websocket`) |
|
|
82
|
+
| `--skip-install` | Write files only; skip `uv sync` and the CSS build |
|
|
83
|
+
| `-y` | No prompts |
|
|
84
|
+
|
|
85
|
+
## Update an existing app
|
|
86
|
+
|
|
87
|
+
Run this from the project folder:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uvx --from create-caspian-app casp update project # latest
|
|
91
|
+
uvx --from create-caspian-app casp update project 0.0.1 # a specific version
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Running `create-caspian-app <name>` inside a folder that has a `caspian.config.json`
|
|
95
|
+
does the same. The update refreshes the Caspian template files and asks about any
|
|
96
|
+
features that aren't enabled yet. It removes the files of features you turn off and
|
|
97
|
+
re-pins the managed dependencies.
|
|
98
|
+
|
|
99
|
+
An update never overwrites:
|
|
100
|
+
|
|
101
|
+
- `.env`
|
|
102
|
+
- `pyproject.toml` (only the managed requirements are re-pinned, with `uv add` / `uv remove`)
|
|
103
|
+
- `uv.lock`
|
|
104
|
+
- `src/app/globals.css` (only the Tailwind directives are added or removed)
|
|
105
|
+
- `prisma/`
|
|
106
|
+
- any path listed in `caspian.config.json` → `"excludeFiles"`
|
|
107
|
+
|
|
108
|
+
Everything else that comes from the template, such as `src/app/index.py`, gets
|
|
109
|
+
overwritten. Add the files you've customized to `excludeFiles` first.
|
|
110
|
+
|
|
111
|
+
## Requirements
|
|
112
|
+
|
|
113
|
+
- Python 3.14+ for the installer and generated apps. uv can download Python 3.14
|
|
114
|
+
if it is missing.
|
|
115
|
+
- [uv](https://docs.astral.sh/uv/). If it's missing, the installer tries
|
|
116
|
+
`python -m pip install uv`.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# create-caspian-app
|
|
2
|
+
|
|
3
|
+
Create a new [Caspian](https://caspian.tsnc.tech/) app, a high-performance Python web
|
|
4
|
+
framework built on FastAPI and the PulsePoint browser runtime.
|
|
5
|
+
|
|
6
|
+
The installer and application tooling use Python.
|
|
7
|
+
|
|
8
|
+
Version **0.0.1** is the initial **alpha** release. APIs and generated project
|
|
9
|
+
structure may change as Caspian develops.
|
|
10
|
+
|
|
11
|
+
## Create an app
|
|
12
|
+
|
|
13
|
+
With [uv](https://docs.astral.sh/uv/) (recommended, nothing to install first):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx create-caspian-app my-app
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install the command once:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
uv tool install create-caspian-app # or: pipx install create-caspian-app
|
|
23
|
+
create-caspian-app my-app
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
You'll be asked which features you want:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
? What is your project named? (my-app)
|
|
30
|
+
? Would you like to use Tailwind CSS? y/N
|
|
31
|
+
? Would you like to use MCP (Model Context Protocol)? y/N
|
|
32
|
+
? Would you like to use Prisma ORM (Caspian DB)? y/N
|
|
33
|
+
? Would you like to use WebSocket? y/N
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The installer writes the project, generates a `.env` with fresh secrets, runs
|
|
37
|
+
`uv sync --group dev`, and builds the CSS. When Tailwind is on, it also downloads the
|
|
38
|
+
pinned, checksum-verified Tailwind standalone compiler. Then:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd my-app
|
|
42
|
+
uv run python main.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Skip the questions
|
|
46
|
+
|
|
47
|
+
Pass flags and `-y`. Only the flags you pass are turned on:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx create-caspian-app my-app --tailwindcss --mcp --prisma --websocket -y
|
|
51
|
+
uvx create-caspian-app my-app --prisma --websocket -y
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
| Flag | Feature |
|
|
55
|
+
| ---------------- | -------------------------------------------------------------- |
|
|
56
|
+
| `--tailwindcss` | Tailwind CSS v4 via the standalone compiler |
|
|
57
|
+
| `--mcp` | MCP server mounted in FastAPI (`src/lib/mcp`) |
|
|
58
|
+
| `--prisma` | Caspian DB: `prisma/schema.prisma` + generated Python client |
|
|
59
|
+
| `--websocket` | Named sockets (`src/lib/websocket`) |
|
|
60
|
+
| `--skip-install` | Write files only; skip `uv sync` and the CSS build |
|
|
61
|
+
| `-y` | No prompts |
|
|
62
|
+
|
|
63
|
+
## Update an existing app
|
|
64
|
+
|
|
65
|
+
Run this from the project folder:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uvx --from create-caspian-app casp update project # latest
|
|
69
|
+
uvx --from create-caspian-app casp update project 0.0.1 # a specific version
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Running `create-caspian-app <name>` inside a folder that has a `caspian.config.json`
|
|
73
|
+
does the same. The update refreshes the Caspian template files and asks about any
|
|
74
|
+
features that aren't enabled yet. It removes the files of features you turn off and
|
|
75
|
+
re-pins the managed dependencies.
|
|
76
|
+
|
|
77
|
+
An update never overwrites:
|
|
78
|
+
|
|
79
|
+
- `.env`
|
|
80
|
+
- `pyproject.toml` (only the managed requirements are re-pinned, with `uv add` / `uv remove`)
|
|
81
|
+
- `uv.lock`
|
|
82
|
+
- `src/app/globals.css` (only the Tailwind directives are added or removed)
|
|
83
|
+
- `prisma/`
|
|
84
|
+
- any path listed in `caspian.config.json` → `"excludeFiles"`
|
|
85
|
+
|
|
86
|
+
Everything else that comes from the template, such as `src/app/index.py`, gets
|
|
87
|
+
overwritten. Add the files you've customized to `excludeFiles` first.
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- Python 3.14+ for the installer and generated apps. uv can download Python 3.14
|
|
92
|
+
if it is missing.
|
|
93
|
+
- [uv](https://docs.astral.sh/uv/). If it's missing, the installer tries
|
|
94
|
+
`python -m pip install uv`.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "create-caspian-app"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Create Caspian apps: a high-performance Python web framework (FastAPI + PulsePoint)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.14"
|
|
13
|
+
authors = [{ name = "Jefferson Abraham Omier", email = "thesteelninjacode@gmail.com" }]
|
|
14
|
+
keywords = ["caspian", "python", "fastapi", "scaffold", "create-app", "boilerplate", "tailwindcss", "mcp"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Framework :: FastAPI",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
24
|
+
"Topic :: Software Development :: Code Generators",
|
|
25
|
+
]
|
|
26
|
+
# Deliberately empty: the installer uses only the standard library and uv.
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://caspian.tsnc.tech/"
|
|
31
|
+
Repository = "https://github.com/TheSteelNinjaCode/create-caspian-app"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
create-caspian-app = "create_caspian_app.cli:main"
|
|
35
|
+
casp = "create_caspian_app.casp:main"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.version]
|
|
41
|
+
path = "src/create_caspian_app/__init__.py"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/create_caspian_app"]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.sdist]
|
|
47
|
+
include = ["src/", "scripts/", "template_overrides/", "tests/", "README.md", "CHANGELOG.md", "CONTRIBUTING.md", "LICENSE"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
addopts = "-q"
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 100
|
|
55
|
+
extend-exclude = ["src/create_caspian_app/template"]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Copy the Caspian Python starter project into the package template.
|
|
2
|
+
|
|
3
|
+
The source of truth is the Caspian app repository (by default the sibling
|
|
4
|
+
folder ``../create-caspian-app-python``). Only files tracked by git in that
|
|
5
|
+
repository are copied, so its own ``.gitignore`` decides what is private
|
|
6
|
+
(``.env``, ``.tools/``, ``.casp/``, caches).
|
|
7
|
+
|
|
8
|
+
That repository is also the framework's development workspace. Its demo pages,
|
|
9
|
+
benchmarks and framework test suite are not part of a new
|
|
10
|
+
app, so they are left out here, and ``template_overrides/`` supplies what a new
|
|
11
|
+
app gets instead (the app test scaffold and the agent testing guide). Every
|
|
12
|
+
text edit below must match the source exactly; when the source changes so an
|
|
13
|
+
edit no longer applies, the sync fails instead of silently shipping
|
|
14
|
+
development content.
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
uv run python scripts/sync_template.py
|
|
18
|
+
uv run python scripts/sync_template.py --source C:/path/to/caspian-app
|
|
19
|
+
uv run python scripts/sync_template.py --check # fail if template is stale
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
30
|
+
TEMPLATE = ROOT / "src" / "create_caspian_app" / "template"
|
|
31
|
+
OVERRIDES = ROOT / "template_overrides"
|
|
32
|
+
DEFAULT_SOURCE = ROOT.parent / "create-caspian-app-python"
|
|
33
|
+
|
|
34
|
+
# Tracked in the source repo but never shipped to a new app.
|
|
35
|
+
EXCLUDED_PREFIXES = (
|
|
36
|
+
".venv/", # the installed runtime belongs to uv, not the template
|
|
37
|
+
".git/",
|
|
38
|
+
)
|
|
39
|
+
EXCLUDED_FILES = {
|
|
40
|
+
"caspian.config.json", # generated per project by the installer
|
|
41
|
+
".env", # generated per project (fresh secrets)
|
|
42
|
+
}
|
|
43
|
+
EXCLUDED_SUFFIXES = (".pyc", ".pyo")
|
|
44
|
+
|
|
45
|
+
# Development-only content of the source workspace.
|
|
46
|
+
DEVELOPMENT_ONLY = (
|
|
47
|
+
"docs/prepared-pages-example.md",
|
|
48
|
+
"settings/benchmark_pages.py",
|
|
49
|
+
"src/app/performance/",
|
|
50
|
+
"src/components/performance/",
|
|
51
|
+
"src/lib/performance_demo.py",
|
|
52
|
+
"src/app/form/",
|
|
53
|
+
"src/components/form/",
|
|
54
|
+
# The framework's own test suite. New apps get the scaffold from
|
|
55
|
+
# template_overrides/tests/ and the guide in docs/caspian/testing.md.
|
|
56
|
+
"tests/",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Exact text edits: (old, new) pairs applied to one file.
|
|
60
|
+
EDITS = {
|
|
61
|
+
"src/app/index.py": (
|
|
62
|
+
("from src.components.performance.ExampleLinks import ExampleLinks\n", ""),
|
|
63
|
+
(" <x-example-links />\n\n", ""),
|
|
64
|
+
),
|
|
65
|
+
"AGENTS.md": (
|
|
66
|
+
(
|
|
67
|
+
(
|
|
68
|
+
' Read `docs/caspian/websockets.md` "Named Sockets"; the handshake has no'
|
|
69
|
+
" dedicated tests yet (`tests/test_request_security.py` covers only the"
|
|
70
|
+
" per-connection message rate)."
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
' Read `docs/caspian/websockets.md` "Named Sockets"; test sockets with'
|
|
74
|
+
' `client.websocket_connect(...)` as shown in `docs/caspian/testing.md` "Sockets".'
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
(
|
|
78
|
+
(
|
|
79
|
+
" - Coverage lives in `tests/test_layout.py` (brace escaping), "
|
|
80
|
+
"`tests/test_runtime_security.py` (environment + CSP + path safety), "
|
|
81
|
+
"`tests/test_request_security.py` (page rate limit, missing-asset 404s, "
|
|
82
|
+
"per-connection socket message rate), `tests/test_python_server.py` "
|
|
83
|
+
"(development cookie scoping), `tests/test_page_policy.py` (prepared-page "
|
|
84
|
+
"policy selection), `tests/test_upload_serving.py`, `tests/test_rpc.py`, and "
|
|
85
|
+
"`tests/test_auth.py`. The `/mcp` token gate (`MCPAuthMiddleware`) and the "
|
|
86
|
+
"named-socket handshake (origin check, `require_auth`/`allowed_roles`, "
|
|
87
|
+
"size/idle limits) have no dedicated tests yet — verify them by reading "
|
|
88
|
+
"`main.py` and `src/lib/websocket/sockets.py`."
|
|
89
|
+
),
|
|
90
|
+
(
|
|
91
|
+
" - The runtime's own security coverage ships with Caspian. Test this app's"
|
|
92
|
+
" security-relevant behavior (route privacy, RBAC, `@rpc()` auth and roles, CSRF on"
|
|
93
|
+
" `route.py`, socket auth) with the `client`, `rpc`, `csrf_headers`, and `sign_in`"
|
|
94
|
+
" fixtures; recipes are in `docs/caspian/testing.md`."
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
(
|
|
98
|
+
(
|
|
99
|
+
"- Testing/type checking/linting: read `tests/README.md`, `settings/test.py`, and"
|
|
100
|
+
" `docs/caspian/testing.md`. Run `uv run python settings/test.py` (fix: `uv run"
|
|
101
|
+
" python settings/fix.py`). The four checks are native ty, Ruff, templates, and"
|
|
102
|
+
" pytest. A green gate does not prove browser health."
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
"- Testing: **every change to app behavior ships with a test in the same edit batch.**"
|
|
106
|
+
" When you add or change a page, layout, component, `@rpc()` action, `route.py`"
|
|
107
|
+
" endpoint, socket, job, auth/RBAC rule, validation rule, or `src/lib/**` function,"
|
|
108
|
+
" read `docs/caspian/testing.md` first: it maps each kind of change to the test it"
|
|
109
|
+
" needs, with working examples built on the fixtures in `tests/conftest.py`"
|
|
110
|
+
" (`client`, `rpc`, `csrf_headers`, `sign_in`, `config`). Put tests in"
|
|
111
|
+
" `tests/test_<feature>.py`, test routes over HTTP (never import"
|
|
112
|
+
" `src/app/**` modules), and never weaken a test to pass the gate. Then run"
|
|
113
|
+
" `uv run python settings/test.py` (ty, Ruff, templates, pytest; fix: `uv run"
|
|
114
|
+
" python settings/fix.py`) and fix every failure. A green gate does not prove"
|
|
115
|
+
" browser health."
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
),
|
|
119
|
+
".github/copilot-instructions.md": (
|
|
120
|
+
(
|
|
121
|
+
(
|
|
122
|
+
"- App Python edits require uv run python settings/test.py: ty, Ruff, templates,"
|
|
123
|
+
" pytest. Fix all errors."
|
|
124
|
+
),
|
|
125
|
+
(
|
|
126
|
+
"- App Python edits require uv run python settings/test.py: ty, Ruff, templates,"
|
|
127
|
+
" pytest. Fix all errors. Every new or changed page, component, @rpc() action,"
|
|
128
|
+
" route.py endpoint, socket, job, or auth rule ships with a test in"
|
|
129
|
+
" tests/test_<feature>.py; follow docs/caspian/testing.md."
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
(
|
|
133
|
+
"pytest exercises app and installed runtime contracts.",
|
|
134
|
+
(
|
|
135
|
+
"pytest runs the app's tests in tests/ (fixtures in tests/conftest.py, recipes in"
|
|
136
|
+
" docs/caspian/testing.md)."
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
".github/instructions/database-generation.instructions.md": (
|
|
141
|
+
(
|
|
142
|
+
(
|
|
143
|
+
"Testing: uv run python settings/test.py. Optional PostgreSQL verification:\n"
|
|
144
|
+
"set CASPIAN_DB_TEST_POSTGRES=1 and run uv run python -m pytest\n"
|
|
145
|
+
"tests/test_prisma_postgresql.py -q, then unset the variable. That test reads .env\n"
|
|
146
|
+
"and uses verified session-local temporary tables, never application tables."
|
|
147
|
+
),
|
|
148
|
+
(
|
|
149
|
+
"Testing: keep queries in src/lib/** functions that routes call through their\n"
|
|
150
|
+
"module, fake them in route tests, and test the queries themselves against a\n"
|
|
151
|
+
'dedicated test database; see docs/caspian/testing.md "Database". Then run\n'
|
|
152
|
+
"uv run python settings/test.py."
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Dotfiles are renamed inside the wheel so packaging tools never treat them as
|
|
159
|
+
# their own ignore rules. The installer renames them back.
|
|
160
|
+
RENAMED = {".gitignore": "_gitignore"}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def tracked_files(source: Path) -> list[str]:
|
|
164
|
+
try:
|
|
165
|
+
output = subprocess.run(
|
|
166
|
+
["git", "ls-files", "-z"],
|
|
167
|
+
cwd=source,
|
|
168
|
+
check=True,
|
|
169
|
+
capture_output=True,
|
|
170
|
+
).stdout
|
|
171
|
+
except (OSError, subprocess.CalledProcessError) as exc:
|
|
172
|
+
raise SystemExit(f"{source} must be a git repository: {exc}") from exc
|
|
173
|
+
return [name for name in output.decode("utf-8").split("\0") if name]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def shipped(name: str) -> bool:
|
|
177
|
+
if name in EXCLUDED_FILES or name.endswith(EXCLUDED_SUFFIXES):
|
|
178
|
+
return False
|
|
179
|
+
if "__pycache__/" in name:
|
|
180
|
+
return False
|
|
181
|
+
if any(name == p or (p.endswith("/") and name.startswith(p)) for p in DEVELOPMENT_ONLY):
|
|
182
|
+
return False
|
|
183
|
+
return not name.startswith(EXCLUDED_PREFIXES)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def apply_edits(name: str, text: str) -> str:
|
|
187
|
+
for old, new in EDITS.get(name, ()):
|
|
188
|
+
if old not in text:
|
|
189
|
+
raise SystemExit(f"{name}: expected text not found (source changed?): {old!r}")
|
|
190
|
+
text = text.replace(old, new)
|
|
191
|
+
return text
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def content(name: str, path: Path) -> bytes:
|
|
195
|
+
if name not in EDITS:
|
|
196
|
+
return path.read_bytes()
|
|
197
|
+
raw = path.read_bytes().decode("utf-8")
|
|
198
|
+
crlf = "\r\n" in raw
|
|
199
|
+
text = apply_edits(name, raw.replace("\r\n", "\n")) # edits are written with \n
|
|
200
|
+
return (text.replace("\n", "\r\n") if crlf else text).encode("utf-8")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def destination(name: str) -> str:
|
|
204
|
+
return RENAMED.get(name, name)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def plan(source: Path) -> dict[str, bytes]:
|
|
208
|
+
files = {}
|
|
209
|
+
for name in tracked_files(source):
|
|
210
|
+
path = source / name
|
|
211
|
+
if shipped(name) and path.is_file():
|
|
212
|
+
files[destination(name)] = content(name, path)
|
|
213
|
+
# Files owned by this package: added, or replacing the source's version.
|
|
214
|
+
for path in sorted(OVERRIDES.rglob("*")):
|
|
215
|
+
if path.is_file() and "__pycache__" not in path.parts:
|
|
216
|
+
files[destination(path.relative_to(OVERRIDES).as_posix())] = path.read_bytes()
|
|
217
|
+
return files
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main() -> int:
|
|
221
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
222
|
+
parser.add_argument("--source", type=Path, default=DEFAULT_SOURCE)
|
|
223
|
+
parser.add_argument("--check", action="store_true", help="report drift, write nothing")
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
source = args.source.resolve()
|
|
227
|
+
if not (source / "main.py").is_file():
|
|
228
|
+
raise SystemExit(f"Not a Caspian app (no main.py): {source}")
|
|
229
|
+
|
|
230
|
+
files = plan(source)
|
|
231
|
+
existing = (
|
|
232
|
+
{p.relative_to(TEMPLATE).as_posix() for p in TEMPLATE.rglob("*") if p.is_file()}
|
|
233
|
+
if TEMPLATE.exists()
|
|
234
|
+
else set()
|
|
235
|
+
)
|
|
236
|
+
stale = sorted(existing - files.keys())
|
|
237
|
+
changed = sorted(
|
|
238
|
+
name
|
|
239
|
+
for name, data in files.items()
|
|
240
|
+
if not (TEMPLATE / name).is_file() or (TEMPLATE / name).read_bytes() != data
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if args.check:
|
|
244
|
+
for name in changed:
|
|
245
|
+
print(f"changed: {name}")
|
|
246
|
+
for name in stale:
|
|
247
|
+
print(f"removed: {name}")
|
|
248
|
+
if changed or stale:
|
|
249
|
+
print("Template is out of date. Run: uv run python scripts/sync_template.py")
|
|
250
|
+
return 1
|
|
251
|
+
print(f"Template is current ({len(files)} files from {source}).")
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
for name in stale:
|
|
255
|
+
(TEMPLATE / name).unlink()
|
|
256
|
+
for name in changed:
|
|
257
|
+
target = TEMPLATE / name
|
|
258
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
259
|
+
target.write_bytes(files[name])
|
|
260
|
+
# Drop directories emptied by removals.
|
|
261
|
+
for directory in sorted(TEMPLATE.rglob("*"), reverse=True):
|
|
262
|
+
if directory.is_dir() and not any(directory.iterdir()):
|
|
263
|
+
directory.rmdir()
|
|
264
|
+
|
|
265
|
+
print(
|
|
266
|
+
f"Synced {len(files)} files from {source} ({len(changed)} updated, {len(stale)} removed)."
|
|
267
|
+
)
|
|
268
|
+
return 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
sys.exit(main())
|