Deleted everything webui
|
@ -10,15 +10,6 @@
|
||||||
treefmt.flakeCheck = true;
|
treefmt.flakeCheck = true;
|
||||||
treefmt.flakeFormatter = true;
|
treefmt.flakeFormatter = true;
|
||||||
treefmt.programs.shellcheck.enable = true;
|
treefmt.programs.shellcheck.enable = true;
|
||||||
treefmt.programs.prettier.enable = true;
|
|
||||||
# TODO: add custom prettier package, that uses our ui/node_modules
|
|
||||||
# treefmt.programs.prettier.settings.plugins = [
|
|
||||||
# "${self'.packages.prettier-plugin-tailwindcss}/lib/node_modules/prettier-plugin-tailwindcss/dist/index.mjs"
|
|
||||||
# ];
|
|
||||||
treefmt.settings.formatter.prettier.excludes = [
|
|
||||||
"secrets.yaml"
|
|
||||||
"key.json"
|
|
||||||
];
|
|
||||||
|
|
||||||
treefmt.programs.mypy.enable = true;
|
treefmt.programs.mypy.enable = true;
|
||||||
treefmt.programs.mypy.directories = {
|
treefmt.programs.mypy.directories = {
|
||||||
|
@ -39,7 +30,6 @@
|
||||||
"--" # this argument is ignored by bash
|
"--" # this argument is ignored by bash
|
||||||
];
|
];
|
||||||
includes = [ "*.nix" ];
|
includes = [ "*.nix" ];
|
||||||
excludes = [ "pkgs/node-packages/*.nix" ];
|
|
||||||
};
|
};
|
||||||
treefmt.settings.formatter.python = {
|
treefmt.settings.formatter.python = {
|
||||||
command = "sh";
|
command = "sh";
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# clan-cli
|
# clan-cli
|
||||||
|
|
||||||
The clan-cli contains the command line interface as well as the graphical webui through the `clan webui` command.
|
The clan-cli contains the command line interface
|
||||||
|
|
||||||
## Hacking on the cli
|
## Hacking on the cli
|
||||||
|
|
||||||
|
@ -17,43 +17,6 @@ After you can use the local bin wrapper to test things in the cli:
|
||||||
./bin/clan
|
./bin/clan
|
||||||
```
|
```
|
||||||
|
|
||||||
## Hacking on the webui
|
|
||||||
|
|
||||||
By default the webui is build from a tarball available https://git.clan.lol/clan/-/packages/generic/ui/.
|
|
||||||
To start a local developement environment instead, use the `--dev` flag:
|
|
||||||
|
|
||||||
```
|
|
||||||
./bin/clan webui --dev
|
|
||||||
```
|
|
||||||
|
|
||||||
This will spawn two webserver, a python one to for the api and a nodejs one that rebuilds the ui on the fly.
|
|
||||||
|
|
||||||
## Run webui directly
|
|
||||||
|
|
||||||
Useful for vscode run and debug option
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python -m clan_cli.webui --reload --no-open
|
|
||||||
```
|
|
||||||
|
|
||||||
Add this `launch.json` to your .vscode directory to have working breakpoints in your vscode editor.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Clan Webui",
|
|
||||||
"type": "python",
|
|
||||||
"request": "launch",
|
|
||||||
"module": "clan_cli.webui",
|
|
||||||
"justMyCode": true,
|
|
||||||
"args": ["--reload", "--no-open", "--log-level", "debug"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run locally single-threaded for debugging
|
## Run locally single-threaded for debugging
|
||||||
|
|
||||||
By default tests run in parallel using pytest-parallel.
|
By default tests run in parallel using pytest-parallel.
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
import argparse
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from uvicorn.importer import import_from_string
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
parser = argparse.ArgumentParser(prog="gen-openapi")
|
|
||||||
parser.add_argument(
|
|
||||||
"app", help='App import string. Eg. "main:app"', default="main:app"
|
|
||||||
)
|
|
||||||
parser.add_argument("--app-dir", help="Directory containing the app", default=None)
|
|
||||||
parser.add_argument(
|
|
||||||
"--out", help="Output file ending in .json", default="openapi.json"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.app_dir is not None:
|
|
||||||
print(f"adding {args.app_dir} to sys.path")
|
|
||||||
sys.path.insert(0, args.app_dir)
|
|
||||||
|
|
||||||
print(f"importing app from {args.app}")
|
|
||||||
app = import_from_string(args.app)
|
|
||||||
openapi = app.openapi()
|
|
||||||
version = openapi.get("openapi", "unknown version")
|
|
||||||
|
|
||||||
print(f"writing openapi spec v{version}")
|
|
||||||
out = Path(args.out)
|
|
||||||
out.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
out.write_text(json.dumps(openapi, indent=2))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from . import backups, config, flakes, history, machines, secrets, vms, webui
|
from . import backups, config, flakes, history, machines, secrets, vms
|
||||||
from .custom_logger import setup_logging
|
from .custom_logger import setup_logging
|
||||||
from .dirs import get_clan_flake_toplevel, is_clan_flake
|
from .dirs import get_clan_flake_toplevel, is_clan_flake
|
||||||
from .ssh import cli as ssh_cli
|
from .ssh import cli as ssh_cli
|
||||||
|
@ -105,9 +105,6 @@ def create_parser(prog: str | None = None) -> argparse.ArgumentParser:
|
||||||
)
|
)
|
||||||
machines.register_parser(parser_machine)
|
machines.register_parser(parser_machine)
|
||||||
|
|
||||||
parser_webui = subparsers.add_parser("webui", help="start webui")
|
|
||||||
webui.register_parser(parser_webui)
|
|
||||||
|
|
||||||
parser_vms = subparsers.add_parser("vms", help="manage virtual machines")
|
parser_vms = subparsers.add_parser("vms", help="manage virtual machines")
|
||||||
vms.register_parser(parser_vms)
|
vms.register_parser(parser_vms)
|
||||||
|
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
import argparse
|
|
||||||
from collections.abc import Callable
|
|
||||||
from typing import NoReturn
|
|
||||||
|
|
||||||
start_server: Callable | None = None
|
|
||||||
ServerImportError: ImportError | None = None
|
|
||||||
try:
|
|
||||||
from .server import start_server
|
|
||||||
except ImportError as e:
|
|
||||||
ServerImportError = e
|
|
||||||
|
|
||||||
|
|
||||||
def fastapi_is_not_installed(_: argparse.Namespace) -> NoReturn:
|
|
||||||
assert ServerImportError is not None
|
|
||||||
print(
|
|
||||||
f"Dependencies for the webserver is not installed. The webui command has been disabled ({ServerImportError})"
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def register_parser(parser: argparse.ArgumentParser) -> None:
|
|
||||||
parser.add_argument("--port", type=int, default=2979, help="Port to listen on")
|
|
||||||
parser.add_argument(
|
|
||||||
"--host", type=str, default="localhost", help="Host to listen on"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--no-open", action="store_true", help="Don't open the browser", default=False
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dev", action="store_true", help="Run in development mode", default=False
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dev-port",
|
|
||||||
type=int,
|
|
||||||
default=3000,
|
|
||||||
help="Port to listen on for the dev server",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dev-host", type=str, default="localhost", help="Host to listen on"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--reload", action="store_true", help="Don't reload on changes", default=False
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--log-level",
|
|
||||||
type=str,
|
|
||||||
default="info",
|
|
||||||
help="Log level",
|
|
||||||
choices=["critical", "error", "warning", "info", "debug", "trace"],
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"sub_url",
|
|
||||||
type=str,
|
|
||||||
default="/",
|
|
||||||
nargs="?",
|
|
||||||
help="Sub url to open in the browser",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Set the args.func variable in args
|
|
||||||
if start_server is None:
|
|
||||||
parser.set_defaults(func=fastapi_is_not_installed)
|
|
||||||
else:
|
|
||||||
parser.set_defaults(func=start_server)
|
|
|
@ -1,15 +0,0 @@
|
||||||
import argparse
|
|
||||||
|
|
||||||
from . import register_parser
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# this is use in our integration test
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
# call the register_parser function, which adds arguments to the parser
|
|
||||||
register_parser(parser)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
# call the function that is stored
|
|
||||||
# in the func attribute of args, and pass args as the argument
|
|
||||||
# look into register_parser to see how this is done
|
|
||||||
args.func(args)
|
|
|
@ -1,10 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class MissingClanImports(BaseModel):
|
|
||||||
missing_clan_imports: list[str] = []
|
|
||||||
msg: str = "Some requested clan modules could not be found"
|
|
|
@ -1,20 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from pydantic import AnyUrl, BaseModel, Extra, parse_obj_as
|
|
||||||
|
|
||||||
from ..flakes.create import DEFAULT_URL
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class FlakeCreateInput(BaseModel):
|
|
||||||
url: AnyUrl = parse_obj_as(AnyUrl, DEFAULT_URL)
|
|
||||||
|
|
||||||
|
|
||||||
class MachineConfig(BaseModel):
|
|
||||||
clanImports: list[str] = [] # noqa: N815
|
|
||||||
clan: dict = {}
|
|
||||||
|
|
||||||
# allow extra fields to cover the full spectrum of a nixos config
|
|
||||||
class Config:
|
|
||||||
extra = Extra.allow
|
|
|
@ -1,68 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from pydantic import BaseModel, Extra, Field
|
|
||||||
|
|
||||||
from ..async_cmd import CmdOut
|
|
||||||
|
|
||||||
|
|
||||||
class Status(Enum):
|
|
||||||
ONLINE = "online"
|
|
||||||
OFFLINE = "offline"
|
|
||||||
UNKNOWN = "unknown"
|
|
||||||
|
|
||||||
|
|
||||||
class ClanModulesResponse(BaseModel):
|
|
||||||
clan_modules: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
class Machine(BaseModel):
|
|
||||||
name: str
|
|
||||||
status: Status
|
|
||||||
|
|
||||||
|
|
||||||
class MachinesResponse(BaseModel):
|
|
||||||
machines: list[Machine]
|
|
||||||
|
|
||||||
|
|
||||||
class MachineResponse(BaseModel):
|
|
||||||
machine: Machine
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigResponse(BaseModel):
|
|
||||||
clanImports: list[str] = [] # noqa: N815
|
|
||||||
clan: dict = {}
|
|
||||||
|
|
||||||
# allow extra fields to cover the full spectrum of a nixos config
|
|
||||||
class Config:
|
|
||||||
extra = Extra.allow
|
|
||||||
|
|
||||||
|
|
||||||
class SchemaResponse(BaseModel):
|
|
||||||
schema_: dict = Field(alias="schema")
|
|
||||||
|
|
||||||
|
|
||||||
class VerifyMachineResponse(BaseModel):
|
|
||||||
success: bool
|
|
||||||
error: str | None
|
|
||||||
|
|
||||||
|
|
||||||
class FlakeAttrResponse(BaseModel):
|
|
||||||
flake_attrs: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
class FlakeAction(BaseModel):
|
|
||||||
id: str
|
|
||||||
uri: str
|
|
||||||
|
|
||||||
|
|
||||||
class FlakeListResponse(BaseModel):
|
|
||||||
flakes: list[str]
|
|
||||||
|
|
||||||
|
|
||||||
class FlakeCreateResponse(BaseModel):
|
|
||||||
cmd_out: dict[str, CmdOut]
|
|
||||||
|
|
||||||
|
|
||||||
class FlakeResponse(BaseModel):
|
|
||||||
content: str
|
|
||||||
actions: list[FlakeAction]
|
|
|
@ -1,56 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import FastAPI
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.routing import APIRoute
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
|
|
||||||
from .assets import asset_path
|
|
||||||
from .error_handlers import clan_error_handler
|
|
||||||
from .routers import clan_modules, flake, health, machines, root
|
|
||||||
from .settings import settings
|
|
||||||
from .tags import tags_metadata
|
|
||||||
|
|
||||||
# Logging setup
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_app() -> FastAPI:
|
|
||||||
app = FastAPI()
|
|
||||||
|
|
||||||
if settings.env.is_development():
|
|
||||||
# Allow CORS in development mode for nextjs dev server
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=["*"],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
app.include_router(clan_modules.router)
|
|
||||||
app.include_router(flake.router)
|
|
||||||
app.include_router(health.router)
|
|
||||||
app.include_router(machines.router)
|
|
||||||
|
|
||||||
# Needs to be last in register. Because of wildcard route
|
|
||||||
app.include_router(root.router)
|
|
||||||
|
|
||||||
app.add_exception_handler(Exception, clan_error_handler)
|
|
||||||
|
|
||||||
app.mount("/static", StaticFiles(directory=asset_path()), name="static")
|
|
||||||
|
|
||||||
# Add tag descriptions to the OpenAPI schema
|
|
||||||
app.openapi_tags = tags_metadata
|
|
||||||
|
|
||||||
for route in app.routes:
|
|
||||||
if isinstance(route, APIRoute):
|
|
||||||
route.operation_id = route.name # in this case, 'read_items'
|
|
||||||
log.debug(f"Registered route: {route}")
|
|
||||||
|
|
||||||
for i in app.exception_handlers.items():
|
|
||||||
log.debug(f"Registered exception handler: {i}")
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
app = setup_app()
|
|
|
@ -1,39 +0,0 @@
|
||||||
import functools
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_hash(string: str) -> str:
|
|
||||||
"""
|
|
||||||
This function takes a string like '/nix/store/kkvk20b8zh8aafdnfjp6dnf062x19732-source'
|
|
||||||
and returns the hash part 'kkvk20b8zh8aafdnfjp6dnf062x19732' after '/nix/store/' and before '-source'.
|
|
||||||
"""
|
|
||||||
# Split the string by '/' and get the last element
|
|
||||||
last_element = string.split("/")[-1]
|
|
||||||
# Split the last element by '-' and get the first element
|
|
||||||
hash_part = last_element.split("-")[0]
|
|
||||||
# Return the hash part
|
|
||||||
return hash_part
|
|
||||||
|
|
||||||
|
|
||||||
def check_divergence(path: Path) -> None:
|
|
||||||
p = path.resolve()
|
|
||||||
|
|
||||||
log.info("Absolute web asset path: %s", p)
|
|
||||||
if not p.is_dir():
|
|
||||||
raise FileNotFoundError(p)
|
|
||||||
|
|
||||||
# Get the hash part of the path
|
|
||||||
gh = get_hash(str(p))
|
|
||||||
|
|
||||||
log.debug(f"Serving webui asset with hash {gh}")
|
|
||||||
|
|
||||||
|
|
||||||
@functools.cache
|
|
||||||
def asset_path() -> Path:
|
|
||||||
path = Path(__file__).parent / "assets"
|
|
||||||
log.debug("Serving assets from: %s", path)
|
|
||||||
check_divergence(path)
|
|
||||||
return path
|
|
|
@ -1,54 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
from fastapi import Request, status
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from fastapi.responses import JSONResponse
|
|
||||||
|
|
||||||
from ..errors import ClanError, ClanHttpError
|
|
||||||
from .settings import settings
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def clan_error_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
||||||
headers = {}
|
|
||||||
|
|
||||||
if settings.env.is_development():
|
|
||||||
headers["Access-Control-Allow-Origin"] = "*"
|
|
||||||
headers["Access-Control-Allow-Methods"] = "*"
|
|
||||||
|
|
||||||
if isinstance(exc, ClanHttpError):
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=exc.status_code,
|
|
||||||
content=jsonable_encoder(dict(detail={"msg": exc.msg})),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
elif isinstance(exc, ClanError):
|
|
||||||
log.error(f"ClanError: {exc}")
|
|
||||||
detail = [
|
|
||||||
{
|
|
||||||
"loc": [],
|
|
||||||
"msg": str(exc),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
||||||
content=jsonable_encoder(dict(detail=detail)),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.exception(f"Unhandled Exception: {exc}")
|
|
||||||
return JSONResponse(
|
|
||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
||||||
content=jsonable_encoder(
|
|
||||||
dict(
|
|
||||||
detail=[
|
|
||||||
{
|
|
||||||
"loc": [],
|
|
||||||
"msg": str(exc),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
),
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
|
@ -1,952 +0,0 @@
|
||||||
{
|
|
||||||
"openapi": "3.1.0",
|
|
||||||
"info": {
|
|
||||||
"title": "FastAPI",
|
|
||||||
"version": "0.1.0"
|
|
||||||
},
|
|
||||||
"paths": {
|
|
||||||
"/api/clan_modules": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["modules"],
|
|
||||||
"summary": "List Clan Modules",
|
|
||||||
"operationId": "list_clan_modules",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ClanModulesResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/flake/history": {
|
|
||||||
"post": {
|
|
||||||
"tags": ["flake"],
|
|
||||||
"summary": "Flake History Append",
|
|
||||||
"operationId": "flake_history_append",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"get": {
|
|
||||||
"tags": ["flake"],
|
|
||||||
"summary": "Flake History List",
|
|
||||||
"operationId": "flake_history_list",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"title": "Response Flake History List Api Flake History Get",
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/flake/attrs": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["flake"],
|
|
||||||
"summary": "Inspect Flake Attrs",
|
|
||||||
"operationId": "inspect_flake_attrs",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "url",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Url",
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"maxLength": 65536,
|
|
||||||
"format": "uri"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FlakeAttrResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/flake/inspect": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["flake"],
|
|
||||||
"summary": "Inspect Flake",
|
|
||||||
"operationId": "inspect_flake",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "url",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Url",
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"minLength": 1,
|
|
||||||
"maxLength": 65536,
|
|
||||||
"format": "uri"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FlakeResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/flake/create": {
|
|
||||||
"post": {
|
|
||||||
"tags": ["flake"],
|
|
||||||
"summary": "Create Flake",
|
|
||||||
"operationId": "create_flake",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FlakeCreateInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FlakeCreateResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/machines": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "List Machines",
|
|
||||||
"operationId": "list_machines",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/MachinesResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/machines/{name}": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "Get Machine",
|
|
||||||
"operationId": "get_machine",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Name",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/MachineResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/machines/{name}/config": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "Get Machine Config",
|
|
||||||
"operationId": "get_machine_config",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Name",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ConfigResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "Set Machine Config",
|
|
||||||
"operationId": "set_machine_config",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Name",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/MachineConfig"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/schema": {
|
|
||||||
"put": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "Get Machine Schema",
|
|
||||||
"operationId": "get_machine_schema",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"title": "Config",
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SchemaResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/MissingClanImports"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Bad Request"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/machines/{name}/verify": {
|
|
||||||
"get": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "Get Verify Machine Config",
|
|
||||||
"operationId": "get_verify_machine_config",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Name",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/VerifyMachineResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"put": {
|
|
||||||
"tags": ["machine"],
|
|
||||||
"summary": "Put Verify Machine Config",
|
|
||||||
"operationId": "put_verify_machine_config",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"in": "path",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Name",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "flake_dir",
|
|
||||||
"in": "query",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"title": "Flake Dir",
|
|
||||||
"type": "string",
|
|
||||||
"format": "path"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"title": "Config",
|
|
||||||
"type": "object"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Successful Response",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/VerifyMachineResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Validation Error",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/HTTPValidationError"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"components": {
|
|
||||||
"schemas": {
|
|
||||||
"ClanModulesResponse": {
|
|
||||||
"properties": {
|
|
||||||
"clan_modules": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Clan Modules"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["clan_modules"],
|
|
||||||
"title": "ClanModulesResponse"
|
|
||||||
},
|
|
||||||
"CmdOut": {
|
|
||||||
"properties": {
|
|
||||||
"stdout": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Stdout"
|
|
||||||
},
|
|
||||||
"stderr": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Stderr"
|
|
||||||
},
|
|
||||||
"cwd": {
|
|
||||||
"type": "string",
|
|
||||||
"format": "path",
|
|
||||||
"title": "Cwd"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["stdout", "stderr", "cwd"],
|
|
||||||
"title": "CmdOut"
|
|
||||||
},
|
|
||||||
"ConfigResponse": {
|
|
||||||
"properties": {
|
|
||||||
"clanImports": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Clanimports",
|
|
||||||
"default": []
|
|
||||||
},
|
|
||||||
"clan": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "Clan",
|
|
||||||
"default": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "ConfigResponse"
|
|
||||||
},
|
|
||||||
"FlakeAction": {
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Id"
|
|
||||||
},
|
|
||||||
"uri": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Uri"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["id", "uri"],
|
|
||||||
"title": "FlakeAction"
|
|
||||||
},
|
|
||||||
"FlakeAttrResponse": {
|
|
||||||
"properties": {
|
|
||||||
"flake_attrs": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Flake Attrs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["flake_attrs"],
|
|
||||||
"title": "FlakeAttrResponse"
|
|
||||||
},
|
|
||||||
"FlakeCreateInput": {
|
|
||||||
"properties": {
|
|
||||||
"url": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 65536,
|
|
||||||
"minLength": 1,
|
|
||||||
"format": "uri",
|
|
||||||
"title": "Url",
|
|
||||||
"default": "git+https://git.clan.lol/clan/clan-core?new-clan"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "FlakeCreateInput"
|
|
||||||
},
|
|
||||||
"FlakeCreateResponse": {
|
|
||||||
"properties": {
|
|
||||||
"cmd_out": {
|
|
||||||
"additionalProperties": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"title": "Stdout"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"title": "Stderr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string",
|
|
||||||
"format": "path",
|
|
||||||
"title": "Cwd"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "array",
|
|
||||||
"maxItems": 3,
|
|
||||||
"minItems": 3
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "Cmd Out"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["cmd_out"],
|
|
||||||
"title": "FlakeCreateResponse"
|
|
||||||
},
|
|
||||||
"FlakeResponse": {
|
|
||||||
"properties": {
|
|
||||||
"content": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Content"
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/FlakeAction"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Actions"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["content", "actions"],
|
|
||||||
"title": "FlakeResponse"
|
|
||||||
},
|
|
||||||
"HTTPValidationError": {
|
|
||||||
"properties": {
|
|
||||||
"detail": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/ValidationError"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Detail"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "HTTPValidationError"
|
|
||||||
},
|
|
||||||
"Machine": {
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Name"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"$ref": "#/components/schemas/Status"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["name", "status"],
|
|
||||||
"title": "Machine"
|
|
||||||
},
|
|
||||||
"MachineConfig": {
|
|
||||||
"properties": {
|
|
||||||
"clanImports": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Clanimports",
|
|
||||||
"default": []
|
|
||||||
},
|
|
||||||
"clan": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "Clan",
|
|
||||||
"default": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "MachineConfig"
|
|
||||||
},
|
|
||||||
"MachineResponse": {
|
|
||||||
"properties": {
|
|
||||||
"machine": {
|
|
||||||
"$ref": "#/components/schemas/Machine"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["machine"],
|
|
||||||
"title": "MachineResponse"
|
|
||||||
},
|
|
||||||
"MachinesResponse": {
|
|
||||||
"properties": {
|
|
||||||
"machines": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/Machine"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Machines"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["machines"],
|
|
||||||
"title": "MachinesResponse"
|
|
||||||
},
|
|
||||||
"MissingClanImports": {
|
|
||||||
"properties": {
|
|
||||||
"missing_clan_imports": {
|
|
||||||
"items": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Missing Clan Imports",
|
|
||||||
"default": []
|
|
||||||
},
|
|
||||||
"msg": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Msg",
|
|
||||||
"default": "Some requested clan modules could not be found"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"title": "MissingClanImports"
|
|
||||||
},
|
|
||||||
"SchemaResponse": {
|
|
||||||
"properties": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"title": "Schema"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["schema"],
|
|
||||||
"title": "SchemaResponse"
|
|
||||||
},
|
|
||||||
"Status": {
|
|
||||||
"enum": ["online", "offline", "unknown"],
|
|
||||||
"title": "Status",
|
|
||||||
"description": "An enumeration."
|
|
||||||
},
|
|
||||||
"ValidationError": {
|
|
||||||
"properties": {
|
|
||||||
"loc": {
|
|
||||||
"items": {
|
|
||||||
"anyOf": [
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"type": "array",
|
|
||||||
"title": "Location"
|
|
||||||
},
|
|
||||||
"msg": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Message"
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Error Type"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["loc", "msg", "type"],
|
|
||||||
"title": "ValidationError"
|
|
||||||
},
|
|
||||||
"VerifyMachineResponse": {
|
|
||||||
"properties": {
|
|
||||||
"success": {
|
|
||||||
"type": "boolean",
|
|
||||||
"title": "Success"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"type": "string",
|
|
||||||
"title": "Error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"type": "object",
|
|
||||||
"required": ["success"],
|
|
||||||
"title": "VerifyMachineResponse"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [
|
|
||||||
{
|
|
||||||
"name": "flake",
|
|
||||||
"description": "Operations on a flake.",
|
|
||||||
"externalDocs": {
|
|
||||||
"description": "What is a flake?",
|
|
||||||
"url": "https://www.tweag.io/blog/2020-05-25-flakes/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "machine",
|
|
||||||
"description": "Manage physical machines. Instances of a flake"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "vm",
|
|
||||||
"description": "Manage virtual machines. Instances of a flake"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "modules",
|
|
||||||
"description": "Manage cLAN modules of a flake"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
# Logging setup
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
|
|
||||||
from clan_cli.clan_modules import get_clan_module_names
|
|
||||||
|
|
||||||
from ..api_outputs import (
|
|
||||||
ClanModulesResponse,
|
|
||||||
)
|
|
||||||
from ..tags import Tags
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/clan_modules", tags=[Tags.modules])
|
|
||||||
async def list_clan_modules(flake_dir: Path) -> ClanModulesResponse:
|
|
||||||
module_names, error = get_clan_module_names(flake_dir)
|
|
||||||
if error is not None:
|
|
||||||
raise HTTPException(status_code=400, detail=error)
|
|
||||||
return ClanModulesResponse(clan_modules=module_names)
|
|
|
@ -1,93 +0,0 @@
|
||||||
import json
|
|
||||||
from json.decoder import JSONDecodeError
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Body, HTTPException, status
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
|
|
||||||
from clan_cli.webui.api_inputs import (
|
|
||||||
FlakeCreateInput,
|
|
||||||
)
|
|
||||||
from clan_cli.webui.api_outputs import (
|
|
||||||
FlakeAction,
|
|
||||||
FlakeAttrResponse,
|
|
||||||
FlakeCreateResponse,
|
|
||||||
FlakeResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ...async_cmd import run
|
|
||||||
from ...flakes import create
|
|
||||||
from ...nix import nix_command, nix_flake_show
|
|
||||||
from ..tags import Tags
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Check for directory traversal
|
|
||||||
async def get_attrs(url: AnyUrl | Path) -> list[str]:
|
|
||||||
cmd = nix_flake_show(url)
|
|
||||||
out = await run(cmd)
|
|
||||||
|
|
||||||
data: dict[str, dict] = {}
|
|
||||||
try:
|
|
||||||
data = json.loads(out.stdout)
|
|
||||||
except JSONDecodeError:
|
|
||||||
raise HTTPException(status_code=422, detail="Could not load flake.")
|
|
||||||
|
|
||||||
nixos_configs = data.get("nixosConfigurations", {})
|
|
||||||
flake_attrs = list(nixos_configs.keys())
|
|
||||||
|
|
||||||
if not flake_attrs:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=422, detail="No entry or no attribute: nixosConfigurations"
|
|
||||||
)
|
|
||||||
return flake_attrs
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Check for directory traversal
|
|
||||||
@router.get("/api/flake/attrs", tags=[Tags.flake])
|
|
||||||
async def inspect_flake_attrs(url: AnyUrl | Path) -> FlakeAttrResponse:
|
|
||||||
return FlakeAttrResponse(flake_attrs=await get_attrs(url))
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Check for directory traversal
|
|
||||||
@router.get("/api/flake/inspect", tags=[Tags.flake])
|
|
||||||
async def inspect_flake(
|
|
||||||
url: AnyUrl | Path,
|
|
||||||
) -> FlakeResponse:
|
|
||||||
actions = []
|
|
||||||
# Extract the flake from the given URL
|
|
||||||
# We do this by running 'nix flake prefetch {url} --json'
|
|
||||||
cmd = nix_command(["flake", "prefetch", str(url), "--json", "--refresh"])
|
|
||||||
out = await run(cmd)
|
|
||||||
data: dict[str, str] = json.loads(out.stdout)
|
|
||||||
|
|
||||||
if data.get("storePath") is None:
|
|
||||||
raise HTTPException(status_code=500, detail="Could not load flake")
|
|
||||||
|
|
||||||
content: str
|
|
||||||
with open(Path(data.get("storePath", "")) / Path("flake.nix")) as f:
|
|
||||||
content = f.read()
|
|
||||||
|
|
||||||
# TODO: Figure out some measure when it is insecure to inspect or create a VM
|
|
||||||
actions.append(FlakeAction(id="vms/inspect", uri="api/vms/inspect"))
|
|
||||||
actions.append(FlakeAction(id="vms/create", uri="api/vms/create"))
|
|
||||||
|
|
||||||
return FlakeResponse(content=content, actions=actions)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/api/flake/create", tags=[Tags.flake], status_code=status.HTTP_201_CREATED
|
|
||||||
)
|
|
||||||
async def create_flake(
|
|
||||||
flake_dir: Path, args: Annotated[FlakeCreateInput, Body()]
|
|
||||||
) -> FlakeCreateResponse:
|
|
||||||
if flake_dir.exists():
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT,
|
|
||||||
detail="Flake already exists",
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd_out = await create.create_flake(flake_dir, args.url)
|
|
||||||
return FlakeCreateResponse(cmd_out=cmd_out)
|
|
|
@ -1,8 +0,0 @@
|
||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health", include_in_schema=False)
|
|
||||||
async def health() -> str:
|
|
||||||
return "OK"
|
|
|
@ -1,92 +0,0 @@
|
||||||
# Logging setup
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Body
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
|
|
||||||
from clan_cli.webui.api_errors import MissingClanImports
|
|
||||||
from clan_cli.webui.api_inputs import MachineConfig
|
|
||||||
|
|
||||||
from ...config.machine import (
|
|
||||||
config_for_machine,
|
|
||||||
set_config_for_machine,
|
|
||||||
verify_machine_config,
|
|
||||||
)
|
|
||||||
from ...config.schema import machine_schema
|
|
||||||
from ...machines.list import list_machines as _list_machines
|
|
||||||
from ..api_outputs import (
|
|
||||||
ConfigResponse,
|
|
||||||
Machine,
|
|
||||||
MachineResponse,
|
|
||||||
MachinesResponse,
|
|
||||||
SchemaResponse,
|
|
||||||
Status,
|
|
||||||
VerifyMachineResponse,
|
|
||||||
)
|
|
||||||
from ..tags import Tags
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/machines", tags=[Tags.machine])
|
|
||||||
async def list_machines(flake_dir: Path) -> MachinesResponse:
|
|
||||||
machines = []
|
|
||||||
for m in _list_machines(flake_dir):
|
|
||||||
machines.append(Machine(name=m, status=Status.UNKNOWN))
|
|
||||||
|
|
||||||
return MachinesResponse(machines=machines)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/machines/{name}", tags=[Tags.machine])
|
|
||||||
async def get_machine(flake_dir: Path, name: str) -> MachineResponse:
|
|
||||||
log.error("TODO")
|
|
||||||
return MachineResponse(machine=Machine(name=name, status=Status.UNKNOWN))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/machines/{name}/config", tags=[Tags.machine])
|
|
||||||
async def get_machine_config(flake_dir: Path, name: str) -> ConfigResponse:
|
|
||||||
config = config_for_machine(flake_dir, name)
|
|
||||||
return ConfigResponse(**config)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/api/machines/{name}/config", tags=[Tags.machine])
|
|
||||||
async def set_machine_config(
|
|
||||||
flake_dir: Path, name: str, config: Annotated[MachineConfig, Body()]
|
|
||||||
) -> None:
|
|
||||||
conf = jsonable_encoder(config)
|
|
||||||
set_config_for_machine(flake_dir, name, conf)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
"/api/schema",
|
|
||||||
tags=[Tags.machine],
|
|
||||||
responses={400: {"model": MissingClanImports}},
|
|
||||||
)
|
|
||||||
async def get_machine_schema(
|
|
||||||
flake_dir: Path, config: Annotated[dict, Body()]
|
|
||||||
) -> SchemaResponse:
|
|
||||||
schema = machine_schema(flake_dir, config=config)
|
|
||||||
return SchemaResponse(schema=schema)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/machines/{name}/verify", tags=[Tags.machine])
|
|
||||||
async def get_verify_machine_config(
|
|
||||||
flake_dir: Path, name: str
|
|
||||||
) -> VerifyMachineResponse:
|
|
||||||
error = verify_machine_config(flake_dir, name)
|
|
||||||
success = error is None
|
|
||||||
return VerifyMachineResponse(success=success, error=error)
|
|
||||||
|
|
||||||
|
|
||||||
@router.put("/api/machines/{name}/verify", tags=[Tags.machine])
|
|
||||||
async def put_verify_machine_config(
|
|
||||||
flake_dir: Path,
|
|
||||||
name: str,
|
|
||||||
config: Annotated[dict, Body()],
|
|
||||||
) -> VerifyMachineResponse:
|
|
||||||
error = verify_machine_config(flake_dir, name, config)
|
|
||||||
success = error is None
|
|
||||||
return VerifyMachineResponse(success=success, error=error)
|
|
|
@ -1,36 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from mimetypes import guess_type
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Response
|
|
||||||
|
|
||||||
from ..assets import asset_path
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{path_name:path}", include_in_schema=False)
|
|
||||||
async def root(path_name: str) -> Response:
|
|
||||||
if path_name == "":
|
|
||||||
path_name = "index.html"
|
|
||||||
filename = Path(os.path.normpath(asset_path() / path_name))
|
|
||||||
|
|
||||||
if not filename.is_relative_to(asset_path()):
|
|
||||||
log.error("Prevented directory traversal: %s", filename)
|
|
||||||
# prevent directory traversal
|
|
||||||
return Response(status_code=403)
|
|
||||||
|
|
||||||
if not filename.is_file():
|
|
||||||
if filename.suffix == "":
|
|
||||||
filename = filename.with_suffix(".html")
|
|
||||||
if not filename.is_file():
|
|
||||||
log.error("File not found: %s", filename)
|
|
||||||
return Response(status_code=404)
|
|
||||||
else:
|
|
||||||
return Response(status_code=404)
|
|
||||||
|
|
||||||
content_type, _ = guess_type(filename)
|
|
||||||
return Response(filename.read_bytes(), media_type=content_type)
|
|
|
@ -1,105 +0,0 @@
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from contextlib import ExitStack, contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
# XXX: can we dynamically load this using nix develop?
|
|
||||||
import uvicorn
|
|
||||||
from pydantic import AnyUrl, IPvAnyAddress
|
|
||||||
from pydantic.tools import parse_obj_as
|
|
||||||
|
|
||||||
from clan_cli.errors import ClanError
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def open_browser(base_url: AnyUrl, sub_url: str) -> None:
|
|
||||||
for i in range(5):
|
|
||||||
try:
|
|
||||||
urllib.request.urlopen(base_url + "/health")
|
|
||||||
break
|
|
||||||
except OSError:
|
|
||||||
time.sleep(i)
|
|
||||||
url = parse_obj_as(AnyUrl, f"{base_url}/{sub_url.removeprefix('/')}")
|
|
||||||
_open_browser(url)
|
|
||||||
|
|
||||||
|
|
||||||
def _open_browser(url: AnyUrl) -> subprocess.Popen:
|
|
||||||
for browser in ("firefox", "iceweasel", "iceape", "seamonkey"):
|
|
||||||
if shutil.which(browser):
|
|
||||||
# Do not add a new profile, as it will break in combination with
|
|
||||||
# the -kiosk flag.
|
|
||||||
cmd = [
|
|
||||||
browser,
|
|
||||||
"-kiosk",
|
|
||||||
"-new-window",
|
|
||||||
url,
|
|
||||||
]
|
|
||||||
print(" ".join(cmd))
|
|
||||||
return subprocess.Popen(cmd)
|
|
||||||
for browser in ("chromium", "chromium-browser", "google-chrome", "chrome"):
|
|
||||||
if shutil.which(browser):
|
|
||||||
return subprocess.Popen([browser, f"--app={url}"])
|
|
||||||
raise ClanError("No browser found")
|
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def spawn_node_dev_server(host: IPvAnyAddress, port: int) -> Iterator[None]:
|
|
||||||
log.info("Starting node dev server...")
|
|
||||||
path = Path(__file__).parent.parent.parent.parent / "ui"
|
|
||||||
with subprocess.Popen(
|
|
||||||
[
|
|
||||||
"direnv",
|
|
||||||
"exec",
|
|
||||||
path,
|
|
||||||
"npm",
|
|
||||||
"run",
|
|
||||||
"dev",
|
|
||||||
"--",
|
|
||||||
"--hostname",
|
|
||||||
str(host),
|
|
||||||
"--port",
|
|
||||||
str(port),
|
|
||||||
],
|
|
||||||
cwd=path,
|
|
||||||
) as proc:
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
proc.terminate()
|
|
||||||
|
|
||||||
|
|
||||||
def start_server(args: argparse.Namespace) -> None:
|
|
||||||
os.environ["CLAN_WEBUI_ENV"] = "development" if args.dev else "production"
|
|
||||||
|
|
||||||
with ExitStack() as stack:
|
|
||||||
headers: list[tuple[str, str]] = []
|
|
||||||
if args.dev:
|
|
||||||
stack.enter_context(spawn_node_dev_server(args.dev_host, args.dev_port))
|
|
||||||
|
|
||||||
base_url = f"http://{args.dev_host}:{args.dev_port}"
|
|
||||||
host = args.dev_host
|
|
||||||
if ":" in host:
|
|
||||||
host = f"[{host}]"
|
|
||||||
else:
|
|
||||||
base_url = f"http://{args.host}:{args.port}"
|
|
||||||
|
|
||||||
if not args.no_open:
|
|
||||||
Thread(target=open_browser, args=(base_url, args.sub_url)).start()
|
|
||||||
|
|
||||||
uvicorn.run(
|
|
||||||
"clan_cli.webui.app:app",
|
|
||||||
host=args.host,
|
|
||||||
port=args.port,
|
|
||||||
log_level=args.log_level,
|
|
||||||
reload=args.reload,
|
|
||||||
access_log=args.log_level == "debug",
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
|
@ -1,32 +0,0 @@
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class EnvType(Enum):
|
|
||||||
production = "production"
|
|
||||||
development = "development"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_environment() -> "EnvType":
|
|
||||||
t = os.environ.get("CLAN_WEBUI_ENV", "production")
|
|
||||||
try:
|
|
||||||
return EnvType[t]
|
|
||||||
except KeyError:
|
|
||||||
log.warning(f"Invalid environment type: {t}, fallback to production")
|
|
||||||
return EnvType.production
|
|
||||||
|
|
||||||
def is_production(self) -> bool:
|
|
||||||
return self == EnvType.production
|
|
||||||
|
|
||||||
def is_development(self) -> bool:
|
|
||||||
return self == EnvType.development
|
|
||||||
|
|
||||||
|
|
||||||
class Settings:
|
|
||||||
env = EnvType.from_environment()
|
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
|
|
@ -1,37 +0,0 @@
|
||||||
from enum import Enum
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
|
|
||||||
class Tags(Enum):
|
|
||||||
flake = "flake"
|
|
||||||
machine = "machine"
|
|
||||||
vm = "vm"
|
|
||||||
modules = "modules"
|
|
||||||
root = "root"
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
|
|
||||||
tags_metadata: list[dict[str, Any]] = [
|
|
||||||
{
|
|
||||||
"name": str(Tags.flake),
|
|
||||||
"description": "Operations on a flake.",
|
|
||||||
"externalDocs": {
|
|
||||||
"description": "What is a flake?",
|
|
||||||
"url": "https://www.tweag.io/blog/2020-05-25-flakes/",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": str(Tags.machine),
|
|
||||||
"description": "Manage physical machines. Instances of a flake",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": str(Tags.vm),
|
|
||||||
"description": "Manage virtual machines. Instances of a flake",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": str(Tags.modules),
|
|
||||||
"description": "Manage cLAN modules of a flake",
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,8 +1,6 @@
|
||||||
{ age
|
{ age
|
||||||
, lib
|
, lib
|
||||||
, argcomplete
|
, argcomplete
|
||||||
, fastapi
|
|
||||||
, uvicorn
|
|
||||||
, installShellFiles
|
, installShellFiles
|
||||||
, nix
|
, nix
|
||||||
, openssh
|
, openssh
|
||||||
|
@ -21,7 +19,6 @@
|
||||||
, wheel
|
, wheel
|
||||||
, fakeroot
|
, fakeroot
|
||||||
, rsync
|
, rsync
|
||||||
, ui-assets
|
|
||||||
, bash
|
, bash
|
||||||
, sshpass
|
, sshpass
|
||||||
, zbar
|
, zbar
|
||||||
|
@ -36,7 +33,6 @@
|
||||||
, rope
|
, rope
|
||||||
, clan-core-path
|
, clan-core-path
|
||||||
, writeShellScriptBin
|
, writeShellScriptBin
|
||||||
, nodePackages
|
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
|
||||||
|
@ -45,8 +41,6 @@ let
|
||||||
];
|
];
|
||||||
|
|
||||||
pytestDependencies = runtimeDependencies ++ dependencies ++ [
|
pytestDependencies = runtimeDependencies ++ dependencies ++ [
|
||||||
fastapi # optional dependencies: if not enabled, webui subcommand will not work
|
|
||||||
uvicorn # optional dependencies: if not enabled, webui subcommand will not work
|
|
||||||
|
|
||||||
#schemathesis # optional for http fuzzing
|
#schemathesis # optional for http fuzzing
|
||||||
pytest
|
pytest
|
||||||
|
@ -93,7 +87,6 @@ let
|
||||||
rm $out/clan_cli/config/jsonschema
|
rm $out/clan_cli/config/jsonschema
|
||||||
ln -s ${nixpkgs'} $out/clan_cli/nixpkgs
|
ln -s ${nixpkgs'} $out/clan_cli/nixpkgs
|
||||||
cp -r ${../../lib/jsonschema} $out/clan_cli/config/jsonschema
|
cp -r ${../../lib/jsonschema} $out/clan_cli/config/jsonschema
|
||||||
ln -s ${ui-assets} $out/clan_cli/webui/assets
|
|
||||||
'';
|
'';
|
||||||
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
|
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
|
||||||
mkdir $out
|
mkdir $out
|
||||||
|
@ -168,28 +161,8 @@ python3.pkgs.buildPythonApplication {
|
||||||
fi
|
fi
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
check-clan-openapi = runCommand "check-clan-openapi" { } ''
|
|
||||||
export PATH=${checkPython}/bin:$PATH
|
|
||||||
${checkPython}/bin/python ${source}/bin/gen-openapi --out ./openapi.json --app-dir ${source} clan_cli.webui.app:app
|
|
||||||
${lib.getExe nodePackages.prettier} --write ./openapi.json
|
|
||||||
|
|
||||||
if ! diff -u ./openapi.json ${source}/clan_cli/webui/openapi.json; then
|
|
||||||
echo "nix run .#update-clan-openapi to update the openapi.json file."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
touch $out
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
passthru.update-clan-openapi = writeShellScriptBin "update-clan-openapi" ''
|
|
||||||
export PATH=${checkPython}/bin:$PATH
|
|
||||||
git_root=$(git rev-parse --show-toplevel)
|
|
||||||
cd "$git_root/pkgs/clan-cli"
|
|
||||||
|
|
||||||
${checkPython}/bin/python ./bin/gen-openapi --out clan_cli/webui/openapi.json --app-dir . clan_cli.webui.app:app
|
|
||||||
${lib.getExe nodePackages.prettier} --write clan_cli/webui/openapi.json
|
|
||||||
'';
|
|
||||||
passthru.nixpkgs = nixpkgs';
|
passthru.nixpkgs = nixpkgs';
|
||||||
passthru.checkPython = checkPython;
|
passthru.checkPython = checkPython;
|
||||||
|
|
||||||
|
|
|
@ -30,12 +30,11 @@
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.clan-cli = pkgs.callPackage ./shell.nix {
|
devShells.clan-cli = pkgs.callPackage ./shell.nix {
|
||||||
inherit (self'.packages) clan-cli ui-assets nix-unit;
|
inherit (self'.packages) clan-cli nix-unit;
|
||||||
# inherit (inputs) democlan;
|
# inherit (inputs) democlan;
|
||||||
};
|
};
|
||||||
packages = {
|
packages = {
|
||||||
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
clan-cli = pkgs.python3.pkgs.callPackage ./default.nix {
|
||||||
inherit (self'.packages) ui-assets;
|
|
||||||
inherit (inputs) nixpkgs;
|
inherit (inputs) nixpkgs;
|
||||||
# inherit (inputs) democlan;
|
# inherit (inputs) democlan;
|
||||||
inherit (inputs.nixpkgs-for-deal.legacyPackages.${system}.python3Packages) deal;
|
inherit (inputs.nixpkgs-for-deal.legacyPackages.${system}.python3Packages) deal;
|
||||||
|
@ -45,11 +44,6 @@
|
||||||
default = self'.packages.clan-cli;
|
default = self'.packages.clan-cli;
|
||||||
};
|
};
|
||||||
|
|
||||||
apps.update-clan-openapi = {
|
|
||||||
type = "app";
|
|
||||||
program = "${self'.packages.clan-cli.passthru.update-clan-openapi}/bin/update-clan-openapi";
|
|
||||||
};
|
|
||||||
|
|
||||||
checks = self'.packages.clan-cli.tests;
|
checks = self'.packages.clan-cli.tests;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
{ nix-unit, clan-cli, ui-assets, system, mkShell, writeScriptBin, openssh, ruff, python3 }:
|
{ nix-unit, clan-cli, system, mkShell, writeScriptBin, openssh, ruff, python3 }:
|
||||||
let
|
let
|
||||||
checkScript = writeScriptBin "check" ''
|
checkScript = writeScriptBin "check" ''
|
||||||
nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@"
|
nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@"
|
||||||
|
@ -39,7 +39,6 @@ mkShell {
|
||||||
--editable $repo_root
|
--editable $repo_root
|
||||||
|
|
||||||
ln -sfT ${clan-cli.nixpkgs} clan_cli/nixpkgs
|
ln -sfT ${clan-cli.nixpkgs} clan_cli/nixpkgs
|
||||||
ln -sfT ${ui-assets} clan_cli/webui/assets
|
|
||||||
|
|
||||||
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
|
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
|
||||||
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:"
|
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:"
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import logging
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from fastapi.testclient import TestClient
|
|
||||||
|
|
||||||
from clan_cli.webui.app import app
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: Why stateful
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def api() -> TestClient:
|
|
||||||
# logging.getLogger("httpx").setLevel(level=logging.WARNING)
|
|
||||||
logging.getLogger("asyncio").setLevel(logging.INFO)
|
|
||||||
return TestClient(app, raise_server_exceptions=False)
|
|
|
@ -10,7 +10,6 @@ from clan_cli.nix import nix_shell
|
||||||
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
|
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
|
||||||
|
|
||||||
pytest_plugins = [
|
pytest_plugins = [
|
||||||
"api",
|
|
||||||
"temporary_dir",
|
"temporary_dir",
|
||||||
"root",
|
"root",
|
||||||
"age_keys",
|
"age_keys",
|
||||||
|
|
|
@ -9,8 +9,6 @@ from pathlib import Path
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import AnyUrl
|
|
||||||
from pydantic.tools import parse_obj_as
|
|
||||||
from root import CLAN_CORE
|
from root import CLAN_CORE
|
||||||
|
|
||||||
from clan_cli.dirs import nixpkgs_source
|
from clan_cli.dirs import nixpkgs_source
|
||||||
|
@ -136,16 +134,6 @@ def test_local_democlan(
|
||||||
yield FlakeForTest(democlan_p)
|
yield FlakeForTest(democlan_p)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_democlan_url(
|
|
||||||
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
|
||||||
) -> Iterator[AnyUrl]:
|
|
||||||
yield parse_obj_as(
|
|
||||||
AnyUrl,
|
|
||||||
"https://git.clan.lol/clan/democlan/archive/main.tar.gz",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_flake_with_core_and_pass(
|
def test_flake_with_core_and_pass(
|
||||||
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
monkeypatch: pytest.MonkeyPatch, temporary_home: Path
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import pytest
|
|
||||||
from api import TestClient
|
|
||||||
from fixtures_flakes import FlakeForTest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.with_core
|
|
||||||
def test_configure_machine(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
|
|
||||||
# retrieve the list of available clanModules
|
|
||||||
response = api.get(f"/api/clan_modules?flake_dir={test_flake_with_core.path}")
|
|
||||||
assert response.status_code == 200, response.text
|
|
||||||
response_json = response.json()
|
|
||||||
assert isinstance(response_json, dict)
|
|
||||||
assert "clan_modules" in response_json
|
|
||||||
assert len(response_json["clan_modules"]) > 0
|
|
||||||
# ensure all entries are a string
|
|
||||||
assert all(isinstance(x, str) for x in response_json["clan_modules"])
|
|
|
@ -3,35 +3,14 @@ import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from api import TestClient
|
|
||||||
from cli import Cli
|
from cli import Cli
|
||||||
|
|
||||||
from clan_cli.flakes.create import DEFAULT_URL
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def cli() -> Cli:
|
def cli() -> Cli:
|
||||||
return Cli()
|
return Cli()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
|
||||||
def test_create_flake_api(
|
|
||||||
monkeypatch: pytest.MonkeyPatch, api: TestClient, temporary_home: Path
|
|
||||||
) -> None:
|
|
||||||
flake_dir = temporary_home / "test-flake"
|
|
||||||
response = api.post(
|
|
||||||
f"/api/flake/create?flake_dir={flake_dir}",
|
|
||||||
json=dict(
|
|
||||||
flake_dir=str(flake_dir),
|
|
||||||
url=str(DEFAULT_URL),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert response.status_code == 201, f"Failed to create flake {response.text}"
|
|
||||||
assert (flake_dir / ".clan-flake").exists()
|
|
||||||
assert (flake_dir / "flake.nix").exists()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
def test_create_flake(
|
def test_create_flake(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from api import TestClient
|
|
||||||
from fixtures_flakes import FlakeForTest
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
|
||||||
def test_inspect_ok(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
|
|
||||||
params = {"url": str(test_flake_with_core.path)}
|
|
||||||
response = api.get(
|
|
||||||
"/api/flake/attrs",
|
|
||||||
params=params,
|
|
||||||
)
|
|
||||||
assert response.status_code == 200, "Failed to inspect vm"
|
|
||||||
data = response.json()
|
|
||||||
print("Data: ", data)
|
|
||||||
assert data.get("flake_attrs") == ["vm1", "vm2"]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
|
||||||
def test_inspect_err(api: TestClient) -> None:
|
|
||||||
params = {"url": "flake-parts"}
|
|
||||||
response = api.get(
|
|
||||||
"/api/flake/attrs",
|
|
||||||
params=params,
|
|
||||||
)
|
|
||||||
assert response.status_code != 200, "Succeed to inspect vm but expected to fail"
|
|
||||||
data = response.json()
|
|
||||||
print("Data: ", data)
|
|
||||||
assert data.get("detail")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
|
||||||
def test_inspect_flake(api: TestClient, test_flake_with_core: FlakeForTest) -> None:
|
|
||||||
params = {"url": str(test_flake_with_core.path)}
|
|
||||||
response = api.get(
|
|
||||||
"/api/flake/inspect",
|
|
||||||
params=params,
|
|
||||||
)
|
|
||||||
assert response.status_code == 200, "Failed to inspect vm"
|
|
||||||
data = response.json()
|
|
||||||
print("Data: ", json.dumps(data, indent=2))
|
|
||||||
assert data.get("content") is not None
|
|
||||||
actions = data.get("actions")
|
|
||||||
assert actions is not None
|
|
||||||
assert len(actions) == 2
|
|
||||||
assert actions[0].get("id") == "vms/inspect"
|
|
||||||
assert actions[0].get("uri") == "api/vms/inspect"
|
|
||||||
assert actions[1].get("id") == "vms/create"
|
|
||||||
assert actions[1].get("uri") == "api/vms/create"
|
|
|
@ -1,10 +0,0 @@
|
||||||
import pytest
|
|
||||||
from api import TestClient
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
|
||||||
def test_static_files(api: TestClient) -> None:
|
|
||||||
response = api.get("/")
|
|
||||||
assert response.headers["content-type"] == "text/html; charset=utf-8"
|
|
||||||
response = api.get("/does-no-exists.txt")
|
|
||||||
assert response.status_code == 404
|
|
|
@ -1,64 +0,0 @@
|
||||||
import os
|
|
||||||
import select
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from cli import Cli
|
|
||||||
from ports import PortFunction
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.timeout(10)
|
|
||||||
def test_start_server(unused_tcp_port: PortFunction, temporary_home: Path) -> None:
|
|
||||||
Cli()
|
|
||||||
port = unused_tcp_port()
|
|
||||||
|
|
||||||
fifo = temporary_home / "fifo"
|
|
||||||
os.mkfifo(fifo)
|
|
||||||
|
|
||||||
# Create a script called "firefox" in the temporary home directory that
|
|
||||||
# writes "1" to the fifo. This is used to notify the test that the firefox has been
|
|
||||||
# started.
|
|
||||||
notify_script = temporary_home / "firefox"
|
|
||||||
bash = shutil.which("bash")
|
|
||||||
assert bash is not None
|
|
||||||
notify_script.write_text(
|
|
||||||
f"""#!{bash}
|
|
||||||
set -x
|
|
||||||
echo "1" > {fifo}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
notify_script.chmod(0o700)
|
|
||||||
|
|
||||||
# Add the temporary home directory to the PATH so that the script is found
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["PATH"] = f"{temporary_home}:{env['PATH']}"
|
|
||||||
|
|
||||||
# Add build/src to PYTHONPATH so that the webui module is found in nix sandbox
|
|
||||||
# TODO: We need a way to make sure things which work in the devshell also work in the sandbox
|
|
||||||
python_path = env.get("PYTHONPATH")
|
|
||||||
if python_path:
|
|
||||||
env["PYTHONPATH"] = f"/build/src:{python_path}"
|
|
||||||
|
|
||||||
# breakpoint_container(
|
|
||||||
# cmd=[sys.executable, "-m", "clan_cli.webui", "--port", str(port)],
|
|
||||||
# env=env,
|
|
||||||
# work_dir=temporary_home,
|
|
||||||
# )
|
|
||||||
|
|
||||||
with subprocess.Popen(
|
|
||||||
[sys.executable, "-m", "clan_cli.webui", "--port", str(port)],
|
|
||||||
env=env,
|
|
||||||
stdout=sys.stderr,
|
|
||||||
stderr=sys.stderr,
|
|
||||||
text=True,
|
|
||||||
) as p:
|
|
||||||
try:
|
|
||||||
with open(fifo) as f:
|
|
||||||
r, _, _ = select.select([f], [], [], 10)
|
|
||||||
assert f in r
|
|
||||||
assert f.read().strip() == "1"
|
|
||||||
finally:
|
|
||||||
p.kill()
|
|
|
@ -3,8 +3,6 @@
|
||||||
./clan-cli/flake-module.nix
|
./clan-cli/flake-module.nix
|
||||||
./clan-vm-manager/flake-module.nix
|
./clan-vm-manager/flake-module.nix
|
||||||
./installer/flake-module.nix
|
./installer/flake-module.nix
|
||||||
./ui/flake-module.nix
|
|
||||||
./theme/flake-module.nix
|
|
||||||
];
|
];
|
||||||
|
|
||||||
perSystem = { pkgs, config, lib, ... }: {
|
perSystem = { pkgs, config, lib, ... }: {
|
||||||
|
@ -17,7 +15,6 @@
|
||||||
pending-reviews = pkgs.callPackage ./pending-reviews { };
|
pending-reviews = pkgs.callPackage ./pending-reviews { };
|
||||||
nix-unit = pkgs.callPackage ./nix-unit { };
|
nix-unit = pkgs.callPackage ./nix-unit { };
|
||||||
meshname = pkgs.callPackage ./meshname { };
|
meshname = pkgs.callPackage ./meshname { };
|
||||||
inherit (pkgs.callPackages ./node-packages { }) prettier-plugin-tailwindcss;
|
|
||||||
} // lib.optionalAttrs pkgs.stdenv.isLinux {
|
} // lib.optionalAttrs pkgs.stdenv.isLinux {
|
||||||
aemu = pkgs.callPackage ./aemu { };
|
aemu = pkgs.callPackage ./aemu { };
|
||||||
gfxstream = pkgs.callPackage ./gfxstream {
|
gfxstream = pkgs.callPackage ./gfxstream {
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
# This file has been generated by node2nix 1.11.1. Do not edit!
|
|
||||||
|
|
||||||
{pkgs ? import <nixpkgs> {
|
|
||||||
inherit system;
|
|
||||||
}, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_18"}:
|
|
||||||
|
|
||||||
let
|
|
||||||
nodeEnv = import ./node-env.nix {
|
|
||||||
inherit (pkgs) stdenv lib python2 runCommand writeTextFile writeShellScript;
|
|
||||||
inherit pkgs nodejs;
|
|
||||||
libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
import ./node-packages.nix {
|
|
||||||
inherit (pkgs) fetchurl nix-gitignore stdenv lib fetchgit;
|
|
||||||
inherit nodeEnv;
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{ pkgs, system, nodejs-18_x, makeWrapper }:
|
|
||||||
let
|
|
||||||
nodePackages = import ./composition.nix {
|
|
||||||
inherit pkgs system;
|
|
||||||
nodejs = nodejs-18_x;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
nodePackages
|
|
|
@ -1,6 +0,0 @@
|
||||||
#!/usr/bin/env nix-shell
|
|
||||||
#! nix-shell -i bash -p nodePackages.node2nix
|
|
||||||
# shellcheck shell=bash
|
|
||||||
|
|
||||||
rm -f node-env.nix
|
|
||||||
node2nix -18 -i node-packages.json -o node-packages.nix -c composition.nix
|
|
|
@ -1,689 +0,0 @@
|
||||||
# This file originates from node2nix
|
|
||||||
|
|
||||||
{lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile, writeShellScript}:
|
|
||||||
|
|
||||||
let
|
|
||||||
# Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master
|
|
||||||
utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux;
|
|
||||||
|
|
||||||
python = if nodejs ? python then nodejs.python else python2;
|
|
||||||
|
|
||||||
# Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise
|
|
||||||
tarWrapper = runCommand "tarWrapper" {} ''
|
|
||||||
mkdir -p $out/bin
|
|
||||||
|
|
||||||
cat > $out/bin/tar <<EOF
|
|
||||||
#! ${stdenv.shell} -e
|
|
||||||
$(type -p tar) "\$@" --warning=no-unknown-keyword --delay-directory-restore
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x $out/bin/tar
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Function that generates a TGZ file from a NPM project
|
|
||||||
buildNodeSourceDist =
|
|
||||||
{ name, version, src, ... }:
|
|
||||||
|
|
||||||
stdenv.mkDerivation {
|
|
||||||
name = "node-tarball-${name}-${version}";
|
|
||||||
inherit src;
|
|
||||||
buildInputs = [ nodejs ];
|
|
||||||
buildPhase = ''
|
|
||||||
export HOME=$TMPDIR
|
|
||||||
tgzFile=$(npm pack | tail -n 1) # Hooks to the pack command will add output (https://docs.npmjs.com/misc/scripts)
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/tarballs
|
|
||||||
mv $tgzFile $out/tarballs
|
|
||||||
mkdir -p $out/nix-support
|
|
||||||
echo "file source-dist $out/tarballs/$tgzFile" >> $out/nix-support/hydra-build-products
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Common shell logic
|
|
||||||
installPackage = writeShellScript "install-package" ''
|
|
||||||
installPackage() {
|
|
||||||
local packageName=$1 src=$2
|
|
||||||
|
|
||||||
local strippedName
|
|
||||||
|
|
||||||
local DIR=$PWD
|
|
||||||
cd $TMPDIR
|
|
||||||
|
|
||||||
unpackFile $src
|
|
||||||
|
|
||||||
# Make the base dir in which the target dependency resides first
|
|
||||||
mkdir -p "$(dirname "$DIR/$packageName")"
|
|
||||||
|
|
||||||
if [ -f "$src" ]
|
|
||||||
then
|
|
||||||
# Figure out what directory has been unpacked
|
|
||||||
packageDir="$(find . -maxdepth 1 -type d | tail -1)"
|
|
||||||
|
|
||||||
# Restore write permissions to make building work
|
|
||||||
find "$packageDir" -type d -exec chmod u+x {} \;
|
|
||||||
chmod -R u+w "$packageDir"
|
|
||||||
|
|
||||||
# Move the extracted tarball into the output folder
|
|
||||||
mv "$packageDir" "$DIR/$packageName"
|
|
||||||
elif [ -d "$src" ]
|
|
||||||
then
|
|
||||||
# Get a stripped name (without hash) of the source directory.
|
|
||||||
# On old nixpkgs it's already set internally.
|
|
||||||
if [ -z "$strippedName" ]
|
|
||||||
then
|
|
||||||
strippedName="$(stripHash $src)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restore write permissions to make building work
|
|
||||||
chmod -R u+w "$strippedName"
|
|
||||||
|
|
||||||
# Move the extracted directory into the output folder
|
|
||||||
mv "$strippedName" "$DIR/$packageName"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Change to the package directory to install dependencies
|
|
||||||
cd "$DIR/$packageName"
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Bundle the dependencies of the package
|
|
||||||
#
|
|
||||||
# Only include dependencies if they don't exist. They may also be bundled in the package.
|
|
||||||
includeDependencies = {dependencies}:
|
|
||||||
lib.optionalString (dependencies != []) (
|
|
||||||
''
|
|
||||||
mkdir -p node_modules
|
|
||||||
cd node_modules
|
|
||||||
''
|
|
||||||
+ (lib.concatMapStrings (dependency:
|
|
||||||
''
|
|
||||||
if [ ! -e "${dependency.packageName}" ]; then
|
|
||||||
${composePackage dependency}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
) dependencies)
|
|
||||||
+ ''
|
|
||||||
cd ..
|
|
||||||
''
|
|
||||||
);
|
|
||||||
|
|
||||||
# Recursively composes the dependencies of a package
|
|
||||||
composePackage = { name, packageName, src, dependencies ? [], ... }@args:
|
|
||||||
builtins.addErrorContext "while evaluating node package '${packageName}'" ''
|
|
||||||
installPackage "${packageName}" "${src}"
|
|
||||||
${includeDependencies { inherit dependencies; }}
|
|
||||||
cd ..
|
|
||||||
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
|
|
||||||
'';
|
|
||||||
|
|
||||||
pinpointDependencies = {dependencies, production}:
|
|
||||||
let
|
|
||||||
pinpointDependenciesFromPackageJSON = writeTextFile {
|
|
||||||
name = "pinpointDependencies.js";
|
|
||||||
text = ''
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
function resolveDependencyVersion(location, name) {
|
|
||||||
if(location == process.env['NIX_STORE']) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json");
|
|
||||||
|
|
||||||
if(fs.existsSync(dependencyPackageJSON)) {
|
|
||||||
var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON));
|
|
||||||
|
|
||||||
if(dependencyPackageObj.name == name) {
|
|
||||||
return dependencyPackageObj.version;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return resolveDependencyVersion(path.resolve(location, ".."), name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceDependencies(dependencies) {
|
|
||||||
if(typeof dependencies == "object" && dependencies !== null) {
|
|
||||||
for(var dependency in dependencies) {
|
|
||||||
var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency);
|
|
||||||
|
|
||||||
if(resolvedVersion === null) {
|
|
||||||
process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n");
|
|
||||||
} else {
|
|
||||||
dependencies[dependency] = resolvedVersion;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Read the package.json configuration */
|
|
||||||
var packageObj = JSON.parse(fs.readFileSync('./package.json'));
|
|
||||||
|
|
||||||
/* Pinpoint all dependencies */
|
|
||||||
replaceDependencies(packageObj.dependencies);
|
|
||||||
if(process.argv[2] == "development") {
|
|
||||||
replaceDependencies(packageObj.devDependencies);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
packageObj.devDependencies = {};
|
|
||||||
}
|
|
||||||
replaceDependencies(packageObj.optionalDependencies);
|
|
||||||
replaceDependencies(packageObj.peerDependencies);
|
|
||||||
|
|
||||||
/* Write the fixed package.json file */
|
|
||||||
fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2));
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
in
|
|
||||||
''
|
|
||||||
node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"}
|
|
||||||
|
|
||||||
${lib.optionalString (dependencies != [])
|
|
||||||
''
|
|
||||||
if [ -d node_modules ]
|
|
||||||
then
|
|
||||||
cd node_modules
|
|
||||||
${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies}
|
|
||||||
cd ..
|
|
||||||
fi
|
|
||||||
''}
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Recursively traverses all dependencies of a package and pinpoints all
|
|
||||||
# dependencies in the package.json file to the versions that are actually
|
|
||||||
# being used.
|
|
||||||
|
|
||||||
pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args:
|
|
||||||
''
|
|
||||||
if [ -d "${packageName}" ]
|
|
||||||
then
|
|
||||||
cd "${packageName}"
|
|
||||||
${pinpointDependencies { inherit dependencies production; }}
|
|
||||||
cd ..
|
|
||||||
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Extract the Node.js source code which is used to compile packages with
|
|
||||||
# native bindings
|
|
||||||
nodeSources = runCommand "node-sources" {} ''
|
|
||||||
tar --no-same-owner --no-same-permissions -xf ${nodejs.src}
|
|
||||||
mv node-* $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty)
|
|
||||||
addIntegrityFieldsScript = writeTextFile {
|
|
||||||
name = "addintegrityfields.js";
|
|
||||||
text = ''
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
function augmentDependencies(baseDir, dependencies) {
|
|
||||||
for(var dependencyName in dependencies) {
|
|
||||||
var dependency = dependencies[dependencyName];
|
|
||||||
|
|
||||||
// Open package.json and augment metadata fields
|
|
||||||
var packageJSONDir = path.join(baseDir, "node_modules", dependencyName);
|
|
||||||
var packageJSONPath = path.join(packageJSONDir, "package.json");
|
|
||||||
|
|
||||||
if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored
|
|
||||||
console.log("Adding metadata fields to: "+packageJSONPath);
|
|
||||||
var packageObj = JSON.parse(fs.readFileSync(packageJSONPath));
|
|
||||||
|
|
||||||
if(dependency.integrity) {
|
|
||||||
packageObj["_integrity"] = dependency.integrity;
|
|
||||||
} else {
|
|
||||||
packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads.
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dependency.resolved) {
|
|
||||||
packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided
|
|
||||||
} else {
|
|
||||||
packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories.
|
|
||||||
}
|
|
||||||
|
|
||||||
if(dependency.from !== undefined) { // Adopt from property if one has been provided
|
|
||||||
packageObj["_from"] = dependency.from;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Augment transitive dependencies
|
|
||||||
if(dependency.dependencies !== undefined) {
|
|
||||||
augmentDependencies(packageJSONDir, dependency.dependencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(fs.existsSync("./package-lock.json")) {
|
|
||||||
var packageLock = JSON.parse(fs.readFileSync("./package-lock.json"));
|
|
||||||
|
|
||||||
if(![1, 2].includes(packageLock.lockfileVersion)) {
|
|
||||||
process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(packageLock.dependencies !== undefined) {
|
|
||||||
augmentDependencies(".", packageLock.dependencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes
|
|
||||||
reconstructPackageLock = writeTextFile {
|
|
||||||
name = "reconstructpackagelock.js";
|
|
||||||
text = ''
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
var packageObj = JSON.parse(fs.readFileSync("package.json"));
|
|
||||||
|
|
||||||
var lockObj = {
|
|
||||||
name: packageObj.name,
|
|
||||||
version: packageObj.version,
|
|
||||||
lockfileVersion: 2,
|
|
||||||
requires: true,
|
|
||||||
packages: {
|
|
||||||
"": {
|
|
||||||
name: packageObj.name,
|
|
||||||
version: packageObj.version,
|
|
||||||
license: packageObj.license,
|
|
||||||
bin: packageObj.bin,
|
|
||||||
dependencies: packageObj.dependencies,
|
|
||||||
engines: packageObj.engines,
|
|
||||||
optionalDependencies: packageObj.optionalDependencies
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dependencies: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
function augmentPackageJSON(filePath, packages, dependencies) {
|
|
||||||
var packageJSON = path.join(filePath, "package.json");
|
|
||||||
if(fs.existsSync(packageJSON)) {
|
|
||||||
var packageObj = JSON.parse(fs.readFileSync(packageJSON));
|
|
||||||
packages[filePath] = {
|
|
||||||
version: packageObj.version,
|
|
||||||
integrity: "sha1-000000000000000000000000000=",
|
|
||||||
dependencies: packageObj.dependencies,
|
|
||||||
engines: packageObj.engines,
|
|
||||||
optionalDependencies: packageObj.optionalDependencies
|
|
||||||
};
|
|
||||||
dependencies[packageObj.name] = {
|
|
||||||
version: packageObj.version,
|
|
||||||
integrity: "sha1-000000000000000000000000000=",
|
|
||||||
dependencies: {}
|
|
||||||
};
|
|
||||||
processDependencies(path.join(filePath, "node_modules"), packages, dependencies[packageObj.name].dependencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processDependencies(dir, packages, dependencies) {
|
|
||||||
if(fs.existsSync(dir)) {
|
|
||||||
var files = fs.readdirSync(dir);
|
|
||||||
|
|
||||||
files.forEach(function(entry) {
|
|
||||||
var filePath = path.join(dir, entry);
|
|
||||||
var stats = fs.statSync(filePath);
|
|
||||||
|
|
||||||
if(stats.isDirectory()) {
|
|
||||||
if(entry.substr(0, 1) == "@") {
|
|
||||||
// When we encounter a namespace folder, augment all packages belonging to the scope
|
|
||||||
var pkgFiles = fs.readdirSync(filePath);
|
|
||||||
|
|
||||||
pkgFiles.forEach(function(entry) {
|
|
||||||
if(stats.isDirectory()) {
|
|
||||||
var pkgFilePath = path.join(filePath, entry);
|
|
||||||
augmentPackageJSON(pkgFilePath, packages, dependencies);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
augmentPackageJSON(filePath, packages, dependencies);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
processDependencies("node_modules", lockObj.packages, lockObj.dependencies);
|
|
||||||
|
|
||||||
fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2));
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
# Script that links bins defined in package.json to the node_modules bin directory
|
|
||||||
# NPM does not do this for top-level packages itself anymore as of v7
|
|
||||||
linkBinsScript = writeTextFile {
|
|
||||||
name = "linkbins.js";
|
|
||||||
text = ''
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
var packageObj = JSON.parse(fs.readFileSync("package.json"));
|
|
||||||
|
|
||||||
var nodeModules = Array(packageObj.name.split("/").length).fill("..").join(path.sep);
|
|
||||||
|
|
||||||
if(packageObj.bin !== undefined) {
|
|
||||||
fs.mkdirSync(path.join(nodeModules, ".bin"))
|
|
||||||
|
|
||||||
if(typeof packageObj.bin == "object") {
|
|
||||||
Object.keys(packageObj.bin).forEach(function(exe) {
|
|
||||||
if(fs.existsSync(packageObj.bin[exe])) {
|
|
||||||
console.log("linking bin '" + exe + "'");
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.join("..", packageObj.name, packageObj.bin[exe]),
|
|
||||||
path.join(nodeModules, ".bin", exe)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log("skipping non-existent bin '" + exe + "'");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if(fs.existsSync(packageObj.bin)) {
|
|
||||||
console.log("linking bin '" + packageObj.bin + "'");
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.join("..", packageObj.name, packageObj.bin),
|
|
||||||
path.join(nodeModules, ".bin", packageObj.name.split("/").pop())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log("skipping non-existent bin '" + packageObj.bin + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(packageObj.directories !== undefined && packageObj.directories.bin !== undefined) {
|
|
||||||
fs.mkdirSync(path.join(nodeModules, ".bin"))
|
|
||||||
|
|
||||||
fs.readdirSync(packageObj.directories.bin).forEach(function(exe) {
|
|
||||||
if(fs.existsSync(path.join(packageObj.directories.bin, exe))) {
|
|
||||||
console.log("linking bin '" + exe + "'");
|
|
||||||
fs.symlinkSync(
|
|
||||||
path.join("..", packageObj.name, packageObj.directories.bin, exe),
|
|
||||||
path.join(nodeModules, ".bin", exe)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log("skipping non-existent bin '" + exe + "'");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}:
|
|
||||||
let
|
|
||||||
forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com";
|
|
||||||
in
|
|
||||||
''
|
|
||||||
# Pinpoint the versions of all dependencies to the ones that are actually being used
|
|
||||||
echo "pinpointing versions of dependencies..."
|
|
||||||
source $pinpointDependenciesScriptPath
|
|
||||||
|
|
||||||
# Patch the shebangs of the bundled modules to prevent them from
|
|
||||||
# calling executables outside the Nix store as much as possible
|
|
||||||
patchShebangs .
|
|
||||||
|
|
||||||
# Deploy the Node.js package by running npm install. Since the
|
|
||||||
# dependencies have been provided already by ourselves, it should not
|
|
||||||
# attempt to install them again, which is good, because we want to make
|
|
||||||
# it Nix's responsibility. If it needs to install any dependencies
|
|
||||||
# anyway (e.g. because the dependency parameters are
|
|
||||||
# incomplete/incorrect), it fails.
|
|
||||||
#
|
|
||||||
# The other responsibilities of NPM are kept -- version checks, build
|
|
||||||
# steps, postprocessing etc.
|
|
||||||
|
|
||||||
export HOME=$TMPDIR
|
|
||||||
cd "${packageName}"
|
|
||||||
runHook preRebuild
|
|
||||||
|
|
||||||
${lib.optionalString bypassCache ''
|
|
||||||
${lib.optionalString reconstructLock ''
|
|
||||||
if [ -f package-lock.json ]
|
|
||||||
then
|
|
||||||
echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!"
|
|
||||||
echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!"
|
|
||||||
rm package-lock.json
|
|
||||||
else
|
|
||||||
echo "No package-lock.json file found, reconstructing..."
|
|
||||||
fi
|
|
||||||
|
|
||||||
node ${reconstructPackageLock}
|
|
||||||
''}
|
|
||||||
|
|
||||||
node ${addIntegrityFieldsScript}
|
|
||||||
''}
|
|
||||||
|
|
||||||
npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild
|
|
||||||
|
|
||||||
runHook postRebuild
|
|
||||||
|
|
||||||
if [ "''${dontNpmInstall-}" != "1" ]
|
|
||||||
then
|
|
||||||
# NPM tries to download packages even when they already exist if npm-shrinkwrap is used.
|
|
||||||
rm -f npm-shrinkwrap.json
|
|
||||||
|
|
||||||
npm ${forceOfflineFlag} --nodedir=${nodeSources} --no-bin-links --ignore-scripts ${npmFlags} ${lib.optionalString production "--production"} install
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Link executables defined in package.json
|
|
||||||
node ${linkBinsScript}
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Builds and composes an NPM package including all its dependencies
|
|
||||||
buildNodePackage =
|
|
||||||
{ name
|
|
||||||
, packageName
|
|
||||||
, version ? null
|
|
||||||
, dependencies ? []
|
|
||||||
, buildInputs ? []
|
|
||||||
, production ? true
|
|
||||||
, npmFlags ? ""
|
|
||||||
, dontNpmInstall ? false
|
|
||||||
, bypassCache ? false
|
|
||||||
, reconstructLock ? false
|
|
||||||
, preRebuild ? ""
|
|
||||||
, dontStrip ? true
|
|
||||||
, unpackPhase ? "true"
|
|
||||||
, buildPhase ? "true"
|
|
||||||
, meta ? {}
|
|
||||||
, ... }@args:
|
|
||||||
|
|
||||||
let
|
|
||||||
extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ];
|
|
||||||
in
|
|
||||||
stdenv.mkDerivation ({
|
|
||||||
name = "${name}${if version == null then "" else "-${version}"}";
|
|
||||||
buildInputs = [ tarWrapper python nodejs ]
|
|
||||||
++ lib.optional (stdenv.isLinux) utillinux
|
|
||||||
++ lib.optional (stdenv.isDarwin) libtool
|
|
||||||
++ buildInputs;
|
|
||||||
|
|
||||||
inherit nodejs;
|
|
||||||
|
|
||||||
inherit dontStrip; # Stripping may fail a build for some package deployments
|
|
||||||
inherit dontNpmInstall preRebuild unpackPhase buildPhase;
|
|
||||||
|
|
||||||
compositionScript = composePackage args;
|
|
||||||
pinpointDependenciesScript = pinpointDependenciesOfPackage args;
|
|
||||||
|
|
||||||
passAsFile = [ "compositionScript" "pinpointDependenciesScript" ];
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
source ${installPackage}
|
|
||||||
|
|
||||||
# Create and enter a root node_modules/ folder
|
|
||||||
mkdir -p $out/lib/node_modules
|
|
||||||
cd $out/lib/node_modules
|
|
||||||
|
|
||||||
# Compose the package and all its dependencies
|
|
||||||
source $compositionScriptPath
|
|
||||||
|
|
||||||
${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
|
|
||||||
|
|
||||||
# Create symlink to the deployed executable folder, if applicable
|
|
||||||
if [ -d "$out/lib/node_modules/.bin" ]
|
|
||||||
then
|
|
||||||
ln -s $out/lib/node_modules/.bin $out/bin
|
|
||||||
|
|
||||||
# Fixup all executables
|
|
||||||
ls $out/bin/* | while read i
|
|
||||||
do
|
|
||||||
file="$(readlink -f "$i")"
|
|
||||||
chmod u+rwx "$file"
|
|
||||||
if isScript "$file"
|
|
||||||
then
|
|
||||||
sed -i 's/\r$//' "$file" # convert crlf to lf
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create symlinks to the deployed manual page folders, if applicable
|
|
||||||
if [ -d "$out/lib/node_modules/${packageName}/man" ]
|
|
||||||
then
|
|
||||||
mkdir -p $out/share
|
|
||||||
for dir in "$out/lib/node_modules/${packageName}/man/"*
|
|
||||||
do
|
|
||||||
mkdir -p $out/share/man/$(basename "$dir")
|
|
||||||
for page in "$dir"/*
|
|
||||||
do
|
|
||||||
ln -s $page $out/share/man/$(basename "$dir")
|
|
||||||
done
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run post install hook, if provided
|
|
||||||
runHook postInstall
|
|
||||||
'';
|
|
||||||
|
|
||||||
meta = {
|
|
||||||
# default to Node.js' platforms
|
|
||||||
platforms = nodejs.meta.platforms;
|
|
||||||
} // meta;
|
|
||||||
} // extraArgs);
|
|
||||||
|
|
||||||
# Builds a node environment (a node_modules folder and a set of binaries)
|
|
||||||
buildNodeDependencies =
|
|
||||||
{ name
|
|
||||||
, packageName
|
|
||||||
, version ? null
|
|
||||||
, src
|
|
||||||
, dependencies ? []
|
|
||||||
, buildInputs ? []
|
|
||||||
, production ? true
|
|
||||||
, npmFlags ? ""
|
|
||||||
, dontNpmInstall ? false
|
|
||||||
, bypassCache ? false
|
|
||||||
, reconstructLock ? false
|
|
||||||
, dontStrip ? true
|
|
||||||
, unpackPhase ? "true"
|
|
||||||
, buildPhase ? "true"
|
|
||||||
, ... }@args:
|
|
||||||
|
|
||||||
let
|
|
||||||
extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ];
|
|
||||||
in
|
|
||||||
stdenv.mkDerivation ({
|
|
||||||
name = "node-dependencies-${name}${if version == null then "" else "-${version}"}";
|
|
||||||
|
|
||||||
buildInputs = [ tarWrapper python nodejs ]
|
|
||||||
++ lib.optional (stdenv.isLinux) utillinux
|
|
||||||
++ lib.optional (stdenv.isDarwin) libtool
|
|
||||||
++ buildInputs;
|
|
||||||
|
|
||||||
inherit dontStrip; # Stripping may fail a build for some package deployments
|
|
||||||
inherit dontNpmInstall unpackPhase buildPhase;
|
|
||||||
|
|
||||||
includeScript = includeDependencies { inherit dependencies; };
|
|
||||||
pinpointDependenciesScript = pinpointDependenciesOfPackage args;
|
|
||||||
|
|
||||||
passAsFile = [ "includeScript" "pinpointDependenciesScript" ];
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
source ${installPackage}
|
|
||||||
|
|
||||||
mkdir -p $out/${packageName}
|
|
||||||
cd $out/${packageName}
|
|
||||||
|
|
||||||
source $includeScriptPath
|
|
||||||
|
|
||||||
# Create fake package.json to make the npm commands work properly
|
|
||||||
cp ${src}/package.json .
|
|
||||||
chmod 644 package.json
|
|
||||||
${lib.optionalString bypassCache ''
|
|
||||||
if [ -f ${src}/package-lock.json ]
|
|
||||||
then
|
|
||||||
cp ${src}/package-lock.json .
|
|
||||||
chmod 644 package-lock.json
|
|
||||||
fi
|
|
||||||
''}
|
|
||||||
|
|
||||||
# Go to the parent folder to make sure that all packages are pinpointed
|
|
||||||
cd ..
|
|
||||||
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
|
|
||||||
|
|
||||||
${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }}
|
|
||||||
|
|
||||||
# Expose the executables that were installed
|
|
||||||
cd ..
|
|
||||||
${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."}
|
|
||||||
|
|
||||||
mv ${packageName} lib
|
|
||||||
ln -s $out/lib/node_modules/.bin $out/bin
|
|
||||||
'';
|
|
||||||
} // extraArgs);
|
|
||||||
|
|
||||||
# Builds a development shell
|
|
||||||
buildNodeShell =
|
|
||||||
{ name
|
|
||||||
, packageName
|
|
||||||
, version ? null
|
|
||||||
, src
|
|
||||||
, dependencies ? []
|
|
||||||
, buildInputs ? []
|
|
||||||
, production ? true
|
|
||||||
, npmFlags ? ""
|
|
||||||
, dontNpmInstall ? false
|
|
||||||
, bypassCache ? false
|
|
||||||
, reconstructLock ? false
|
|
||||||
, dontStrip ? true
|
|
||||||
, unpackPhase ? "true"
|
|
||||||
, buildPhase ? "true"
|
|
||||||
, ... }@args:
|
|
||||||
|
|
||||||
let
|
|
||||||
nodeDependencies = buildNodeDependencies args;
|
|
||||||
extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "unpackPhase" "buildPhase" ];
|
|
||||||
in
|
|
||||||
stdenv.mkDerivation ({
|
|
||||||
name = "node-shell-${name}${if version == null then "" else "-${version}"}";
|
|
||||||
|
|
||||||
buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs;
|
|
||||||
buildCommand = ''
|
|
||||||
mkdir -p $out/bin
|
|
||||||
cat > $out/bin/shell <<EOF
|
|
||||||
#! ${stdenv.shell} -e
|
|
||||||
$shellHook
|
|
||||||
exec ${stdenv.shell}
|
|
||||||
EOF
|
|
||||||
chmod +x $out/bin/shell
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Provide the dependencies in a development shell through the NODE_PATH environment variable
|
|
||||||
inherit nodeDependencies;
|
|
||||||
shellHook = lib.optionalString (dependencies != []) ''
|
|
||||||
export NODE_PATH=${nodeDependencies}/lib/node_modules
|
|
||||||
export PATH="${nodeDependencies}/bin:$PATH"
|
|
||||||
'';
|
|
||||||
} // extraArgs);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
buildNodeSourceDist = lib.makeOverridable buildNodeSourceDist;
|
|
||||||
buildNodePackage = lib.makeOverridable buildNodePackage;
|
|
||||||
buildNodeDependencies = lib.makeOverridable buildNodeDependencies;
|
|
||||||
buildNodeShell = lib.makeOverridable buildNodeShell;
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
["prettier-plugin-tailwindcss"]
|
|
|
@ -1,3 +0,0 @@
|
||||||
[*.{js,jsx,ts,tsx,json}]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Because we depend on nixpkgs sources, uploading to builders takes a long time
|
|
||||||
|
|
||||||
source_up
|
|
||||||
|
|
||||||
files=(flake-module.nix package.json package-lock.json)
|
|
||||||
if type nix_direnv_watch_file &>/dev/null; then
|
|
||||||
nix_direnv_watch_file "${files[@]}"
|
|
||||||
else
|
|
||||||
watch_file "${files[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
use flake .#theme --builders ''
|
|
43
pkgs/theme/.gitignore
vendored
|
@ -1,43 +0,0 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# nix
|
|
||||||
.floco
|
|
||||||
src/fonts
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# Generated api code
|
|
||||||
openapi.json
|
|
||||||
api/
|
|
|
@ -1,41 +0,0 @@
|
||||||
{ floco
|
|
||||||
, system
|
|
||||||
, pkgs
|
|
||||||
, clanPkgs
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
|
|
||||||
lib = floco.lib;
|
|
||||||
|
|
||||||
pjs =
|
|
||||||
let
|
|
||||||
msg = "default.nix: Expected to find `package.json' to lookup " +
|
|
||||||
"package name/version, but no such file exists at: " +
|
|
||||||
(toString ./package.json);
|
|
||||||
in
|
|
||||||
if builtins.pathExists ./package.json then lib.importJSON ./package.json
|
|
||||||
else throw msg;
|
|
||||||
ident = pjs.name;
|
|
||||||
inherit (pjs) version;
|
|
||||||
|
|
||||||
|
|
||||||
fmod = lib.evalModules {
|
|
||||||
modules = [
|
|
||||||
floco.nixosModules.floco
|
|
||||||
{ config.floco.settings = { inherit system; basedir = ./.; }; }
|
|
||||||
./nix/floco-cfg.nix
|
|
||||||
];
|
|
||||||
specialArgs = {
|
|
||||||
inherit pkgs clanPkgs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# This attrset holds a few derivations related to our package.
|
|
||||||
# We'll expose these below to the CLI.
|
|
||||||
pkg = fmod.config.floco.packages.${ident}.${version};
|
|
||||||
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit pkg fmod;
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{ self, ... }:
|
|
||||||
{
|
|
||||||
perSystem = { self', pkgs, ... }:
|
|
||||||
let
|
|
||||||
inherit (self.inputs) floco;
|
|
||||||
base = pkgs.callPackage ./default.nix { inherit floco; clanPkgs = self'.packages; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = {
|
|
||||||
theme = base.pkg.global;
|
|
||||||
};
|
|
||||||
devShells.theme = pkgs.callPackage ./shell.nix {
|
|
||||||
inherit pkgs;
|
|
||||||
inherit (base) fmod pkg;
|
|
||||||
clanPkgs = self'.packages;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
# ============================================================================ #
|
|
||||||
#
|
|
||||||
# Aggregates configs making them available to `default.nix', `flake.nix',
|
|
||||||
# or other projects that want to consume this module/package as a dependency.
|
|
||||||
#
|
|
||||||
# ---------------------------------------------------------------------------- #
|
|
||||||
{
|
|
||||||
_file = "theme/nix/floco-cfg.nix";
|
|
||||||
imports =
|
|
||||||
let
|
|
||||||
ifExist = builtins.filter builtins.pathExists [
|
|
||||||
./pdefs.nix # Generated `pdefs.nix'
|
|
||||||
./foverrides.nix # Explicit config
|
|
||||||
];
|
|
||||||
in
|
|
||||||
ifExist
|
|
||||||
++ [
|
|
||||||
|
|
||||||
];
|
|
||||||
}
|
|
||||||
# ---------------------------------------------------------------------------- #
|
|
||||||
#
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# ============================================================================ #
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
{ lib, config, ... }:
|
|
||||||
let
|
|
||||||
pjs = lib.importJSON ../package.json;
|
|
||||||
ident = pjs.name;
|
|
||||||
inherit (pjs) version;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config.floco.packages.${ident}.${version} =
|
|
||||||
{
|
|
||||||
source = lib.libfloco.cleanLocalSource ../.;
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
{
|
|
||||||
floco = {
|
|
||||||
pdefs = {
|
|
||||||
"@clan/colors" = {
|
|
||||||
"1.0.0" = {
|
|
||||||
depInfo = {
|
|
||||||
"@material/material-color-utilities" = {
|
|
||||||
descriptor = "^0.2.6";
|
|
||||||
pin = "0.2.7";
|
|
||||||
};
|
|
||||||
"@types/node" = {
|
|
||||||
descriptor = "^20.3.2";
|
|
||||||
pin = "20.8.2";
|
|
||||||
};
|
|
||||||
typescript = {
|
|
||||||
descriptor = "^5.1.5";
|
|
||||||
pin = "5.2.2";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
fetchInfo = "path:..";
|
|
||||||
ident = "@clan/colors";
|
|
||||||
lifecycle = {
|
|
||||||
build = true;
|
|
||||||
};
|
|
||||||
ltype = "dir";
|
|
||||||
treeInfo = {
|
|
||||||
"node_modules/@material/material-color-utilities" = {
|
|
||||||
dev = true;
|
|
||||||
key = "@material/material-color-utilities/0.2.7";
|
|
||||||
};
|
|
||||||
"node_modules/@types/node" = {
|
|
||||||
dev = true;
|
|
||||||
key = "@types/node/20.8.2";
|
|
||||||
};
|
|
||||||
"node_modules/typescript" = {
|
|
||||||
dev = true;
|
|
||||||
key = "typescript/5.2.2";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
version = "1.0.0";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"@material/material-color-utilities" = {
|
|
||||||
"0.2.7" = {
|
|
||||||
fetchInfo = {
|
|
||||||
narHash = "sha256-hRYXqtkoXHoB30v1hstWz7dO7dNeBb6EJqZG66hHi94=";
|
|
||||||
type = "tarball";
|
|
||||||
url = "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz";
|
|
||||||
};
|
|
||||||
ident = "@material/material-color-utilities";
|
|
||||||
ltype = "file";
|
|
||||||
treeInfo = { };
|
|
||||||
version = "0.2.7";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
"@types/node" = {
|
|
||||||
"20.8.2" = {
|
|
||||||
fetchInfo = {
|
|
||||||
narHash = "sha256-o4hyob1kLnm0OE8Rngm0d6XJxobpMlYSoquusktmLPk=";
|
|
||||||
type = "tarball";
|
|
||||||
url = "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz";
|
|
||||||
};
|
|
||||||
ident = "@types/node";
|
|
||||||
ltype = "file";
|
|
||||||
treeInfo = { };
|
|
||||||
version = "20.8.2";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
typescript = {
|
|
||||||
"5.2.2" = {
|
|
||||||
binInfo = {
|
|
||||||
binPairs = {
|
|
||||||
tsc = "bin/tsc";
|
|
||||||
tsserver = "bin/tsserver";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
fetchInfo = {
|
|
||||||
narHash = "sha256-io9rXH9RLRLB0484ZdvcqblLQquLFUBGxDuwSixWxus=";
|
|
||||||
type = "tarball";
|
|
||||||
url = "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz";
|
|
||||||
};
|
|
||||||
ident = "typescript";
|
|
||||||
ltype = "file";
|
|
||||||
treeInfo = { };
|
|
||||||
version = "5.2.2";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
63
pkgs/theme/package-lock.json
generated
|
@ -1,63 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@clan/colors",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 2,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "@clan/colors",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"@material/material-color-utilities": "^0.2.6",
|
|
||||||
"@types/node": "^20.3.2",
|
|
||||||
"typescript": "^5.1.5"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@material/material-color-utilities": {
|
|
||||||
"version": "0.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz",
|
|
||||||
"integrity": "sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "20.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz",
|
|
||||||
"integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@material/material-color-utilities": {
|
|
||||||
"version": "0.2.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/@material/material-color-utilities/-/material-color-utilities-0.2.7.tgz",
|
|
||||||
"integrity": "sha512-0FCeqG6WvK4/Cc06F/xXMd/pv4FeisI0c1tUpBbfhA2n9Y8eZEv4Karjbmf2ZqQCPUWMrGp8A571tCjizxoTiQ==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"@types/node": {
|
|
||||||
"version": "20.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz",
|
|
||||||
"integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"typescript": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
|
|
||||||
"dev": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@clan/colors",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"type": "module",
|
|
||||||
"files": [
|
|
||||||
"colors.json"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"typecheck": "./node_modules/.bin/tsc -p ./tsconfig.json --noEmit",
|
|
||||||
"build": "tsc --build --clean && tsc && node ./build/main.js",
|
|
||||||
"html": "tsc --build --clean && tsc && node ./build/generate.js",
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"devDependencies": {
|
|
||||||
"@material/material-color-utilities": "^0.2.6",
|
|
||||||
"typescript": "^5.1.5",
|
|
||||||
"@types/node": "^20.3.2"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
{ fmod
|
|
||||||
, pkg
|
|
||||||
, pkgs
|
|
||||||
, clanPkgs
|
|
||||||
}:
|
|
||||||
pkgs.mkShell {
|
|
||||||
buildInputs = [
|
|
||||||
fmod.config.floco.settings.nodePackage
|
|
||||||
];
|
|
||||||
shellHook = ''
|
|
||||||
ID=${pkg.built.tree}
|
|
||||||
currID=$(cat .floco/.node_modules_id 2> /dev/null)
|
|
||||||
|
|
||||||
mkdir -p .floco
|
|
||||||
if [[ "$ID" != "$currID" || ! -d "node_modules" ]];
|
|
||||||
then
|
|
||||||
${pkgs.rsync}/bin/rsync -a --chmod=ug+w --delete ${pkg.built.tree}/node_modules/ ./node_modules/
|
|
||||||
echo -n $ID > .floco/.node_modules_id
|
|
||||||
echo "floco ok: node_modules updated"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PATH="$PATH:$(realpath ./node_modules)/.bin"
|
|
||||||
'';
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { AliasMap, BaseColors, HexString } from "./types.js";
|
|
||||||
|
|
||||||
export type PaletteConfig = {
|
|
||||||
baseColors: BaseColors;
|
|
||||||
tones: number[];
|
|
||||||
aliases: AliasMap<"primary" | "secondary" | "error">;
|
|
||||||
common: {
|
|
||||||
// Black and white is always constant
|
|
||||||
// We declare this on the type level
|
|
||||||
white: "#ffffff";
|
|
||||||
black: "#000000";
|
|
||||||
// Some other color constants/reservation
|
|
||||||
[id: string]: HexString;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const config: PaletteConfig = {
|
|
||||||
/** All color shades that are available
|
|
||||||
* This colors are used as "key colors" to generate a tonal palette from 0 to 100
|
|
||||||
* Steps are defined in 'tones'
|
|
||||||
*/
|
|
||||||
baseColors: {
|
|
||||||
neutral: {
|
|
||||||
keyColor: "#808080",
|
|
||||||
tones: [2, 5, 8, 92, 95, 98],
|
|
||||||
},
|
|
||||||
green: {
|
|
||||||
keyColor: "#7AC51B",
|
|
||||||
tones: [2, 98],
|
|
||||||
},
|
|
||||||
yellow: {
|
|
||||||
keyColor: "#E0E01F",
|
|
||||||
tones: [2, 98],
|
|
||||||
},
|
|
||||||
purple: {
|
|
||||||
keyColor: "#661bc5",
|
|
||||||
tones: [2, 98],
|
|
||||||
},
|
|
||||||
|
|
||||||
red: {
|
|
||||||
keyColor: "#e82439",
|
|
||||||
tones: [95],
|
|
||||||
},
|
|
||||||
blue: {
|
|
||||||
keyColor: "#1B7AC5",
|
|
||||||
tones: [1, 2, 3, 5, 95, 98],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Common tones to generate out of all the baseColors
|
|
||||||
* number equals to the amount of light present in the color (HCT Color Space)
|
|
||||||
*/
|
|
||||||
tones: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100],
|
|
||||||
|
|
||||||
/** create aliases from the color palette
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
*
|
|
||||||
* primary: "blue"
|
|
||||||
* ->
|
|
||||||
* ...
|
|
||||||
* primary40 -> blue40
|
|
||||||
* primary50 -> blue50
|
|
||||||
* ...
|
|
||||||
*/
|
|
||||||
aliases: {
|
|
||||||
primary: "purple",
|
|
||||||
secondary: "green",
|
|
||||||
error: "red",
|
|
||||||
},
|
|
||||||
/** some color names are reserved
|
|
||||||
* typically those colors do not change when switching theme
|
|
||||||
* or are other types of constant in the UI
|
|
||||||
*/
|
|
||||||
common: {
|
|
||||||
white: "#ffffff",
|
|
||||||
black: "#000000",
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { writeFile } from "fs";
|
|
||||||
import palette from "./colors.json" assert { type: "json" };
|
|
||||||
import { config } from "./config.js";
|
|
||||||
|
|
||||||
type PaletteFile = typeof palette;
|
|
||||||
|
|
||||||
const html = (palette: PaletteFile): string => {
|
|
||||||
const colors = Object.keys(config.baseColors).map((baseName) => {
|
|
||||||
const colors = Object.entries(palette.ref.palette)
|
|
||||||
.filter(([name, _]) => name.includes(baseName))
|
|
||||||
.sort((a, b) => {
|
|
||||||
return a[1].meta.color.shade - b[1].meta.color.shade;
|
|
||||||
})
|
|
||||||
.map(([key, color]) => {
|
|
||||||
console.log({ key, color });
|
|
||||||
return `<div style="background-color:${color.value}; color:${
|
|
||||||
color.meta.color.shade < 48 ? "#fff" : "#000"
|
|
||||||
}; height: 10rem; border:solid 1px grey; display:grid; place-items:end;">${key}</div>`;
|
|
||||||
});
|
|
||||||
return `<div style="display: grid; grid-template-columns: repeat(${13}, minmax(0, 1fr)); gap: 1rem; margin-bottom: 1rem">${colors.join(
|
|
||||||
"\n",
|
|
||||||
)}</div>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Page Title</title>
|
|
||||||
<style>
|
|
||||||
</style>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
${colors.join("\n")}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
writeFile("index.html", html(palette), (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error({ err });
|
|
||||||
} else {
|
|
||||||
console.log("Exported colors to html");
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,182 +0,0 @@
|
||||||
#!usr/bin/node
|
|
||||||
import * as fs from "fs";
|
|
||||||
import {
|
|
||||||
argbFromHex,
|
|
||||||
Hct,
|
|
||||||
hexFromArgb,
|
|
||||||
} from "@material/material-color-utilities";
|
|
||||||
import {
|
|
||||||
AliasTokenMap,
|
|
||||||
ColorDesignToken,
|
|
||||||
ColorSet,
|
|
||||||
HexString,
|
|
||||||
RefTokenSystem,
|
|
||||||
TonalPalette,
|
|
||||||
TonalPaletteConfig,
|
|
||||||
TonalPaletteItem,
|
|
||||||
} from "./types.js";
|
|
||||||
import { config } from "./config.js";
|
|
||||||
|
|
||||||
const { baseColors, tones, aliases, common } = config;
|
|
||||||
|
|
||||||
/** Takes a color, tone and name
|
|
||||||
* If a tone is given adjust the lightning level accordingly
|
|
||||||
*
|
|
||||||
* @returns TonalPaletteItem (meta wrapper around HCT)
|
|
||||||
*/
|
|
||||||
const getTonalPaletteItem = (
|
|
||||||
value: HexString,
|
|
||||||
name: string,
|
|
||||||
tone?: number,
|
|
||||||
): TonalPaletteItem => {
|
|
||||||
const aRGB = argbFromHex(value);
|
|
||||||
const color = Hct.fromInt(aRGB);
|
|
||||||
if (tone !== undefined) {
|
|
||||||
color.tone = tone;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
shade: color.tone,
|
|
||||||
name: `${name || color.chroma}${Math.round(color.tone)}`,
|
|
||||||
baseName: name,
|
|
||||||
value: color,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/** create a flat list of the cross product from all colors and all tones.
|
|
||||||
*
|
|
||||||
* every color is mapped in the range from 0 to 100
|
|
||||||
* with the steps configure in `config.tones'
|
|
||||||
* additionally the key color is added unmodified
|
|
||||||
* lightning levels are rounded to the next natural number to form the 'name'
|
|
||||||
* Example:
|
|
||||||
*
|
|
||||||
* "blue" x [20.1, 30.3]
|
|
||||||
* ->
|
|
||||||
* [blue20, blue30]
|
|
||||||
*/
|
|
||||||
const mkTonalPalette =
|
|
||||||
(config: TonalPaletteConfig) =>
|
|
||||||
(name: string) =>
|
|
||||||
(keyTone: HexString): TonalPalette => {
|
|
||||||
const { tones } = config;
|
|
||||||
const aRGB = argbFromHex(keyTone);
|
|
||||||
const HctColor = Hct.fromInt(aRGB);
|
|
||||||
const roundedTone = Math.round(HctColor.tone * 100) / 100;
|
|
||||||
|
|
||||||
const localTones = [...tones, roundedTone];
|
|
||||||
|
|
||||||
return localTones.map((t) => getTonalPaletteItem(keyTone, name, t));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a PaletteItem into a hex color. (Wrapped)
|
|
||||||
* Adding meta attributes which avoids any information loss.
|
|
||||||
*/
|
|
||||||
const toDesignTokenContent = (color: TonalPaletteItem): ColorDesignToken => {
|
|
||||||
const { value } = color;
|
|
||||||
return {
|
|
||||||
type: "color",
|
|
||||||
value: hexFromArgb(value.toInt()),
|
|
||||||
meta: {
|
|
||||||
color,
|
|
||||||
date: new Date(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const color: ColorSet = Object.entries(baseColors)
|
|
||||||
.map(([name, baseColor]) => ({
|
|
||||||
name,
|
|
||||||
baseColor,
|
|
||||||
tones: mkTonalPalette({
|
|
||||||
tones: [...tones, ...baseColor.tones].sort((a, b) => a - b),
|
|
||||||
})(name)(baseColor.keyColor),
|
|
||||||
}))
|
|
||||||
.reduce((acc, curr) => {
|
|
||||||
let currTones = curr.tones.reduce(
|
|
||||||
(o, v) => ({
|
|
||||||
...o,
|
|
||||||
[v.name]: toDesignTokenContent(v),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...acc,
|
|
||||||
...currTones,
|
|
||||||
};
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
/** Generate a set of tokens from a given alias mapping
|
|
||||||
*
|
|
||||||
* @param alias A string e.g. Primary -> Blue (Primary is the alias)
|
|
||||||
* @param name A string; Basename of the referenced value (e.g. Blue)
|
|
||||||
* @param colors A set of colors
|
|
||||||
* @returns All aliases from the given color set
|
|
||||||
*/
|
|
||||||
function resolveAlias(
|
|
||||||
alias: string,
|
|
||||||
name: string,
|
|
||||||
colors: ColorSet,
|
|
||||||
): AliasTokenMap {
|
|
||||||
// All colors from the color map belonging to that single alias
|
|
||||||
// Example:
|
|
||||||
// Primary -> "blue"
|
|
||||||
// =>
|
|
||||||
// [ (blue0) , (blue10) , ..., (blue100) ]
|
|
||||||
const all = Object.values(colors)
|
|
||||||
.filter((n) => n.meta.color.name.includes(name))
|
|
||||||
.filter((n) => !n.meta.color.name.includes("."));
|
|
||||||
|
|
||||||
const tokens = all
|
|
||||||
.map((shade) => {
|
|
||||||
const shadeNumber = shade.meta.color.shade;
|
|
||||||
return {
|
|
||||||
name: `${alias}${Math.round(shadeNumber)}`,
|
|
||||||
value: { value: `{ref.palette.${shade.meta.color.name}}` },
|
|
||||||
// propagate the meta attribute of the actual value
|
|
||||||
meta: shade.meta,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
// sort by tone
|
|
||||||
.sort((a, b) => a.meta.color.value.tone - b.meta.color.value.tone)
|
|
||||||
.reduce((acc, { name, value }) => ({ ...acc, [name]: value }), {});
|
|
||||||
return tokens;
|
|
||||||
}
|
|
||||||
|
|
||||||
const aliasMap = Object.entries(aliases).reduce(
|
|
||||||
(prev, [key, value]) => ({
|
|
||||||
...prev,
|
|
||||||
...resolveAlias(key, value, color),
|
|
||||||
}),
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonColors = Object.entries(common)
|
|
||||||
.map(([name, value]) =>
|
|
||||||
toDesignTokenContent(getTonalPaletteItem(value, name)),
|
|
||||||
)
|
|
||||||
.reduce(
|
|
||||||
(acc, val) => ({ ...acc, [val.meta.color.baseName]: val }),
|
|
||||||
{},
|
|
||||||
) as ColorSet;
|
|
||||||
|
|
||||||
const toPaletteToken = (color: ColorSet): RefTokenSystem => ({
|
|
||||||
ref: {
|
|
||||||
palette: color,
|
|
||||||
alias: aliasMap,
|
|
||||||
common: commonColors,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Dump tokens to json file
|
|
||||||
fs.writeFile(
|
|
||||||
"colors.json",
|
|
||||||
JSON.stringify(toPaletteToken(color), null, 2),
|
|
||||||
(err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error({ err });
|
|
||||||
} else {
|
|
||||||
console.log("tokens successfully exported");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
|
@ -1,90 +0,0 @@
|
||||||
import { Hct } from "@material/material-color-utilities";
|
|
||||||
|
|
||||||
export type BaseColors = {
|
|
||||||
neutral: BaseColor;
|
|
||||||
red: BaseColor;
|
|
||||||
green: BaseColor;
|
|
||||||
yellow: BaseColor;
|
|
||||||
purple: BaseColor;
|
|
||||||
blue: BaseColor;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BaseColor = {
|
|
||||||
keyColor: HexString;
|
|
||||||
tones: number[];
|
|
||||||
follows?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ColorSet = { [key: string]: ColorDesignToken };
|
|
||||||
|
|
||||||
/** The resolved alias tokens
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* {
|
|
||||||
* primary: "blue"
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export type AliasMap<T extends string> = {
|
|
||||||
[alias in T]: keyof BaseColors;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** The resolved alias tokens
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* {
|
|
||||||
* primary0: "blue40"
|
|
||||||
* primary10: "blue40"
|
|
||||||
* ...
|
|
||||||
* primary100: "blue100"
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Unfortunately My Typescript skills lack the ability to express this type any narrower :/
|
|
||||||
*/
|
|
||||||
export type AliasTokenMap = {
|
|
||||||
[alias: string]: { value: string };
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TonalPaletteConfig = {
|
|
||||||
tones: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type HexString = string;
|
|
||||||
|
|
||||||
export type TonalPaletteItem = {
|
|
||||||
/**
|
|
||||||
* @example
|
|
||||||
* 20
|
|
||||||
*/
|
|
||||||
shade: number;
|
|
||||||
/**
|
|
||||||
* @example
|
|
||||||
* "blue20"
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* @example
|
|
||||||
* "blue"
|
|
||||||
*/
|
|
||||||
baseName: string;
|
|
||||||
value: Hct;
|
|
||||||
};
|
|
||||||
export type TonalPalette = TonalPaletteItem[];
|
|
||||||
|
|
||||||
export type ColorDesignToken = {
|
|
||||||
type: "color";
|
|
||||||
value: HexString;
|
|
||||||
meta: {
|
|
||||||
color: TonalPaletteItem;
|
|
||||||
date: Date;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RefTokenSystem = {
|
|
||||||
ref: {
|
|
||||||
palette: ColorSet;
|
|
||||||
common: ColorSet;
|
|
||||||
alias: AliasTokenMap;
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"include": ["src"],
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
|
||||||
|
|
||||||
"lib": [
|
|
||||||
"ESNext",
|
|
||||||
"dom"
|
|
||||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
|
||||||
"module": "NodeNext" /* Specify what module code is generated. */,
|
|
||||||
"rootDir": "src" /* Specify the root folder within your source files. */,
|
|
||||||
"moduleResolution": "nodenext" /* Specify how TypeScript looks up a file from a given module specifier. */,
|
|
||||||
"resolveJsonModule": true /* Enable importing .json files. */,
|
|
||||||
"outDir": "build",
|
|
||||||
|
|
||||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
|
||||||
|
|
||||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
|
||||||
|
|
||||||
/* Type Checking */
|
|
||||||
"strict": true /* Enable all strict type-checking options. */,
|
|
||||||
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
|
|
||||||
"strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
|
|
||||||
"strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */,
|
|
||||||
"strictBindCallApply": true /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */,
|
|
||||||
"strictPropertyInitialization": true /* Check for class properties that are declared but not set in the constructor. */,
|
|
||||||
"noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */,
|
|
||||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
|
||||||
"alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
|
|
||||||
"noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
|
|
||||||
"noUnusedParameters": true /* Raise an error when a function parameter isn't read. */,
|
|
||||||
"exactOptionalPropertyTypes": true /* Interpret optional property types as written, rather than adding 'undefined'. */,
|
|
||||||
"noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */,
|
|
||||||
"noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
|
|
||||||
"noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */,
|
|
||||||
"noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */,
|
|
||||||
"noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type. */
|
|
||||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
|
||||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
# Because we depend on nixpkgs sources, uploading to builders takes a long time
|
|
||||||
|
|
||||||
source_up
|
|
||||||
|
|
||||||
files=(../../flake.nix ../theme default.nix flake-module.nix package.json package-lock.json)
|
|
||||||
if type nix_direnv_watch_file &>/dev/null; then
|
|
||||||
nix_direnv_watch_file "${files[@]}"
|
|
||||||
else
|
|
||||||
watch_file "${files[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
use flake .#ui --builders ''
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"extends": ["next/core-web-vitals", "plugin:tailwindcss/recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"ignorePatterns": ["**/src/api/*"],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off"
|
|
||||||
}
|
|
||||||
}
|
|
43
pkgs/ui/.gitignore
vendored
|
@ -1,43 +0,0 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# nix
|
|
||||||
.floco
|
|
||||||
src/fonts
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
|
|
||||||
# Generated api code
|
|
||||||
openapi.json
|
|
||||||
api/
|
|
|
@ -1,27 +0,0 @@
|
||||||
# cLan - awesome UI
|
|
||||||
|
|
||||||
## Updating dependencies
|
|
||||||
|
|
||||||
After changing dependencies with
|
|
||||||
|
|
||||||
`npm <dep> i --package-lock-only`
|
|
||||||
|
|
||||||
Update floco dependencies:
|
|
||||||
|
|
||||||
`nix run github:aakropotkin/floco -- translate -pt -o ./nix/pdefs.nix`
|
|
||||||
|
|
||||||
The prettier tailwind class sorting is not yet working properly with our devShell integration.
|
|
||||||
|
|
||||||
To sort classnames manually:
|
|
||||||
|
|
||||||
`cd /clan-core/pkgs/ui/`
|
|
||||||
|
|
||||||
## Upload ui to gitea
|
|
||||||
|
|
||||||
Create a gitea token here: https://git.clan.lol/user/settings/applications
|
|
||||||
|
|
||||||
Than run this command:
|
|
||||||
|
|
||||||
```
|
|
||||||
GITEA_TOKEN=<YOUR_TOKEN> nix run .#update-ui-assets
|
|
||||||
```
|
|
|
@ -1,38 +0,0 @@
|
||||||
{ floco
|
|
||||||
, system
|
|
||||||
, pkgs
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
|
|
||||||
lib = floco.lib;
|
|
||||||
|
|
||||||
pjs =
|
|
||||||
let
|
|
||||||
msg = "default.nix: Expected to find `package.json' to lookup " +
|
|
||||||
"package name/version, but no such file exists at: " +
|
|
||||||
(toString ./package.json);
|
|
||||||
in
|
|
||||||
if builtins.pathExists ./package.json then lib.importJSON ./package.json
|
|
||||||
else throw msg;
|
|
||||||
ident = pjs.name;
|
|
||||||
inherit (pjs) version;
|
|
||||||
|
|
||||||
|
|
||||||
fmod = lib.evalModules {
|
|
||||||
modules = [
|
|
||||||
floco.nixosModules.floco
|
|
||||||
{ config.floco.settings = { inherit system; basedir = ./.; }; }
|
|
||||||
./nix/floco-cfg.nix
|
|
||||||
];
|
|
||||||
specialArgs = { inherit pkgs; };
|
|
||||||
};
|
|
||||||
|
|
||||||
# This attrset holds a few derivations related to our package.
|
|
||||||
# We'll expose these below to the CLI.
|
|
||||||
pkg = fmod.config.floco.packages.${ident}.${version};
|
|
||||||
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit pkg fmod;
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
{ self, ... }:
|
|
||||||
{
|
|
||||||
perSystem = { pkgs, ... }:
|
|
||||||
let
|
|
||||||
inherit (self.inputs) floco;
|
|
||||||
base = pkgs.callPackage ./default.nix { inherit floco; };
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = {
|
|
||||||
ui = base.pkg.global;
|
|
||||||
ui-assets = pkgs.callPackage ./nix/ui-assets.nix { };
|
|
||||||
# EXAMPLE: GITEA_TOKEN=$(rbw get -f GITEA_TOKEN git.clan.lol) nix run .#update-ui-assets
|
|
||||||
update-ui-assets = pkgs.callPackage ./nix/update-ui-assets.nix { };
|
|
||||||
};
|
|
||||||
devShells.ui = pkgs.callPackage ./shell.nix {
|
|
||||||
inherit pkgs;
|
|
||||||
inherit (base) fmod pkg;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
output: "export",
|
|
||||||
images: { unoptimized: true },
|
|
||||||
eslint: {
|
|
||||||
dirs: ["src"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = nextConfig;
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./pdefs.nix
|
|
||||||
./foverrides.nix
|
|
||||||
../../theme/nix/floco-cfg.nix
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,114 +0,0 @@
|
||||||
{ lib, config, pkgs, ... }:
|
|
||||||
let
|
|
||||||
|
|
||||||
pjs =
|
|
||||||
let
|
|
||||||
msg = "foverrides.nix: Expected to find `package.json' to lookup " +
|
|
||||||
"package name/version, but no such file exists at: " +
|
|
||||||
(toString ../package.json);
|
|
||||||
in
|
|
||||||
if builtins.pathExists ../package.json then lib.importJSON ../package.json
|
|
||||||
else throw msg;
|
|
||||||
ident = pjs.name;
|
|
||||||
inherit (pjs) version;
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
# example: "13.4.2"
|
|
||||||
nextVersion = builtins.head (builtins.attrNames (lib.filterAttrs (name: _attrs: name == "next") config.floco.pdefs).next);
|
|
||||||
|
|
||||||
# we must change the precompiled swc binary depending on the curerent system.
|
|
||||||
# example: "@next/swc-linux-x64-gnu"
|
|
||||||
swcArch = {
|
|
||||||
"x86_64-linux" = "@next/swc-linux-x64-gnu";
|
|
||||||
"aarch64-linux" = "@next/swc-linux-arm64-gnu";
|
|
||||||
|
|
||||||
"x86_64-darwin" = "@next/swc-darwin-x64";
|
|
||||||
"aarch64-darwin" = "@next/swc-darwin-arm64";
|
|
||||||
}.${config.floco.settings.system};
|
|
||||||
|
|
||||||
esbuildVersions = lib.filterAttrs (name: _attrs: name == "esbuild") config.floco.pdefs;
|
|
||||||
highestEsbuildVersion = lib.last (builtins.attrNames esbuildVersions.esbuild);
|
|
||||||
|
|
||||||
esbuildArch = {
|
|
||||||
# esbuild-linux-64
|
|
||||||
"x86_64-linux" = "esbuild-linux-64";
|
|
||||||
"x86_64-darwin" = "esbuild-darwin-64";
|
|
||||||
"aarch64-darwin" = "esbuild-darwin-arm64";
|
|
||||||
"aarch64-linux" = "esbuild-linux-arm64";
|
|
||||||
}.${config.floco.settings.system};
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config.floco.packages.esbuild =
|
|
||||||
builtins.mapAttrs
|
|
||||||
(
|
|
||||||
version: _attrs: {
|
|
||||||
installed.override.copyTree = true;
|
|
||||||
installed.tree = config.floco.packages.${esbuildArch}.${version}.global;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
esbuildVersions.esbuild;
|
|
||||||
# ---------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
config.floco.packages.${ident}.${version} =
|
|
||||||
let
|
|
||||||
cfg = config.floco.packages.${ident}.${version};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
# Removes any `*.nix' files as well as `node_modules/' and
|
|
||||||
# `package-lock.json' from the source tree before using them in builds.
|
|
||||||
source = lib.libfloco.cleanLocalSource ../.;
|
|
||||||
|
|
||||||
|
|
||||||
# nextjs writes in node_mdules
|
|
||||||
built.override.copyTree = true;
|
|
||||||
|
|
||||||
# nextjs chaches some stuff in $HOME
|
|
||||||
built.override.preBuild = ''
|
|
||||||
export HOME=./home
|
|
||||||
|
|
||||||
|
|
||||||
echo "----------- GENERATE API TS ------------"
|
|
||||||
cp ${../../clan-cli/clan_cli/webui/openapi.json} openapi.json
|
|
||||||
./node_modules/.bin/orval
|
|
||||||
|
|
||||||
ln -fs ${pkgs.roboto}/share/fonts ./src/
|
|
||||||
|
|
||||||
echo "----------- RUNNING LINT ------------"
|
|
||||||
next lint --max-warnings 0
|
|
||||||
'';
|
|
||||||
|
|
||||||
built.tree =
|
|
||||||
let
|
|
||||||
customOverrides = cfg.trees.dev.overrideAttrs (prev: {
|
|
||||||
treeInfo =
|
|
||||||
prev.treeInfo
|
|
||||||
// {
|
|
||||||
"node_modules/${swcArch}" = {
|
|
||||||
key = "${swcArch}/${nextVersion}";
|
|
||||||
link = false;
|
|
||||||
optional = false;
|
|
||||||
dev = true;
|
|
||||||
};
|
|
||||||
"node_modules/${esbuildArch}" = {
|
|
||||||
key = "${esbuildArch}/${highestEsbuildVersion}";
|
|
||||||
link = false;
|
|
||||||
optional = false;
|
|
||||||
dev = true;
|
|
||||||
};
|
|
||||||
"node_modules/@clan/colors" = {
|
|
||||||
key = "@clan/colors/1.0.0";
|
|
||||||
link = false;
|
|
||||||
optional = false;
|
|
||||||
dev = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
in
|
|
||||||
lib.mkForce customOverrides;
|
|
||||||
};
|
|
||||||
}
|
|
18441
pkgs/ui/nix/pdefs.nix
|
@ -1,5 +0,0 @@
|
||||||
{ fetchzip }:
|
|
||||||
fetchzip {
|
|
||||||
url = "https://git.clan.lol/api/packages/clan/generic/ui/0w48mjn2gdd102p3r875hcd0lbm5hrzk1jy01r637sy733qzk32j/assets.tar.gz";
|
|
||||||
sha256 = "0w48mjn2gdd102p3r875hcd0lbm5hrzk1jy01r637sy733qzk32j";
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
{ writeShellApplication
|
|
||||||
, curl
|
|
||||||
, nix
|
|
||||||
, gnutar
|
|
||||||
, gitMinimal
|
|
||||||
, coreutils
|
|
||||||
}:
|
|
||||||
writeShellApplication {
|
|
||||||
name = "update-ui-assets";
|
|
||||||
runtimeInputs = [
|
|
||||||
curl
|
|
||||||
nix
|
|
||||||
gnutar
|
|
||||||
gitMinimal
|
|
||||||
coreutils
|
|
||||||
];
|
|
||||||
text = builtins.readFile ./update-ui-assets.sh;
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
# shellcheck shell=bash
|
|
||||||
set -xeuo pipefail
|
|
||||||
|
|
||||||
# GITEA_TOKEN
|
|
||||||
if [[ -z "${GITEA_TOKEN:-}" ]]; then
|
|
||||||
echo "GITEA_TOKEN is not set"
|
|
||||||
echo "Go to https://git.clan.lol/user/settings/applications and generate a token"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
DEPS=$(nix shell --inputs-from '.#' "nixpkgs#gnutar" "nixpkgs#curl" "nixpkgs#gzip" -c bash -c "echo \$PATH")
|
|
||||||
export PATH=$PATH:$DEPS
|
|
||||||
|
|
||||||
|
|
||||||
PROJECT_DIR=$(git rev-parse --show-toplevel)
|
|
||||||
tmpdir=$(mktemp -d)
|
|
||||||
cleanup() { rm -rf "$tmpdir"; }
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
nix build '.#ui' --out-link "$tmpdir/result"
|
|
||||||
|
|
||||||
tar --transform 's,^\.,assets,' -czvf "$tmpdir/assets.tar.gz" -C "$tmpdir"/result/lib/node_modules/*/out .
|
|
||||||
NAR_HASH=$(nix-prefetch-url --unpack file://<(cat "$tmpdir/assets.tar.gz"))
|
|
||||||
|
|
||||||
|
|
||||||
url="https://git.clan.lol/api/packages/clan/generic/ui/$NAR_HASH/assets.tar.gz"
|
|
||||||
set +x
|
|
||||||
curl --upload-file "$tmpdir/assets.tar.gz" -X PUT "$url?token=$GITEA_TOKEN"
|
|
||||||
set -x
|
|
||||||
|
|
||||||
TEST_URL=$(nix-prefetch-url --unpack "$url")
|
|
||||||
if [[ $TEST_URL != "$NAR_HASH" ]]; then
|
|
||||||
echo "Prefetch failed. Expected $NAR_HASH, got $TEST_URL"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
cat > "$PROJECT_DIR/pkgs/ui/nix/ui-assets.nix" <<EOF
|
|
||||||
{ fetchzip }:
|
|
||||||
fetchzip {
|
|
||||||
url = "$url";
|
|
||||||
sha256 = "$NAR_HASH";
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
const config = {
|
|
||||||
clan: {
|
|
||||||
output: {
|
|
||||||
mode: "tags-split",
|
|
||||||
target: "src/api",
|
|
||||||
schemas: "src/api/model",
|
|
||||||
client: "swr",
|
|
||||||
// mock: true,
|
|
||||||
},
|
|
||||||
input: {
|
|
||||||
target: "./openapi.json",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
8366
pkgs/ui/package-lock.json
generated
|
@ -1,53 +0,0 @@
|
||||||
{
|
|
||||||
"name": "clan-ui",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": false,
|
|
||||||
"files": [
|
|
||||||
"./out"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@emotion/react": "^11.11.1",
|
|
||||||
"@emotion/styled": "^11.11.0",
|
|
||||||
"@mui/icons-material": "^5.14.3",
|
|
||||||
"@mui/material": "^5.14.3",
|
|
||||||
"@rjsf/core": "^5.12.1",
|
|
||||||
"@rjsf/mui": "^5.12.1",
|
|
||||||
"@rjsf/validator-ajv8": "^5.12.1",
|
|
||||||
"@types/json-schema": "^7.0.12",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
|
||||||
"autoprefixer": "10.4.14",
|
|
||||||
"axios": "^1.4.0",
|
|
||||||
"classnames": "^2.3.2",
|
|
||||||
"hex-rgb": "^5.0.0",
|
|
||||||
"next": "13.4.12",
|
|
||||||
"postcss": "8.4.27",
|
|
||||||
"pretty-bytes": "^6.1.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hook-form": "^7.45.4",
|
|
||||||
"react-hot-toast": "^2.4.1",
|
|
||||||
"recharts": "^2.7.3",
|
|
||||||
"swr": "^2.2.1",
|
|
||||||
"tailwindcss": "3.3.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "20.4.7",
|
|
||||||
"@types/react": "18.2.18",
|
|
||||||
"@types/react-dom": "18.2.7",
|
|
||||||
"@types/w3c-web-usb": "^1.0.6",
|
|
||||||
"esbuild": "^0.15.18",
|
|
||||||
"eslint": "^8.46.0",
|
|
||||||
"eslint-config-next": "13.4.12",
|
|
||||||
"eslint-plugin-tailwindcss": "^3.13.0",
|
|
||||||
"orval": "^6.17.0",
|
|
||||||
"prettier": "^3.0.1",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.4.1",
|
|
||||||
"typescript": "5.1.6"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M255.9 120.9l9.1-15.7c5.6-9.8 18.1-13.1 27.9-7.5 9.8 5.6 13.1 18.1 7.5 27.9l-87.5 151.5h63.3c20.5 0 32 24.1 23.1 40.8H113.8c-11.3 0-20.4-9.1-20.4-20.4 0-11.3 9.1-20.4 20.4-20.4h52l66.6-115.4-20.8-36.1c-5.6-9.8-2.3-22.2 7.5-27.9 9.8-5.6 22.2-2.3 27.9 7.5l8.9 15.7zm-78.7 218l-19.6 34c-5.6 9.8-18.1 13.1-27.9 7.5-9.8-5.6-13.1-18.1-7.5-27.9l14.6-25.2c16.4-5.1 29.8-1.2 40.4 11.6zm168.9-61.7h53.1c11.3 0 20.4 9.1 20.4 20.4 0 11.3-9.1 20.4-20.4 20.4h-29.5l19.9 34.5c5.6 9.8 2.3 22.2-7.5 27.9-9.8 5.6-22.2 2.3-27.9-7.5-33.5-58.1-58.7-101.6-75.4-130.6-17.1-29.5-4.9-59.1 7.2-69.1 13.4 23 33.4 57.7 60.1 104zM256 8C119 8 8 119 8 256s111 248 248 248 248-111 248-248S393 8 256 8zm216 248c0 118.7-96.1 216-216 216-118.7 0-216-96.1-216-216 0-118.7 96.1-216 216-216 118.7 0 216 96.1 216 216z"/></svg>
|
|
Before Width: | Height: | Size: 1.0 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M96 48L82.7 61.3C70.7 73.3 64 89.5 64 106.5V238.9c0 10.7 5.3 20.7 14.2 26.6l10.6 7c14.3 9.6 32.7 10.7 48.1 3l3.2-1.6c2.6-1.3 5-2.8 7.3-4.5l49.4-37c6.6-5 15.7-5 22.3 0c10.2 7.7 9.9 23.1-.7 30.3L90.4 350C73.9 361.3 64 380 64 400H384l28.9-159c2.1-11.3 3.1-22.8 3.1-34.3V192C416 86 330 0 224 0H83.8C72.9 0 64 8.9 64 19.8c0 7.5 4.2 14.3 10.9 17.7L96 48zm24 68a20 20 0 1 1 40 0 20 20 0 1 1 -40 0zM22.6 473.4c-4.2 4.2-6.6 10-6.6 16C16 501.9 26.1 512 38.6 512H409.4c12.5 0 22.6-10.1 22.6-22.6c0-6-2.4-11.8-6.6-16L384 432H64L22.6 473.4z"/></svg>
|
|
Before Width: | Height: | Size: 775 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>
|
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 416 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M397.9 160H256V19.6L397.9 160zM304 192v130c0 66.8-36.5 100.1-113.3 100.1H96V84.8h94.7c12 0 23.1.8 33.1 2.5v-84C212.9 1.1 201.4 0 189.2 0H0v512h189.2C329.7 512 400 447.4 400 318.1V192h-96z"/></svg>
|
|
Before Width: | Height: | Size: 435 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M503.52,241.48c-.12-1.56-.24-3.12-.24-4.68v-.12l-.36-4.68v-.12a245.86,245.86,0,0,0-7.32-41.15c0-.12,0-.12-.12-.24l-1.08-4c-.12-.24-.12-.48-.24-.6-.36-1.2-.72-2.52-1.08-3.72-.12-.24-.12-.6-.24-.84-.36-1.2-.72-2.4-1.08-3.48-.12-.36-.24-.6-.36-1-.36-1.2-.72-2.28-1.2-3.48l-.36-1.08c-.36-1.08-.84-2.28-1.2-3.36a8.27,8.27,0,0,0-.36-1c-.48-1.08-.84-2.28-1.32-3.36-.12-.24-.24-.6-.36-.84-.48-1.2-1-2.28-1.44-3.48,0-.12-.12-.24-.12-.36-1.56-3.84-3.24-7.68-5-11.4l-.36-.72c-.48-1-.84-1.8-1.32-2.64-.24-.48-.48-1.08-.72-1.56-.36-.84-.84-1.56-1.2-2.4-.36-.6-.6-1.2-1-1.8s-.84-1.44-1.2-2.28c-.36-.6-.72-1.32-1.08-1.92s-.84-1.44-1.2-2.16a18.07,18.07,0,0,0-1.2-2c-.36-.72-.84-1.32-1.2-2s-.84-1.32-1.2-2-.84-1.32-1.2-1.92-.84-1.44-1.32-2.16a15.63,15.63,0,0,0-1.2-1.8L463.2,119a15.63,15.63,0,0,0-1.2-1.8c-.48-.72-1.08-1.56-1.56-2.28-.36-.48-.72-1.08-1.08-1.56l-1.8-2.52c-.36-.48-.6-.84-1-1.32-1-1.32-1.8-2.52-2.76-3.72a248.76,248.76,0,0,0-23.51-26.64A186.82,186.82,0,0,0,412,62.46c-4-3.48-8.16-6.72-12.48-9.84a162.49,162.49,0,0,0-24.6-15.12c-2.4-1.32-4.8-2.52-7.2-3.72a254,254,0,0,0-55.43-19.56c-1.92-.36-3.84-.84-5.64-1.2h-.12c-1-.12-1.8-.36-2.76-.48a236.35,236.35,0,0,0-38-4H255.14a234.62,234.62,0,0,0-45.48,5c-33.59,7.08-63.23,21.24-82.91,39-1.08,1-1.92,1.68-2.4,2.16l-.48.48H124l-.12.12.12-.12a.12.12,0,0,0,.12-.12l-.12.12a.42.42,0,0,1,.24-.12c14.64-8.76,34.92-16,49.44-19.56l5.88-1.44c.36-.12.84-.12,1.2-.24,1.68-.36,3.36-.72,5.16-1.08.24,0,.6-.12.84-.12C250.94,20.94,319.34,40.14,367,85.61a171.49,171.49,0,0,1,26.88,32.76c30.36,49.2,27.48,111.11,3.84,147.59-34.44,53-111.35,71.27-159,24.84a84.19,84.19,0,0,1-25.56-59,74.05,74.05,0,0,1,6.24-31c1.68-3.84,13.08-25.67,18.24-24.59-13.08-2.76-37.55,2.64-54.71,28.19-15.36,22.92-14.52,58.2-5,83.28a132.85,132.85,0,0,1-12.12-39.24c-12.24-82.55,43.31-153,94.31-170.51-27.48-24-96.47-22.31-147.71,15.36-29.88,22-51.23,53.16-62.51,90.36,1.68-20.88,9.6-52.08,25.8-83.88-17.16,8.88-39,37-49.8,62.88-15.6,37.43-21,82.19-16.08,124.79.36,3.24.72,6.36,1.08,9.6,19.92,117.11,122,206.38,244.78,206.38C392.77,503.42,504,392.19,504,255,503.88,250.48,503.76,245.92,503.52,241.48Z"/></svg>
|
|
Before Width: | Height: | Size: 2.3 KiB |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M274.9 34.3c-28.1-28.1-73.7-28.1-101.8 0L34.3 173.1c-28.1 28.1-28.1 73.7 0 101.8L173.1 413.7c28.1 28.1 73.7 28.1 101.8 0L413.7 274.9c28.1-28.1 28.1-73.7 0-101.8L274.9 34.3zM200 224a24 24 0 1 1 48 0 24 24 0 1 1 -48 0zM96 200a24 24 0 1 1 0 48 24 24 0 1 1 0-48zM224 376a24 24 0 1 1 0-48 24 24 0 1 1 0 48zM352 200a24 24 0 1 1 0 48 24 24 0 1 1 0-48zM224 120a24 24 0 1 1 0-48 24 24 0 1 1 0 48zm96 328c0 35.3 28.7 64 64 64H576c35.3 0 64-28.7 64-64V256c0-35.3-28.7-64-64-64H461.7c11.6 36 3.1 77-25.4 105.5L320 413.8V448zM480 328a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>
|
|
Before Width: | Height: | Size: 803 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M64 112c-8.8 0-16 7.2-16 16v22.1L220.5 291.7c20.7 17 50.4 17 71.1 0L464 150.1V128c0-8.8-7.2-16-16-16H64zM48 212.2V384c0 8.8 7.2 16 16 16H448c8.8 0 16-7.2 16-16V212.2L322 328.8c-38.4 31.5-93.7 31.5-132 0L48 212.2zM0 128C0 92.7 28.7 64 64 64H448c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128z"/></svg>
|
|
Before Width: | Height: | Size: 567 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M96 0C43 0 0 43 0 96V352c0 48 35.2 87.7 81.1 94.9l-46 46C28.1 499.9 33.1 512 43 512H82.7c8.5 0 16.6-3.4 22.6-9.4L160 448H288l54.6 54.6c6 6 14.1 9.4 22.6 9.4H405c10 0 15-12.1 7.9-19.1l-46-46c46-7.1 81.1-46.9 81.1-94.9V96c0-53-43-96-96-96H96zM64 96c0-17.7 14.3-32 32-32H352c17.7 0 32 14.3 32 32v96c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V96zM224 288a48 48 0 1 1 0 96 48 48 0 1 1 0-96z"/></svg>
|
|
Before Width: | Height: | Size: 636 B |
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!--! Font Awesome Pro 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M549.655 124.083c-6.281-23.65-24.787-42.276-48.284-48.597C458.781 64 288 64 288 64S117.22 64 74.629 75.486c-23.497 6.322-42.003 24.947-48.284 48.597-11.412 42.867-11.412 132.305-11.412 132.305s0 89.438 11.412 132.305c6.281 23.65 24.787 41.5 48.284 47.821C117.22 448 288 448 288 448s170.78 0 213.371-11.486c23.497-6.321 42.003-24.171 48.284-47.821 11.412-42.867 11.412-132.305 11.412-132.305s0-89.438-11.412-132.305zm-317.51 213.508V175.185l142.739 81.205-142.739 81.201z"/></svg>
|
|
Before Width: | Height: | Size: 718 B |
Before Width: | Height: | Size: 689 KiB |
Before Width: | Height: | Size: 266 KiB |
Before Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 149 KiB |
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 4.1 KiB |
Before Width: | Height: | Size: 360 B |
Before Width: | Height: | Size: 815 B |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 490 B |
Before Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 375 B |