mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-27 02:41:53 +00:00
Compare commits
34 Commits
feature/ag
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a81c01b232 | ||
|
|
c96a5b7754 | ||
|
|
b39760d9d7 | ||
|
|
2f4bb1e35e | ||
|
|
0534b5813b | ||
|
|
3333416331 | ||
|
|
50540e4847 | ||
|
|
35545056e7 | ||
|
|
3828b03790 | ||
|
|
6a1c1fde06 | ||
|
|
2e5cd4b8ca | ||
|
|
99727e132c | ||
|
|
0a0f0850d7 | ||
|
|
65fc3e5c52 | ||
|
|
0d4ddc3451 | ||
|
|
4ee72d560a | ||
|
|
9d364b0691 | ||
|
|
d786424353 | ||
|
|
5ecb230bcc | ||
|
|
fee03ef407 | ||
|
|
8ca3d56aa9 | ||
|
|
763c7d9f87 | ||
|
|
ea86117e5f | ||
|
|
708b273b42 | ||
|
|
db344386ef | ||
|
|
d3048c59e8 | ||
|
|
5e5aa5d830 | ||
|
|
880aa8040d | ||
|
|
93fda28393 | ||
|
|
3fba55a54d | ||
|
|
075ed97c96 | ||
|
|
42ef2bdf7e | ||
|
|
645083913c | ||
|
|
02bce4ed61 |
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"attribution": {
|
||||
"commit": ""
|
||||
}
|
||||
}
|
||||
81
.github/actions/setup-build/action.yml
vendored
81
.github/actions/setup-build/action.yml
vendored
@@ -1,81 +0,0 @@
|
||||
name: Setup Build Environment
|
||||
description: Common build environment setup steps
|
||||
|
||||
inputs:
|
||||
nodejs-version:
|
||||
description: Node.js version
|
||||
required: true
|
||||
setup-python:
|
||||
description: Set up Python
|
||||
required: false
|
||||
default: "false"
|
||||
setup-docker:
|
||||
description: Set up Docker QEMU and Buildx
|
||||
required: false
|
||||
default: "true"
|
||||
setup-sccache:
|
||||
description: Configure sccache for GitHub Actions
|
||||
required: false
|
||||
default: "true"
|
||||
free-space:
|
||||
description: Remove unnecessary packages to free disk space
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Free disk space
|
||||
if: inputs.free-space == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get remove --purge -y azure-cli || true
|
||||
sudo apt-get remove --purge -y firefox || true
|
||||
sudo apt-get remove --purge -y ghc-* || true
|
||||
sudo apt-get remove --purge -y google-cloud-sdk || true
|
||||
sudo apt-get remove --purge -y google-chrome-stable || true
|
||||
sudo apt-get remove --purge -y powershell || true
|
||||
sudo apt-get remove --purge -y php* || true
|
||||
sudo apt-get remove --purge -y ruby* || true
|
||||
sudo apt-get remove --purge -y mono-* || true
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /usr/lib/jvm
|
||||
sudo rm -rf /usr/local/.ghcup
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/share/swift
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
|
||||
# BuildJet runners lack /opt/hostedtoolcache, which setup-python and setup-qemu expect
|
||||
- name: Ensure hostedtoolcache exists
|
||||
shell: bash
|
||||
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
||||
|
||||
- name: Set up Python
|
||||
if: inputs.setup-python == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.nodejs-version }}
|
||||
cache: npm
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
|
||||
- name: Set up Docker QEMU
|
||||
if: inputs.setup-docker == 'true'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: inputs.setup-docker == 'true'
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Configure sccache
|
||||
if: inputs.setup-sccache == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
48
.github/workflows/start-cli.yaml
vendored
48
.github/workflows/start-cli.yaml
vendored
@@ -37,10 +37,6 @@ on:
|
||||
- master
|
||||
- next/*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||
@@ -48,7 +44,6 @@ env:
|
||||
jobs:
|
||||
compile:
|
||||
name: Build Debian Package
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
@@ -65,15 +60,50 @@ jobs:
|
||||
}}
|
||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||
steps:
|
||||
- name: Mount tmpfs
|
||||
- name: Cleaning up unnecessary files
|
||||
run: |
|
||||
sudo apt-get remove --purge -y mono-* \
|
||||
ghc* cabal-install* \
|
||||
dotnet* \
|
||||
php* \
|
||||
ruby* \
|
||||
mysql-* \
|
||||
postgresql-* \
|
||||
azure-cli \
|
||||
powershell \
|
||||
google-cloud-sdk \
|
||||
msodbcsql* mssql-tools* \
|
||||
imagemagick* \
|
||||
libgl1-mesa-dri \
|
||||
google-chrome-stable \
|
||||
firefox
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get clean
|
||||
|
||||
- run: |
|
||||
sudo mount -t tmpfs tmpfs .
|
||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||
run: sudo mount -t tmpfs tmpfs .
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: ./.github/actions/setup-build
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Configure sccache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Make
|
||||
run: TARGET=${{ matrix.triple }} make cli
|
||||
|
||||
50
.github/workflows/start-registry.yaml
vendored
50
.github/workflows/start-registry.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: start-registry
|
||||
name: Start-Registry
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -35,10 +35,6 @@ on:
|
||||
- master
|
||||
- next/*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||
@@ -46,7 +42,6 @@ env:
|
||||
jobs:
|
||||
compile:
|
||||
name: Build Debian Package
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
@@ -61,15 +56,50 @@ jobs:
|
||||
}}
|
||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||
steps:
|
||||
- name: Mount tmpfs
|
||||
- name: Cleaning up unnecessary files
|
||||
run: |
|
||||
sudo apt-get remove --purge -y mono-* \
|
||||
ghc* cabal-install* \
|
||||
dotnet* \
|
||||
php* \
|
||||
ruby* \
|
||||
mysql-* \
|
||||
postgresql-* \
|
||||
azure-cli \
|
||||
powershell \
|
||||
google-cloud-sdk \
|
||||
msodbcsql* mssql-tools* \
|
||||
imagemagick* \
|
||||
libgl1-mesa-dri \
|
||||
google-chrome-stable \
|
||||
firefox
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get clean
|
||||
|
||||
- run: |
|
||||
sudo mount -t tmpfs tmpfs .
|
||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||
run: sudo mount -t tmpfs tmpfs .
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: ./.github/actions/setup-build
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Configure sccache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Make
|
||||
run: make registry-deb
|
||||
|
||||
50
.github/workflows/start-tunnel.yaml
vendored
50
.github/workflows/start-tunnel.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: start-tunnel
|
||||
name: Start-Tunnel
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -35,10 +35,6 @@ on:
|
||||
- master
|
||||
- next/*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||
@@ -46,7 +42,6 @@ env:
|
||||
jobs:
|
||||
compile:
|
||||
name: Build Debian Package
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
@@ -61,15 +56,50 @@ jobs:
|
||||
}}
|
||||
runs-on: ${{ fromJson('["ubuntu-latest", "buildjet-32vcpu-ubuntu-2204"]')[github.event.inputs.runner == 'fast'] }}
|
||||
steps:
|
||||
- name: Mount tmpfs
|
||||
- name: Cleaning up unnecessary files
|
||||
run: |
|
||||
sudo apt-get remove --purge -y mono-* \
|
||||
ghc* cabal-install* \
|
||||
dotnet* \
|
||||
php* \
|
||||
ruby* \
|
||||
mysql-* \
|
||||
postgresql-* \
|
||||
azure-cli \
|
||||
powershell \
|
||||
google-cloud-sdk \
|
||||
msodbcsql* mssql-tools* \
|
||||
imagemagick* \
|
||||
libgl1-mesa-dri \
|
||||
google-chrome-stable \
|
||||
firefox
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get clean
|
||||
|
||||
- run: |
|
||||
sudo mount -t tmpfs tmpfs .
|
||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||
run: sudo mount -t tmpfs tmpfs .
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: ./.github/actions/setup-build
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Configure sccache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Make
|
||||
run: make tunnel-deb
|
||||
|
||||
62
.github/workflows/startos-iso.yaml
vendored
62
.github/workflows/startos-iso.yaml
vendored
@@ -27,7 +27,7 @@ on:
|
||||
- x86_64-nonfree
|
||||
- aarch64
|
||||
- aarch64-nonfree
|
||||
# - raspberrypi
|
||||
- raspberrypi
|
||||
- riscv64
|
||||
deploy:
|
||||
type: choice
|
||||
@@ -45,10 +45,6 @@ on:
|
||||
- master
|
||||
- next/*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: '${{ fromJson(format(''["{0}", ""]'', github.event.inputs.environment || ''dev''))[github.event.inputs.environment == ''NONE''] }}'
|
||||
@@ -56,7 +52,6 @@ env:
|
||||
jobs:
|
||||
compile:
|
||||
name: Compile Base Binaries
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
@@ -91,16 +86,54 @@ jobs:
|
||||
)[github.event.inputs.runner == 'fast']
|
||||
}}
|
||||
steps:
|
||||
- name: Mount tmpfs
|
||||
- name: Cleaning up unnecessary files
|
||||
run: |
|
||||
sudo apt-get remove --purge -y azure-cli || true
|
||||
sudo apt-get remove --purge -y firefox || true
|
||||
sudo apt-get remove --purge -y ghc-* || true
|
||||
sudo apt-get remove --purge -y google-cloud-sdk || true
|
||||
sudo apt-get remove --purge -y google-chrome-stable || true
|
||||
sudo apt-get remove --purge -y powershell || true
|
||||
sudo apt-get remove --purge -y php* || true
|
||||
sudo apt-get remove --purge -y ruby* || true
|
||||
sudo apt-get remove --purge -y mono-* || true
|
||||
sudo apt-get autoremove -y
|
||||
sudo apt-get clean
|
||||
sudo rm -rf /usr/lib/jvm # All JDKs
|
||||
sudo rm -rf /usr/local/.ghcup # Haskell toolchain
|
||||
sudo rm -rf /usr/local/lib/android # Android SDK/NDK, emulator
|
||||
sudo rm -rf /usr/share/dotnet # .NET SDKs
|
||||
sudo rm -rf /usr/share/swift # Swift toolchain (if present)
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
||||
- run: |
|
||||
sudo mount -t tmpfs tmpfs .
|
||||
if: ${{ github.event.inputs.runner == 'fast' }}
|
||||
run: sudo mount -t tmpfs tmpfs .
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: ./.github/actions/setup-build
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||
setup-python: "true"
|
||||
python-version: "3.x"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Configure sccache
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
core.exportVariable('ACTIONS_RESULTS_URL', process.env.ACTIONS_RESULTS_URL || '');
|
||||
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
|
||||
|
||||
- name: Make
|
||||
run: make ARCH=${{ matrix.arch }} compiled-${{ matrix.arch }}.tar
|
||||
@@ -118,14 +151,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# TODO: re-add "raspberrypi" to the platform list below
|
||||
platform: >-
|
||||
${{
|
||||
fromJson(
|
||||
format(
|
||||
'[
|
||||
["{0}"],
|
||||
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64"]
|
||||
["x86_64", "x86_64-nonfree", "aarch64", "aarch64-nonfree", "riscv64", "raspberrypi"]
|
||||
]',
|
||||
github.event.inputs.platform || 'ALL'
|
||||
)
|
||||
@@ -189,10 +221,6 @@ jobs:
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY" # Pre-cached tool cache (Go, Node, etc.)
|
||||
if: ${{ github.event.inputs.runner != 'fast' }}
|
||||
|
||||
# BuildJet runners lack /opt/hostedtoolcache, which setup-qemu expects
|
||||
- name: Ensure hostedtoolcache exists
|
||||
run: sudo mkdir -p /opt/hostedtoolcache && sudo chown $USER:$USER /opt/hostedtoolcache
|
||||
|
||||
- name: Set up docker QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
|
||||
13
.github/workflows/test.yaml
vendored
13
.github/workflows/test.yaml
vendored
@@ -10,10 +10,6 @@ on:
|
||||
- master
|
||||
- next/*
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: "24.11.0"
|
||||
ENVIRONMENT: dev-unstable
|
||||
@@ -21,18 +17,15 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
name: Run Automated Tests
|
||||
if: github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: ./.github/actions/setup-build
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
nodejs-version: ${{ env.NODEJS_VERSION }}
|
||||
free-space: "false"
|
||||
setup-docker: "false"
|
||||
setup-sccache: "false"
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
|
||||
- name: Build And Run Tests
|
||||
run: make test
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,6 +19,4 @@ secrets.db
|
||||
/compiled.tar
|
||||
/compiled-*.tar
|
||||
/build/lib/firmware
|
||||
tmp
|
||||
web/.i18n-checked
|
||||
agents/USER.md
|
||||
tmp
|
||||
146
CLAUDE.md
146
CLAUDE.md
@@ -1,146 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
StartOS is an open-source Linux distribution for running personal servers. It manages discovery, installation, network configuration, backups, and health monitoring of self-hosted services.
|
||||
|
||||
**Tech Stack:**
|
||||
- Backend: Rust (async/Tokio, Axum web framework)
|
||||
- Frontend: Angular 20 + TypeScript + TaigaUI
|
||||
- Container runtime: Node.js/TypeScript with LXC
|
||||
- Database/State: Patch-DB (git submodule) - storage layer with reactive frontend sync
|
||||
- API: JSON-RPC via rpc-toolkit (see `agents/rpc-toolkit.md`)
|
||||
- Auth: Password + session cookie, public/private key signatures, local authcookie (see `core/src/middleware/auth/`)
|
||||
|
||||
## Build & Development
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for:
|
||||
- Environment setup and requirements
|
||||
- Build commands and make targets
|
||||
- Testing and formatting commands
|
||||
- Environment variables
|
||||
|
||||
**Quick reference:**
|
||||
```bash
|
||||
. ./devmode.sh # Enable dev mode
|
||||
make update-startbox REMOTE=start9@<ip> # Fastest iteration (binary + UI)
|
||||
make test-core # Run Rust tests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core (`/core`)
|
||||
The Rust backend daemon. Main binaries:
|
||||
- `startbox` - Main daemon (runs as `startd`)
|
||||
- `start-cli` - CLI interface
|
||||
- `start-container` - Runs inside LXC containers; communicates with host and manages subcontainers
|
||||
- `registrybox` - Registry daemon
|
||||
- `tunnelbox` - VPN/tunnel daemon
|
||||
|
||||
**Key modules:**
|
||||
- `src/context/` - Context types (RpcContext, CliContext, InitContext, DiagnosticContext)
|
||||
- `src/service/` - Service lifecycle management with actor pattern (`service_actor.rs`)
|
||||
- `src/db/model/` - Patch-DB models (`public.rs` synced to frontend, `private.rs` backend-only)
|
||||
- `src/net/` - Networking (DNS, ACME, WiFi, Tor via Arti, WireGuard)
|
||||
- `src/s9pk/` - S9PK package format (merkle archive)
|
||||
- `src/registry/` - Package registry management
|
||||
|
||||
**RPC Pattern:** See `agents/rpc-toolkit.md`
|
||||
|
||||
### Web (`/web`)
|
||||
Angular projects sharing common code:
|
||||
- `projects/ui/` - Main admin interface
|
||||
- `projects/setup-wizard/` - Initial setup
|
||||
- `projects/start-tunnel/` - VPN management UI
|
||||
- `projects/shared/` - Common library (API clients, components)
|
||||
- `projects/marketplace/` - Service discovery
|
||||
|
||||
**Development:**
|
||||
```bash
|
||||
cd web
|
||||
npm ci
|
||||
npm run start:ui # Dev server with mocks
|
||||
npm run build:ui # Production build
|
||||
npm run check # Type check all projects
|
||||
```
|
||||
|
||||
### Container Runtime (`/container-runtime`)
|
||||
Node.js runtime that manages service containers via RPC. See `RPCSpec.md` for protocol.
|
||||
|
||||
**Container Architecture:**
|
||||
```
|
||||
LXC Container (uniform base for all services)
|
||||
└── systemd
|
||||
└── container-runtime.service
|
||||
└── Loads /usr/lib/startos/package/index.js (from s9pk javascript.squashfs)
|
||||
└── Package JS launches subcontainers (from images in s9pk)
|
||||
```
|
||||
|
||||
The container runtime communicates with the host via JSON-RPC over Unix socket. Package JavaScript must export functions conforming to the `ABI` type defined in `sdk/base/lib/types.ts`.
|
||||
|
||||
**`/media/startos/` directory (mounted by host into container):**
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `volumes/<name>/` | Package data volumes (id-mapped, persistent) |
|
||||
| `assets/` | Read-only assets from s9pk `assets.squashfs` |
|
||||
| `images/<name>/` | Container images (squashfs, used for subcontainers) |
|
||||
| `images/<name>.env` | Environment variables for image |
|
||||
| `images/<name>.json` | Image metadata |
|
||||
| `backup/` | Backup mount point (mounted during backup operations) |
|
||||
| `rpc/service.sock` | RPC socket (container runtime listens here) |
|
||||
| `rpc/host.sock` | Host RPC socket (for effects callbacks to host) |
|
||||
|
||||
**S9PK Structure:** See `agents/s9pk-structure.md`
|
||||
|
||||
### SDK (`/sdk`)
|
||||
TypeScript SDK for packaging services (`@start9labs/start-sdk`).
|
||||
|
||||
- `base/` - Core types, ABI definitions, effects interface (`@start9labs/start-sdk-base`)
|
||||
- `package/` - Full SDK for package developers, re-exports base
|
||||
|
||||
### Patch-DB (`/patch-db`)
|
||||
Git submodule providing diff-based state synchronization. Changes to `db/model/public.rs` automatically sync to the frontend.
|
||||
|
||||
**Key patterns:**
|
||||
- `db.peek().await` - Get a read-only snapshot of the database state
|
||||
- `db.mutate(|db| { ... }).await` - Apply mutations atomically, returns `MutateResult`
|
||||
- `#[derive(HasModel)]` - Derive macro for types stored in the database, generates typed accessors
|
||||
|
||||
**Generated accessor types** (from `HasModel` derive):
|
||||
- `as_field()` - Immutable reference: `&Model<T>`
|
||||
- `as_field_mut()` - Mutable reference: `&mut Model<T>`
|
||||
- `into_field()` - Owned value: `Model<T>`
|
||||
|
||||
**`Model<T>` APIs** (from `db/prelude.rs`):
|
||||
- `.de()` - Deserialize to `T`
|
||||
- `.ser(&value)` - Serialize from `T`
|
||||
- `.mutate(|v| ...)` - Deserialize, mutate, reserialize
|
||||
- For maps: `.keys()`, `.as_idx(&key)`, `.as_idx_mut(&key)`, `.insert()`, `.remove()`, `.contains_key()`
|
||||
|
||||
## Supplementary Documentation
|
||||
|
||||
The `agents/` directory contains detailed documentation for AI assistants:
|
||||
|
||||
- `TODO.md` - Pending tasks for AI agents (check this first, remove items when completed)
|
||||
- `USER.md` - Current user identifier (gitignored, see below)
|
||||
- `rpc-toolkit.md` - JSON-RPC patterns and handler configuration
|
||||
- `core-rust-patterns.md` - Common utilities and patterns for Rust code in `/core` (guard pattern, mount guards, etc.)
|
||||
- `s9pk-structure.md` - S9PK package format structure
|
||||
- `i18n-patterns.md` - Internationalization key conventions and usage in `/core`
|
||||
|
||||
### Session Startup
|
||||
|
||||
On startup:
|
||||
|
||||
1. **Check for `agents/USER.md`** - If it doesn't exist, prompt the user for their name/identifier and create it. This file is gitignored since it varies per developer.
|
||||
|
||||
2. **Check `agents/TODO.md` for relevant tasks** - Show TODOs that either:
|
||||
- Have no `@username` tag (relevant to everyone)
|
||||
- Are tagged with the current user's identifier
|
||||
|
||||
Skip TODOs tagged with a different user.
|
||||
|
||||
3. **Ask "What would you like to do today?"** - Offer options for each relevant TODO item, plus "Something else" for other requests.
|
||||
259
CONTRIBUTING.md
259
CONTRIBUTING.md
@@ -11,190 +11,123 @@ This guide is for contributing to the StartOS. If you are interested in packagin
|
||||
|
||||
```bash
|
||||
/
|
||||
├── assets/ # Screenshots for README
|
||||
├── build/ # Auxiliary files and scripts for deployed images
|
||||
├── container-runtime/ # Node.js program managing package containers
|
||||
├── core/ # Rust backend: API, daemon (startd), CLI (start-cli)
|
||||
├── debian/ # Debian package maintainer scripts
|
||||
├── image-recipe/ # Scripts for building StartOS images
|
||||
├── patch-db/ # (submodule) Diff-based data store for frontend sync
|
||||
├── sdk/ # TypeScript SDK for building StartOS packages
|
||||
└── web/ # Web UIs (Angular)
|
||||
├── assets/
|
||||
├── container-runtime/
|
||||
├── core/
|
||||
├── build/
|
||||
├── debian/
|
||||
├── web/
|
||||
├── image-recipe/
|
||||
├── patch-db
|
||||
└── sdk/
|
||||
```
|
||||
|
||||
See component READMEs for details:
|
||||
- [`core`](core/README.md)
|
||||
- [`web`](web/README.md)
|
||||
- [`build`](build/README.md)
|
||||
- [`patch-db`](https://github.com/Start9Labs/patch-db)
|
||||
#### assets
|
||||
|
||||
screenshots for the StartOS README
|
||||
|
||||
#### container-runtime
|
||||
|
||||
A NodeJS program that dynamically loads maintainer scripts and communicates with the OS to manage packages
|
||||
|
||||
#### core
|
||||
|
||||
An API, daemon (startd), and CLI (start-cli) that together provide the core functionality of StartOS.
|
||||
|
||||
#### build
|
||||
|
||||
Auxiliary files and scripts to include in deployed StartOS images
|
||||
|
||||
#### debian
|
||||
|
||||
Maintainer scripts for the StartOS Debian package
|
||||
|
||||
#### web
|
||||
|
||||
Web UIs served under various conditions and used to interact with StartOS APIs.
|
||||
|
||||
#### image-recipe
|
||||
|
||||
Scripts for building StartOS images
|
||||
|
||||
#### patch-db (submodule)
|
||||
|
||||
A diff based data store used to synchronize data between the web interfaces and server.
|
||||
|
||||
#### sdk
|
||||
|
||||
A typescript sdk for building start-os packages
|
||||
|
||||
## Environment Setup
|
||||
|
||||
#### Clone the StartOS repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Start9Labs/start-os.git --recurse-submodules
|
||||
cd start-os
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
#### Continue to your project of interest for additional instructions:
|
||||
|
||||
For faster iteration during development:
|
||||
|
||||
```sh
|
||||
. ./devmode.sh
|
||||
```
|
||||
|
||||
This sets `ENVIRONMENT=dev` and `GIT_BRANCH_AS_HASH=1` to prevent rebuilds on every commit.
|
||||
- [`core`](core/README.md)
|
||||
- [`web-interfaces`](web-interfaces/README.md)
|
||||
- [`build`](build/README.md)
|
||||
- [`patch-db`](https://github.com/Start9Labs/patch-db)
|
||||
|
||||
## Building
|
||||
|
||||
All builds can be performed on any operating system that can run Docker.
|
||||
|
||||
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components.
|
||||
This project uses [GNU Make](https://www.gnu.org/software/make/) to build its components. To build any specific component, simply run `make <TARGET>` replacing `<TARGET>` with the name of the target you'd like to build
|
||||
|
||||
### Requirements
|
||||
|
||||
- [GNU Make](https://www.gnu.org/software/make/)
|
||||
- [Docker](https://docs.docker.com/get-docker/) or [Podman](https://podman.io/)
|
||||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
- [NodeJS v20.16.0](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
|
||||
- [Rust](https://rustup.rs/) (nightly for formatting)
|
||||
- [sed](https://www.gnu.org/software/sed/), [grep](https://www.gnu.org/software/grep/), [awk](https://www.gnu.org/software/gawk/)
|
||||
- [sed](https://www.gnu.org/software/sed/)
|
||||
- [grep](https://www.gnu.org/software/grep/)
|
||||
- [awk](https://www.gnu.org/software/gawk/)
|
||||
- [jq](https://jqlang.github.io/jq/)
|
||||
- [gzip](https://www.gnu.org/software/gzip/), [brotli](https://github.com/google/brotli)
|
||||
- [gzip](https://www.gnu.org/software/gzip/)
|
||||
- [brotli](https://github.com/google/brotli)
|
||||
|
||||
### Environment Variables
|
||||
### Environment variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `PLATFORM` | Target platform: `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `riscv64`, `raspberrypi` |
|
||||
| `ENVIRONMENT` | Hyphen-separated feature flags (see below) |
|
||||
| `PROFILE` | Build profile: `release` (default) or `dev` |
|
||||
| `GIT_BRANCH_AS_HASH` | Set to `1` to use git branch name as version hash (avoids rebuilds) |
|
||||
- `PLATFORM`: which platform you would like to build for. Must be one of `x86_64`, `x86_64-nonfree`, `aarch64`, `aarch64-nonfree`, `raspberrypi`
|
||||
- NOTE: `nonfree` images are for including `nonfree` firmware packages in the built ISO
|
||||
- `ENVIRONMENT`: a hyphen separated set of feature flags to enable
|
||||
- `dev`: enables password ssh (INSECURE!) and does not compress frontends
|
||||
- `unstable`: enables assertions that will cause errors on unexpected inconsistencies that are undesirable in production use either for performance or reliability reasons
|
||||
- `docker`: use `docker` instead of `podman`
|
||||
- `GIT_BRANCH_AS_HASH`: set to `1` to use the current git branch name as the git hash so that the project does not need to be rebuilt on each commit
|
||||
|
||||
**ENVIRONMENT flags:**
|
||||
- `dev` - Enables password SSH before setup, skips frontend compression
|
||||
- `unstable` - Enables assertions and debugging with performance penalty
|
||||
- `console` - Enables tokio-console for async debugging
|
||||
|
||||
**Platform notes:**
|
||||
- `-nonfree` variants include proprietary firmware and drivers
|
||||
- `raspberrypi` includes non-free components by necessity
|
||||
- Platform is remembered between builds if not specified
|
||||
|
||||
### Make Targets
|
||||
|
||||
#### Building
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `iso` | Create full `.iso` image (not for raspberrypi) |
|
||||
| `img` | Create full `.img` image (raspberrypi only) |
|
||||
| `deb` | Build Debian package |
|
||||
| `all` | Build all Rust binaries |
|
||||
| `uis` | Build all web UIs |
|
||||
| `ui` | Build main UI only |
|
||||
| `ts-bindings` | Generate TypeScript bindings from Rust types |
|
||||
|
||||
#### Deploying to Device
|
||||
|
||||
For devices on the same network:
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `update-startbox REMOTE=start9@<ip>` | Deploy binary + UI only (fastest) |
|
||||
| `update-deb REMOTE=start9@<ip>` | Deploy full Debian package |
|
||||
| `update REMOTE=start9@<ip>` | OTA-style update |
|
||||
| `reflash REMOTE=start9@<ip>` | Reflash as if using live ISO |
|
||||
| `update-overlay REMOTE=start9@<ip>` | Deploy to in-memory overlay (reverts on reboot) |
|
||||
|
||||
For devices on different networks (uses [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)):
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `wormhole` | Send startbox binary |
|
||||
| `wormhole-deb` | Send Debian package |
|
||||
| `wormhole-squashfs` | Send squashfs image |
|
||||
|
||||
#### Other
|
||||
|
||||
| Target | Description |
|
||||
|--------|-------------|
|
||||
| `format` | Run code formatting (Rust nightly required) |
|
||||
| `test` | Run all automated tests |
|
||||
| `test-core` | Run Rust tests |
|
||||
| `test-sdk` | Run SDK tests |
|
||||
| `test-container-runtime` | Run container runtime tests |
|
||||
| `clean` | Delete all compiled artifacts |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
make test # All tests
|
||||
make test-core # Rust tests (via ./core/run-tests.sh)
|
||||
make test-sdk # SDK tests
|
||||
make test-container-runtime # Container runtime tests
|
||||
|
||||
# Run specific Rust test
|
||||
cd core && cargo test <test_name> --features=test
|
||||
```
|
||||
|
||||
## Code Formatting
|
||||
|
||||
```bash
|
||||
# Rust (requires nightly)
|
||||
make format
|
||||
|
||||
# TypeScript/HTML/SCSS (web)
|
||||
cd web && npm run format
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Formatting
|
||||
|
||||
Run the formatters before committing. Configuration is handled by `rustfmt.toml` (Rust) and prettier configs (TypeScript).
|
||||
|
||||
### Documentation & Comments
|
||||
|
||||
**Rust:**
|
||||
- Add doc comments (`///`) to public APIs, structs, and non-obvious functions
|
||||
- Use `//` comments sparingly for complex logic that isn't self-evident
|
||||
- Prefer self-documenting code (clear naming, small functions) over comments
|
||||
|
||||
**TypeScript:**
|
||||
- Document exported functions and complex types with JSDoc
|
||||
- Keep comments focused on "why" rather than "what"
|
||||
|
||||
**General:**
|
||||
- Don't add comments that just restate the code
|
||||
- Update or remove comments when code changes
|
||||
- TODOs should include context: `// TODO(username): reason`
|
||||
|
||||
### Commit Messages
|
||||
|
||||
Use [Conventional Commits](https://www.conventionalcommits.org/):
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer]
|
||||
```
|
||||
|
||||
**Types:**
|
||||
- `feat` - New feature
|
||||
- `fix` - Bug fix
|
||||
- `docs` - Documentation only
|
||||
- `style` - Formatting, no code change
|
||||
- `refactor` - Code change that neither fixes a bug nor adds a feature
|
||||
- `test` - Adding or updating tests
|
||||
- `chore` - Build process, dependencies, etc.
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
feat(web): add dark mode toggle
|
||||
fix(core): resolve race condition in service startup
|
||||
docs: update CONTRIBUTING.md with style guidelines
|
||||
refactor(sdk): simplify package validation logic
|
||||
```
|
||||
### Useful Make Targets
|
||||
|
||||
- `iso`: Create a full `.iso` image
|
||||
- Only possible from Debian
|
||||
- Not available for `PLATFORM=raspberrypi`
|
||||
- Additional Requirements:
|
||||
- [debspawn](https://github.com/lkhq/debspawn)
|
||||
- `img`: Create a full `.img` image
|
||||
- Only possible from Debian
|
||||
- Only available for `PLATFORM=raspberrypi`
|
||||
- Additional Requirements:
|
||||
- [debspawn](https://github.com/lkhq/debspawn)
|
||||
- `format`: Run automatic code formatting for the project
|
||||
- Additional Requirements:
|
||||
- [rust](https://rustup.rs/)
|
||||
- `test`: Run automated tests for the project
|
||||
- Additional Requirements:
|
||||
- [rust](https://rustup.rs/)
|
||||
- `update`: Deploy the current working project to a device over ssh as if through an over-the-air update
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- `reflash`: Deploy the current working project to a device over ssh as if using a live `iso` image to reflash it
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- `update-overlay`: Deploy the current working project to a device over ssh to the in-memory overlay without restarting it
|
||||
- WARNING: changes will be reverted after the device is rebooted
|
||||
- WARNING: changes to `init` will not take effect as the device is already initialized
|
||||
- Requires an argument `REMOTE` which is the ssh address of the device, i.e. `start9@192.168.122.2`
|
||||
- `wormhole`: Deploy the `startbox` to a device using [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
||||
- When the build it complete will emit a command to paste into the shell of the device to upgrade it
|
||||
- Additional Requirements:
|
||||
- [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)
|
||||
- `clean`: Delete all compiled artifacts
|
||||
|
||||
10
Makefile
10
Makefile
@@ -324,19 +324,15 @@ web/.angular/.updated: patch-db/client/dist/index.js sdk/baseDist/package.json w
|
||||
mkdir -p web/.angular
|
||||
touch web/.angular/.updated
|
||||
|
||||
web/.i18n-checked: $(WEB_SHARED_SRC) $(WEB_UI_SRC) $(WEB_SETUP_WIZARD_SRC) $(WEB_START_TUNNEL_SRC)
|
||||
npm --prefix web run check:i18n
|
||||
touch web/.i18n-checked
|
||||
|
||||
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
|
||||
web/dist/raw/ui/index.html: $(WEB_UI_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:ui
|
||||
touch web/dist/raw/ui/index.html
|
||||
|
||||
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
|
||||
web/dist/raw/setup-wizard/index.html: $(WEB_SETUP_WIZARD_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:setup
|
||||
touch web/dist/raw/setup-wizard/index.html
|
||||
|
||||
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated web/.i18n-checked
|
||||
web/dist/raw/start-tunnel/index.html: $(WEB_START_TUNNEL_SRC) $(WEB_SHARED_SRC) web/.angular/.updated
|
||||
npm --prefix web run build:tunnel
|
||||
touch web/dist/raw/start-tunnel/index.html
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# AI Agent TODOs
|
||||
|
||||
Pending tasks for AI agents. Remove items when completed.
|
||||
|
||||
## Unreviewed CLAUDE.md Sections
|
||||
|
||||
- [ ] Architecture - Web (`/web`) - @MattDHill
|
||||
|
||||
|
||||
@@ -1,249 +0,0 @@
|
||||
# Utilities & Patterns
|
||||
|
||||
This document covers common utilities and patterns used throughout the StartOS codebase.
|
||||
|
||||
## Util Module (`core/src/util/`)
|
||||
|
||||
The `util` module contains reusable utilities. Key submodules:
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `actor/` | Actor pattern implementation for concurrent state management |
|
||||
| `collections/` | Custom collection types |
|
||||
| `crypto.rs` | Cryptographic utilities (encryption, hashing) |
|
||||
| `future.rs` | Future/async utilities |
|
||||
| `io.rs` | File I/O helpers (create_file, canonicalize, etc.) |
|
||||
| `iter.rs` | Iterator extensions |
|
||||
| `net.rs` | Network utilities |
|
||||
| `rpc.rs` | RPC helpers |
|
||||
| `rpc_client.rs` | RPC client utilities |
|
||||
| `serde.rs` | Serialization helpers (Base64, display/fromstr, etc.) |
|
||||
| `sync.rs` | Synchronization primitives (SyncMutex, etc.) |
|
||||
|
||||
## Command Invocation (`Invoke` trait)
|
||||
|
||||
The `Invoke` trait provides a clean way to run external commands with error handling:
|
||||
|
||||
```rust
|
||||
use crate::util::Invoke;
|
||||
|
||||
// Simple invocation
|
||||
tokio::process::Command::new("ls")
|
||||
.arg("-la")
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
|
||||
// With timeout
|
||||
tokio::process::Command::new("slow-command")
|
||||
.timeout(Some(Duration::from_secs(30)))
|
||||
.invoke(ErrorKind::Timeout)
|
||||
.await?;
|
||||
|
||||
// With input
|
||||
let mut input = Cursor::new(b"input data");
|
||||
tokio::process::Command::new("cat")
|
||||
.input(Some(&mut input))
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
|
||||
// Piped commands
|
||||
tokio::process::Command::new("cat")
|
||||
.arg("file.txt")
|
||||
.pipe(&mut tokio::process::Command::new("grep").arg("pattern"))
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
```
|
||||
|
||||
## Guard Pattern
|
||||
|
||||
Guards ensure cleanup happens when they go out of scope.
|
||||
|
||||
### `GeneralGuard` / `GeneralBoxedGuard`
|
||||
|
||||
For arbitrary cleanup actions:
|
||||
|
||||
```rust
|
||||
use crate::util::GeneralGuard;
|
||||
|
||||
let guard = GeneralGuard::new(|| {
|
||||
println!("Cleanup runs on drop");
|
||||
});
|
||||
|
||||
// Do work...
|
||||
|
||||
// Explicit drop with action
|
||||
guard.drop();
|
||||
|
||||
// Or skip the action
|
||||
// guard.drop_without_action();
|
||||
```
|
||||
|
||||
### `FileLock`
|
||||
|
||||
File-based locking with automatic unlock:
|
||||
|
||||
```rust
|
||||
use crate::util::FileLock;
|
||||
|
||||
let lock = FileLock::new("/path/to/lockfile", true).await?; // blocking=true
|
||||
// Lock held until dropped or explicitly unlocked
|
||||
lock.unlock().await?;
|
||||
```
|
||||
|
||||
## Mount Guard Pattern (`core/src/disk/mount/guard.rs`)
|
||||
|
||||
RAII guards for filesystem mounts. Ensures filesystems are unmounted when guards are dropped.
|
||||
|
||||
### `MountGuard`
|
||||
|
||||
Basic mount guard:
|
||||
|
||||
```rust
|
||||
use crate::disk::mount::guard::MountGuard;
|
||||
use crate::disk::mount::filesystem::{MountType, ReadOnly};
|
||||
|
||||
let guard = MountGuard::mount(&filesystem, "/mnt/target", ReadOnly).await?;
|
||||
|
||||
// Use the mounted filesystem at guard.path()
|
||||
do_something(guard.path()).await?;
|
||||
|
||||
// Explicit unmount (or auto-unmounts on drop)
|
||||
guard.unmount(false).await?; // false = don't delete mountpoint
|
||||
```
|
||||
|
||||
### `TmpMountGuard`
|
||||
|
||||
Reference-counted temporary mount (mounts to `/media/startos/tmp/`):
|
||||
|
||||
```rust
|
||||
use crate::disk::mount::guard::TmpMountGuard;
|
||||
use crate::disk::mount::filesystem::ReadOnly;
|
||||
|
||||
// Multiple clones share the same mount
|
||||
let guard1 = TmpMountGuard::mount(&filesystem, ReadOnly).await?;
|
||||
let guard2 = guard1.clone();
|
||||
|
||||
// Mount stays alive while any guard exists
|
||||
// Auto-unmounts when last guard is dropped
|
||||
```
|
||||
|
||||
### `GenericMountGuard` trait
|
||||
|
||||
All mount guards implement this trait:
|
||||
|
||||
```rust
|
||||
pub trait GenericMountGuard: std::fmt::Debug + Send + Sync + 'static {
|
||||
fn path(&self) -> &Path;
|
||||
fn unmount(self) -> impl Future<Output = Result<(), Error>> + Send;
|
||||
}
|
||||
```
|
||||
|
||||
### `SubPath`
|
||||
|
||||
Wraps a mount guard to point to a subdirectory:
|
||||
|
||||
```rust
|
||||
use crate::disk::mount::guard::SubPath;
|
||||
|
||||
let mount = TmpMountGuard::mount(&filesystem, ReadOnly).await?;
|
||||
let subdir = SubPath::new(mount, "data/subdir");
|
||||
|
||||
// subdir.path() returns the full path including subdirectory
|
||||
```
|
||||
|
||||
## FileSystem Implementations (`core/src/disk/mount/filesystem/`)
|
||||
|
||||
Various filesystem types that can be mounted:
|
||||
|
||||
| Type | Description |
|
||||
|------|-------------|
|
||||
| `bind.rs` | Bind mounts |
|
||||
| `block_dev.rs` | Block device mounts |
|
||||
| `cifs.rs` | CIFS/SMB network shares |
|
||||
| `ecryptfs.rs` | Encrypted filesystem |
|
||||
| `efivarfs.rs` | EFI variables |
|
||||
| `httpdirfs.rs` | HTTP directory as filesystem |
|
||||
| `idmapped.rs` | ID-mapped mounts |
|
||||
| `label.rs` | Mount by label |
|
||||
| `loop_dev.rs` | Loop device mounts |
|
||||
| `overlayfs.rs` | Overlay filesystem |
|
||||
|
||||
## Other Useful Utilities
|
||||
|
||||
### `Apply` / `ApplyRef` traits
|
||||
|
||||
Fluent method chaining:
|
||||
|
||||
```rust
|
||||
use crate::util::Apply;
|
||||
|
||||
let result = some_value
|
||||
.apply(|v| transform(v))
|
||||
.apply(|v| another_transform(v));
|
||||
```
|
||||
|
||||
### `Container<T>`
|
||||
|
||||
Async-safe optional container:
|
||||
|
||||
```rust
|
||||
use crate::util::Container;
|
||||
|
||||
let container = Container::new(None);
|
||||
container.set(value).await;
|
||||
let taken = container.take().await;
|
||||
```
|
||||
|
||||
### `HashWriter<H, W>`
|
||||
|
||||
Write data while computing hash:
|
||||
|
||||
```rust
|
||||
use crate::util::HashWriter;
|
||||
use sha2::Sha256;
|
||||
|
||||
let writer = HashWriter::new(Sha256::new(), file);
|
||||
// Write data...
|
||||
let (hasher, file) = writer.finish();
|
||||
let hash = hasher.finalize();
|
||||
```
|
||||
|
||||
### `Never` type
|
||||
|
||||
Uninhabited type for impossible cases:
|
||||
|
||||
```rust
|
||||
use crate::util::Never;
|
||||
|
||||
fn impossible() -> Never {
|
||||
// This function can never return
|
||||
}
|
||||
|
||||
let never: Never = impossible();
|
||||
never.absurd::<String>() // Can convert to any type
|
||||
```
|
||||
|
||||
### `MaybeOwned<'a, T>`
|
||||
|
||||
Either borrowed or owned data:
|
||||
|
||||
```rust
|
||||
use crate::util::MaybeOwned;
|
||||
|
||||
fn accept_either(data: MaybeOwned<'_, String>) {
|
||||
// Use &*data to access the value
|
||||
}
|
||||
|
||||
accept_either(MaybeOwned::from(&existing_string));
|
||||
accept_either(MaybeOwned::from(owned_string));
|
||||
```
|
||||
|
||||
### `new_guid()`
|
||||
|
||||
Generate a random GUID:
|
||||
|
||||
```rust
|
||||
use crate::util::new_guid;
|
||||
|
||||
let guid = new_guid(); // Returns InternedString
|
||||
```
|
||||
@@ -1,100 +0,0 @@
|
||||
# i18n Patterns in `core/`
|
||||
|
||||
## Library & Setup
|
||||
|
||||
**Crate:** [`rust-i18n`](https://crates.io/crates/rust-i18n) v3.1.5 (`core/Cargo.toml`)
|
||||
|
||||
**Initialization** (`core/src/lib.rs:3`):
|
||||
```rust
|
||||
rust_i18n::i18n!("locales", fallback = ["en_US"]);
|
||||
```
|
||||
This macro scans `core/locales/` at compile time and embeds all translations as constants.
|
||||
|
||||
**Prelude re-export** (`core/src/prelude.rs:4`):
|
||||
```rust
|
||||
pub use rust_i18n::t;
|
||||
```
|
||||
Most modules import `t!` via the prelude.
|
||||
|
||||
## Translation File
|
||||
|
||||
**Location:** `core/locales/i18n.yaml`
|
||||
**Format:** YAML v2 (~755 keys)
|
||||
|
||||
**Supported languages:** `en_US`, `de_DE`, `es_ES`, `fr_FR`, `pl_PL`
|
||||
|
||||
**Entry structure:**
|
||||
```yaml
|
||||
namespace.sub.key-name:
|
||||
en_US: "English text with %{param}"
|
||||
de_DE: "German text with %{param}"
|
||||
# ...
|
||||
```
|
||||
|
||||
## Using `t!()`
|
||||
|
||||
```rust
|
||||
// Simple key
|
||||
t!("error.unknown")
|
||||
|
||||
// With parameter interpolation (%{name} in YAML)
|
||||
t!("bins.deprecated.renamed", old = old_name, new = new_name)
|
||||
```
|
||||
|
||||
## Key Naming Conventions
|
||||
|
||||
Keys use **dot-separated hierarchical namespaces** with **kebab-case** for multi-word segments:
|
||||
|
||||
```
|
||||
<module>.<submodule>.<descriptive-name>
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `error.incorrect-password` — error kind label
|
||||
- `bins.start-init.updating-firmware` — startup phase message
|
||||
- `backup.bulk.complete-title` — backup notification title
|
||||
- `help.arg.acme-contact` — CLI help text for an argument
|
||||
- `context.diagnostic.starting-diagnostic-ui` — diagnostic context status
|
||||
|
||||
### Top-Level Namespaces
|
||||
|
||||
| Namespace | Purpose |
|
||||
|-----------|---------|
|
||||
| `error.*` | `ErrorKind` display strings (see `src/error.rs`) |
|
||||
| `bins.*` | CLI binary messages (deprecated, start-init, startd, etc.) |
|
||||
| `init.*` | Initialization phase labels |
|
||||
| `setup.*` | First-run setup messages |
|
||||
| `context.*` | Context startup messages (diagnostic, setup, CLI) |
|
||||
| `service.*` | Service lifecycle messages |
|
||||
| `backup.*` | Backup/restore operation messages |
|
||||
| `registry.*` | Package registry messages |
|
||||
| `net.*` | Network-related messages |
|
||||
| `middleware.*` | Request middleware messages (auth, etc.) |
|
||||
| `disk.*` | Disk operation messages |
|
||||
| `lxc.*` | Container management messages |
|
||||
| `system.*` | System monitoring/metrics messages |
|
||||
| `notifications.*` | User-facing notification messages |
|
||||
| `update.*` | OS update messages |
|
||||
| `util.*` | Utility messages (TUI, RPC) |
|
||||
| `ssh.*` | SSH operation messages |
|
||||
| `shutdown.*` | Shutdown-related messages |
|
||||
| `logs.*` | Log-related messages |
|
||||
| `auth.*` | Authentication messages |
|
||||
| `help.*` | CLI help text (`help.arg.<arg-name>`) |
|
||||
| `about.*` | CLI command descriptions |
|
||||
|
||||
## Locale Selection
|
||||
|
||||
`core/src/bins/mod.rs:15-36` — `set_locale_from_env()`:
|
||||
|
||||
1. Reads `LANG` environment variable
|
||||
2. Strips `.UTF-8` suffix
|
||||
3. Exact-matches against available locales, falls back to language-prefix match (e.g. `en_GB` matches `en_US`)
|
||||
|
||||
## Adding New Keys
|
||||
|
||||
1. Add the key to `core/locales/i18n.yaml` with all 5 language translations
|
||||
2. Use the `t!("your.key.name")` macro in Rust code
|
||||
3. Follow existing namespace conventions — match the module path where the key is used
|
||||
4. Use kebab-case for multi-word segments
|
||||
5. Translations are validated at compile time
|
||||
@@ -1,226 +0,0 @@
|
||||
# rpc-toolkit
|
||||
|
||||
StartOS uses [rpc-toolkit](https://github.com/Start9Labs/rpc-toolkit) for its JSON-RPC API. This document covers the patterns used in this codebase.
|
||||
|
||||
## Overview
|
||||
|
||||
The API is JSON-RPC (not REST). All endpoints are RPC methods organized in a hierarchical command structure.
|
||||
|
||||
## Handler Functions
|
||||
|
||||
There are four types of handler functions, chosen based on the function's characteristics:
|
||||
|
||||
### `from_fn_async` - Async handlers
|
||||
For standard async functions. Most handlers use this.
|
||||
|
||||
```rust
|
||||
pub async fn my_handler(ctx: RpcContext, params: MyParams) -> Result<MyResponse, Error> {
|
||||
// Can use .await
|
||||
}
|
||||
|
||||
from_fn_async(my_handler)
|
||||
```
|
||||
|
||||
### `from_fn_async_local` - Non-thread-safe async handlers
|
||||
For async functions that are not `Send` (cannot be safely moved between threads). Use when working with non-thread-safe types.
|
||||
|
||||
```rust
|
||||
pub async fn cli_download(ctx: CliContext, params: Params) -> Result<(), Error> {
|
||||
// Non-Send async operations
|
||||
}
|
||||
|
||||
from_fn_async_local(cli_download)
|
||||
```
|
||||
|
||||
### `from_fn_blocking` - Sync blocking handlers
|
||||
For synchronous functions that perform blocking I/O or long computations.
|
||||
|
||||
```rust
|
||||
pub fn query_dns(ctx: RpcContext, params: DnsParams) -> Result<DnsResponse, Error> {
|
||||
// Blocking operations (file I/O, DNS lookup, etc.)
|
||||
}
|
||||
|
||||
from_fn_blocking(query_dns)
|
||||
```
|
||||
|
||||
### `from_fn` - Sync non-blocking handlers
|
||||
For pure functions or quick synchronous operations with no I/O.
|
||||
|
||||
```rust
|
||||
pub fn echo(ctx: RpcContext, params: EchoParams) -> Result<String, Error> {
|
||||
Ok(params.message)
|
||||
}
|
||||
|
||||
from_fn(echo)
|
||||
```
|
||||
|
||||
## ParentHandler
|
||||
|
||||
Groups related RPC methods into a hierarchy:
|
||||
|
||||
```rust
|
||||
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
|
||||
|
||||
pub fn my_api<C: Context>() -> ParentHandler<C> {
|
||||
ParentHandler::new()
|
||||
.subcommand("list", from_fn_async(list_handler).with_call_remote::<CliContext>())
|
||||
.subcommand("create", from_fn_async(create_handler).with_call_remote::<CliContext>())
|
||||
}
|
||||
```
|
||||
|
||||
## Handler Extensions
|
||||
|
||||
Chain methods to configure handler behavior.
|
||||
|
||||
**Ordering rules:**
|
||||
1. `with_about()` must come AFTER other CLI modifiers (`no_display()`, `with_custom_display_fn()`, etc.)
|
||||
2. `with_call_remote()` must be the LAST adapter in the chain
|
||||
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `.with_metadata("key", Value)` | Attach metadata for middleware |
|
||||
| `.no_cli()` | RPC-only, not available via CLI |
|
||||
| `.no_display()` | No CLI output |
|
||||
| `.with_display_serializable()` | Default JSON/YAML output for CLI |
|
||||
| `.with_custom_display_fn(\|_, res\| ...)` | Custom CLI output formatting |
|
||||
| `.with_about("about.description")` | Add help text (i18n key) - **after CLI modifiers** |
|
||||
| `.with_call_remote::<CliContext>()` | Enable CLI to call remotely - **must be last** |
|
||||
|
||||
### Correct ordering example:
|
||||
```rust
|
||||
from_fn_async(my_handler)
|
||||
.with_metadata("sync_db", Value::Bool(true)) // metadata early
|
||||
.no_display() // CLI modifier
|
||||
.with_about("about.my-handler") // after CLI modifiers
|
||||
.with_call_remote::<CliContext>() // always last
|
||||
```
|
||||
|
||||
## Metadata by Middleware
|
||||
|
||||
Metadata tags are processed by different middleware. Group them logically:
|
||||
|
||||
### Auth Middleware (`middleware/auth/mod.rs`)
|
||||
|
||||
| Metadata | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `authenticated` | `true` | Whether endpoint requires authentication. Set to `false` for public endpoints. |
|
||||
|
||||
### Session Auth Middleware (`middleware/auth/session.rs`)
|
||||
|
||||
| Metadata | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `login` | `false` | Special handling for login endpoints (rate limiting, cookie setting) |
|
||||
| `get_session` | `false` | Inject session ID into params as `__Auth_session` |
|
||||
|
||||
### Signature Auth Middleware (`middleware/auth/signature.rs`)
|
||||
|
||||
| Metadata | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `get_signer` | `false` | Inject signer public key into params as `__Auth_signer` |
|
||||
|
||||
### Registry Auth (extends Signature Auth)
|
||||
|
||||
| Metadata | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `admin` | `false` | Require admin privileges (signer must be in admin list) |
|
||||
| `get_device_info` | `false` | Inject device info header for hardware filtering |
|
||||
|
||||
### Database Middleware (`middleware/db.rs`)
|
||||
|
||||
| Metadata | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `sync_db` | `false` | Sync database after mutation, add `X-Patch-Sequence` header |
|
||||
|
||||
## Context Types
|
||||
|
||||
Different contexts for different execution environments:
|
||||
|
||||
- `RpcContext` - Web/RPC requests with full service access
|
||||
- `CliContext` - CLI operations, calls remote RPC
|
||||
- `InitContext` - During system initialization
|
||||
- `DiagnosticContext` - Diagnostic/recovery mode
|
||||
- `RegistryContext` - Registry daemon context
|
||||
- `EffectContext` - Service effects context (container-to-host calls)
|
||||
|
||||
## Parameter Structs
|
||||
|
||||
Parameters use derive macros for JSON-RPC, CLI parsing, and TypeScript generation:
|
||||
|
||||
```rust
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
#[serde(rename_all = "camelCase")] // JSON-RPC uses camelCase
|
||||
#[command(rename_all = "kebab-case")] // CLI uses kebab-case
|
||||
#[ts(export)] // Generate TypeScript types
|
||||
pub struct MyParams {
|
||||
pub package_id: PackageId,
|
||||
}
|
||||
```
|
||||
|
||||
### Middleware Injection
|
||||
|
||||
Auth middleware can inject values into params using special field names:
|
||||
|
||||
```rust
|
||||
#[derive(Deserialize, Serialize, Parser, TS)]
|
||||
pub struct MyParams {
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__Auth_session")] // Injected by session auth
|
||||
session: InternedString,
|
||||
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__Auth_signer")] // Injected by signature auth
|
||||
signer: AnyVerifyingKey,
|
||||
|
||||
#[ts(skip)]
|
||||
#[serde(rename = "__Auth_userAgent")] // Injected during login
|
||||
user_agent: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Adding a New RPC Endpoint
|
||||
|
||||
1. Define params struct with `Deserialize, Serialize, Parser, TS`
|
||||
2. Choose handler type based on sync/async and thread-safety
|
||||
3. Write handler function taking `(Context, Params) -> Result<Response, Error>`
|
||||
4. Add to parent handler with appropriate extensions (display modifiers before `with_about`)
|
||||
5. TypeScript types auto-generated via `make ts-bindings`
|
||||
|
||||
### Public (Unauthenticated) Endpoint
|
||||
|
||||
```rust
|
||||
from_fn_async(get_info)
|
||||
.with_metadata("authenticated", Value::Bool(false))
|
||||
.with_display_serializable()
|
||||
.with_about("about.get-info")
|
||||
.with_call_remote::<CliContext>() // last
|
||||
```
|
||||
|
||||
### Mutating Endpoint with DB Sync
|
||||
|
||||
```rust
|
||||
from_fn_async(update_config)
|
||||
.with_metadata("sync_db", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("about.update-config")
|
||||
.with_call_remote::<CliContext>() // last
|
||||
```
|
||||
|
||||
### Session-Aware Endpoint
|
||||
|
||||
```rust
|
||||
from_fn_async(logout)
|
||||
.with_metadata("get_session", Value::Bool(true))
|
||||
.no_display()
|
||||
.with_about("about.logout")
|
||||
.with_call_remote::<CliContext>() // last
|
||||
```
|
||||
|
||||
## File Locations
|
||||
|
||||
- Handler definitions: Throughout `core/src/` modules
|
||||
- Main API tree: `core/src/lib.rs` (`main_api()`, `server()`, `package()`)
|
||||
- Auth middleware: `core/src/middleware/auth/`
|
||||
- DB middleware: `core/src/middleware/db.rs`
|
||||
- Context types: `core/src/context/`
|
||||
@@ -1,122 +0,0 @@
|
||||
# S9PK Package Format
|
||||
|
||||
S9PK is the package format for StartOS services. Version 2 uses a merkle archive structure for efficient downloading and cryptographic verification.
|
||||
|
||||
## File Format
|
||||
|
||||
S9PK files begin with a 3-byte header: `0x3b 0x3b 0x02` (magic bytes + version 2).
|
||||
|
||||
The archive is cryptographically signed using Ed25519 with prehashed content (SHA-512 over blake3 merkle root hash).
|
||||
|
||||
## Archive Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── manifest.json # Package metadata (required)
|
||||
├── icon.<ext> # Package icon - any image/* format (required)
|
||||
├── LICENSE.md # License text (required)
|
||||
├── dependencies/ # Dependency metadata (optional)
|
||||
│ └── <package-id>/
|
||||
│ ├── metadata.json # DependencyMetadata
|
||||
│ └── icon.<ext> # Dependency icon
|
||||
├── javascript.squashfs # Package JavaScript code (required)
|
||||
├── assets.squashfs # Static assets (optional, legacy: assets/ directory)
|
||||
└── images/ # Container images by architecture
|
||||
└── <arch>/ # e.g., x86_64, aarch64, riscv64
|
||||
├── <image-id>.squashfs # Container filesystem
|
||||
├── <image-id>.json # Image metadata
|
||||
└── <image-id>.env # Environment variables
|
||||
```
|
||||
|
||||
## Components
|
||||
|
||||
### manifest.json
|
||||
|
||||
The package manifest contains all metadata:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string | Package identifier (e.g., `bitcoind`) |
|
||||
| `title` | string | Display name |
|
||||
| `version` | string | Extended version string |
|
||||
| `satisfies` | string[] | Version ranges this version satisfies |
|
||||
| `releaseNotes` | string/object | Release notes (localized) |
|
||||
| `canMigrateTo` | string | Version range for forward migration |
|
||||
| `canMigrateFrom` | string | Version range for backward migration |
|
||||
| `license` | string | License type |
|
||||
| `wrapperRepo` | string | StartOS wrapper repository URL |
|
||||
| `upstreamRepo` | string | Upstream project URL |
|
||||
| `supportSite` | string | Support site URL |
|
||||
| `marketingSite` | string | Marketing site URL |
|
||||
| `donationUrl` | string? | Optional donation URL |
|
||||
| `docsUrl` | string? | Optional documentation URL |
|
||||
| `description` | object | Short and long descriptions (localized) |
|
||||
| `images` | object | Image configurations by image ID |
|
||||
| `volumes` | string[] | Volume IDs for persistent data |
|
||||
| `alerts` | object | User alerts for lifecycle events |
|
||||
| `dependencies` | object | Package dependencies |
|
||||
| `hardwareRequirements` | object | Hardware requirements (arch, RAM, devices) |
|
||||
| `hardwareAcceleration` | boolean | Whether package uses hardware acceleration |
|
||||
| `gitHash` | string? | Git commit hash |
|
||||
| `osVersion` | string | Minimum StartOS version |
|
||||
| `sdkVersion` | string? | SDK version used to build |
|
||||
|
||||
### javascript.squashfs
|
||||
|
||||
Contains the package JavaScript that implements the `ABI` interface from `@start9labs/start-sdk-base`. This code runs in the container runtime and manages the package lifecycle.
|
||||
|
||||
The squashfs is mounted at `/usr/lib/startos/package/` and the runtime loads `index.js`.
|
||||
|
||||
### images/
|
||||
|
||||
Container images organized by architecture:
|
||||
|
||||
- **`<image-id>.squashfs`** - Container root filesystem
|
||||
- **`<image-id>.json`** - Image metadata (entrypoint, user, workdir, etc.)
|
||||
- **`<image-id>.env`** - Environment variables for the container
|
||||
|
||||
Images are built from Docker/Podman and converted to squashfs. The `ImageConfig` in manifest specifies:
|
||||
- `arch` - Supported architectures
|
||||
- `emulateMissingAs` - Fallback architecture for emulation
|
||||
- `nvidiaContainer` - Whether to enable NVIDIA container support
|
||||
|
||||
### assets.squashfs
|
||||
|
||||
Static assets accessible to the package, mounted read-only at `/media/startos/assets/` in the container.
|
||||
|
||||
### dependencies/
|
||||
|
||||
Metadata for dependencies displayed in the UI:
|
||||
- `metadata.json` - Just title for now
|
||||
- `icon.<ext>` - Icon for the dependency
|
||||
|
||||
## Merkle Archive
|
||||
|
||||
The S9PK uses a merkle tree structure where each file and directory has a blake3 hash. This enables:
|
||||
|
||||
1. **Partial downloads** - Download and verify individual files
|
||||
2. **Integrity verification** - Verify any subset of the archive
|
||||
3. **Efficient updates** - Only download changed portions
|
||||
4. **DOS protection** - Size limits enforced before downloading content
|
||||
|
||||
Files are sorted by priority for streaming (manifest first, then icon, license, dependencies, javascript, assets, images).
|
||||
|
||||
## Building S9PK
|
||||
|
||||
Use `start-cli s9pk pack` to build packages:
|
||||
|
||||
```bash
|
||||
start-cli s9pk pack <manifest-path> -o <output.s9pk>
|
||||
```
|
||||
|
||||
Images can be sourced from:
|
||||
- Docker/Podman build (`--docker-build`)
|
||||
- Existing Docker tag (`--docker-tag`)
|
||||
- Pre-built squashfs files
|
||||
|
||||
## Related Code
|
||||
|
||||
- `core/src/s9pk/v2/mod.rs` - S9pk struct and serialization
|
||||
- `core/src/s9pk/v2/manifest.rs` - Manifest types
|
||||
- `core/src/s9pk/v2/pack.rs` - Packing logic
|
||||
- `core/src/s9pk/merkle_archive/` - Merkle archive implementation
|
||||
@@ -46,7 +46,6 @@ openssh-server
|
||||
podman
|
||||
psmisc
|
||||
qemu-guest-agent
|
||||
qemu-user-static
|
||||
rfkill
|
||||
rsync
|
||||
samba-common-bin
|
||||
|
||||
@@ -111,6 +111,6 @@ if [ "$CHROOT_RES" -eq 0 ]; then
|
||||
reboot
|
||||
fi
|
||||
|
||||
umount /media/startos/next
|
||||
umount -R /media/startos/next
|
||||
umount /media/startos/upper
|
||||
rm -rf /media/startos/upper /media/startos/next
|
||||
@@ -15,12 +15,13 @@ if [ "$SKIP_DL" != "1" ]; then
|
||||
fi
|
||||
|
||||
if [ -n "$RUN_ID" ]; then
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||
for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree raspberrypi; do
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.squashfs -D $(pwd); do sleep 1; done
|
||||
done
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||
for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree; do
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n $arch.iso -D $(pwd); do sleep 1; done
|
||||
done
|
||||
while ! gh run download -R Start9Labs/start-os $RUN_ID -n raspberrypi.img -D $(pwd); do sleep 1; done
|
||||
fi
|
||||
|
||||
if [ -n "$ST_RUN_ID" ]; then
|
||||
@@ -56,23 +57,31 @@ start-cli --registry=https://alpha-registry-x.start9.com registry os version add
|
||||
if [ "$SKIP_UL" = "2" ]; then
|
||||
exit 2
|
||||
elif [ "$SKIP_UL" != "1" ]; then
|
||||
for file in *.deb start-cli_*; do
|
||||
for file in *.squashfs *.iso *.deb start-cli_*; do
|
||||
gh release upload -R Start9Labs/start-os v$VERSION $file
|
||||
done
|
||||
for file in *.iso *.squashfs; do
|
||||
s3cmd put -P $file s3://startos-images/v$VERSION/$file
|
||||
for file in *.img; do
|
||||
if ! [ -f $file.gz ]; then
|
||||
cat $file | pigz > $file.gz
|
||||
fi
|
||||
gh release upload -R Start9Labs/start-os v$VERSION $file.gz
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$SKIP_INDEX" != "1" ]; then
|
||||
for arch in aarch64 aarch64-nonfree riscv64 x86_64 x86_64-nonfree; do
|
||||
for arch in aarch64 aarch64-nonfree riscv64 riscv64-nonfree x86_64 x86_64-nonfree; do
|
||||
for file in *_$arch.squashfs *_$arch.iso; do
|
||||
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$file
|
||||
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://github.com/Start9Labs/start-os/releases/download/v$VERSION/$(echo -n "$file" | sed 's/~/./g')
|
||||
done
|
||||
done
|
||||
for arch in raspberrypi; do
|
||||
for file in *_$arch.squashfs; do
|
||||
start-cli --registry=https://alpha-registry-x.start9.com registry os asset add --platform=$arch --version=$VERSION $file https://github.com/Start9Labs/start-os/releases/download/v$VERSION/$(echo -n "$file" | sed 's/~/./g')
|
||||
done
|
||||
done
|
||||
fi
|
||||
|
||||
for file in *.iso *.squashfs *.deb start-cli_*; do
|
||||
for file in *.iso *.img *.img.gz *.squashfs *.deb start-cli_*; do
|
||||
gpg -u 7CFFDA41CA66056A --detach-sign --armor -o "${file}.asc" "$file"
|
||||
done
|
||||
|
||||
@@ -81,30 +90,20 @@ tar -czvf signatures.tar.gz *.asc
|
||||
|
||||
gh release upload -R Start9Labs/start-os v$VERSION signatures.tar.gz
|
||||
|
||||
cat << EOF
|
||||
# ISO Downloads
|
||||
|
||||
- [x86_64/AMD64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64-nonfree.iso))
|
||||
- [x86_64/AMD64-slim (FOSS-only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_x86_64.iso) "Without proprietary software or drivers")
|
||||
- [aarch64/ARM64](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64-nonfree.iso))
|
||||
- [aarch64/ARM64-slim (FOSS-Only)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_aarch64.iso) "Without proprietary software or drivers")
|
||||
- [RISCV64 (RVA23)](https://startos-images.nyc3.cdn.digitaloceanspaces.com/v$VERSION/$(ls *_riscv64.iso))
|
||||
|
||||
EOF
|
||||
cat << 'EOF'
|
||||
# StartOS Checksums
|
||||
|
||||
## SHA-256
|
||||
```
|
||||
EOF
|
||||
sha256sum *.iso *.squashfs
|
||||
sha256sum *.iso *.img *img.gz *.squashfs
|
||||
cat << 'EOF'
|
||||
```
|
||||
|
||||
## BLAKE-3
|
||||
```
|
||||
EOF
|
||||
b3sum *.iso *.squashfs
|
||||
b3sum *.iso *.img *.img.gz *.squashfs
|
||||
cat << 'EOF'
|
||||
```
|
||||
|
||||
@@ -139,4 +138,5 @@ EOF
|
||||
b3sum start-cli_*
|
||||
cat << 'EOF'
|
||||
```
|
||||
EOF
|
||||
EOF
|
||||
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
# Container RPC Server Specification
|
||||
|
||||
The container runtime exposes a JSON-RPC server over a Unix socket at `/media/startos/rpc/service.sock`.
|
||||
# Container RPC SERVER Specification
|
||||
|
||||
## Methods
|
||||
|
||||
### init
|
||||
|
||||
Initialize the runtime and system.
|
||||
initialize runtime (mount `/proc`, `/sys`, `/dev`, and `/run` to each image in `/media/images`)
|
||||
|
||||
#### params
|
||||
called after os has mounted js and images to the container
|
||||
|
||||
```ts
|
||||
{
|
||||
id: string,
|
||||
kind: "install" | "update" | "restore" | null,
|
||||
}
|
||||
```
|
||||
#### args
|
||||
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
@@ -23,16 +18,11 @@ Initialize the runtime and system.
|
||||
|
||||
### exit
|
||||
|
||||
Shutdown runtime and optionally run exit hooks for a target version.
|
||||
shutdown runtime
|
||||
|
||||
#### params
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
id: string,
|
||||
target: string | null, // ExtendedVersion or VersionRange
|
||||
}
|
||||
```
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
@@ -40,11 +30,11 @@ Shutdown runtime and optionally run exit hooks for a target version.
|
||||
|
||||
### start
|
||||
|
||||
Run main method if not already running.
|
||||
run main method if not already running
|
||||
|
||||
#### params
|
||||
#### args
|
||||
|
||||
None
|
||||
`[]`
|
||||
|
||||
#### response
|
||||
|
||||
@@ -52,11 +42,11 @@ None
|
||||
|
||||
### stop
|
||||
|
||||
Stop main method by sending SIGTERM to child processes, and SIGKILL after timeout.
|
||||
stop main method by sending SIGTERM to child processes, and SIGKILL after timeout
|
||||
|
||||
#### params
|
||||
#### args
|
||||
|
||||
None
|
||||
`{ timeout: millis }`
|
||||
|
||||
#### response
|
||||
|
||||
@@ -64,16 +54,15 @@ None
|
||||
|
||||
### execute
|
||||
|
||||
Run a specific package procedure.
|
||||
run a specific package procedure
|
||||
|
||||
#### params
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
id: string, // event ID
|
||||
procedure: string, // JSON path (e.g., "/backup/create", "/actions/{name}/run")
|
||||
input: any,
|
||||
timeout: number | null,
|
||||
procedure: JsonPath,
|
||||
input: any,
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -83,64 +72,18 @@ Run a specific package procedure.
|
||||
|
||||
### sandbox
|
||||
|
||||
Run a specific package procedure in sandbox mode. Same interface as `execute`.
|
||||
run a specific package procedure in sandbox mode
|
||||
|
||||
UNIMPLEMENTED: this feature is planned but does not exist
|
||||
|
||||
#### params
|
||||
#### args
|
||||
|
||||
```ts
|
||||
{
|
||||
id: string,
|
||||
procedure: string,
|
||||
input: any,
|
||||
timeout: number | null,
|
||||
procedure: JsonPath,
|
||||
input: any,
|
||||
timeout: millis,
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`any`
|
||||
|
||||
### callback
|
||||
|
||||
Handle a callback from an effect.
|
||||
|
||||
#### params
|
||||
|
||||
```ts
|
||||
{
|
||||
id: number,
|
||||
args: any[],
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`null` (no response sent)
|
||||
|
||||
### eval
|
||||
|
||||
Evaluate a script in the runtime context. Used for debugging.
|
||||
|
||||
#### params
|
||||
|
||||
```ts
|
||||
{
|
||||
script: string,
|
||||
}
|
||||
```
|
||||
|
||||
#### response
|
||||
|
||||
`any`
|
||||
|
||||
## Procedures
|
||||
|
||||
The `execute` and `sandbox` methods route to procedures based on the `procedure` path:
|
||||
|
||||
| Procedure | Description |
|
||||
|-----------|-------------|
|
||||
| `/backup/create` | Create a backup |
|
||||
| `/actions/{name}/getInput` | Get input spec for an action |
|
||||
| `/actions/{name}/run` | Run an action with input |
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -7817,7 +7817,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "start-os"
|
||||
version = "0.4.0-alpha.19"
|
||||
version = "0.4.0-alpha.18"
|
||||
dependencies = [
|
||||
"aes 0.7.5",
|
||||
"arti-client",
|
||||
|
||||
@@ -15,7 +15,7 @@ license = "MIT"
|
||||
name = "start-os"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/Start9Labs/start-os"
|
||||
version = "0.4.0-alpha.19" # VERSION_BUMP
|
||||
version = "0.4.0-alpha.18" # VERSION_BUMP
|
||||
|
||||
[lib]
|
||||
name = "startos"
|
||||
@@ -176,7 +176,6 @@ mio = "1"
|
||||
new_mime_guess = "4"
|
||||
nix = { version = "0.30.1", features = [
|
||||
"fs",
|
||||
"hostname",
|
||||
"mount",
|
||||
"net",
|
||||
"process",
|
||||
|
||||
@@ -1843,18 +1843,18 @@ service.mod.failed-to-parse-package-data-entry:
|
||||
pl_PL: "Nie udało się przeanalizować PackageDataEntry, znaleziono: %{error}"
|
||||
|
||||
service.mod.no-matching-subcontainers:
|
||||
en_US: "no matching subcontainers are running for %{id}; some possible choices are:"
|
||||
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:"
|
||||
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:"
|
||||
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :"
|
||||
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:"
|
||||
en_US: "no matching subcontainers are running for %{id}; some possible choices are:\n%{subcontainers}"
|
||||
de_DE: "keine passenden Subcontainer laufen für %{id}; einige mögliche Optionen sind:\n%{subcontainers}"
|
||||
es_ES: "no hay subcontenedores coincidentes ejecutándose para %{id}; algunas opciones posibles son:\n%{subcontainers}"
|
||||
fr_FR: "aucun sous-conteneur correspondant n'est en cours d'exécution pour %{id} ; voici quelques choix possibles :\n%{subcontainers}"
|
||||
pl_PL: "nie działają pasujące podkontenery dla %{id}; niektóre możliwe wybory to:\n%{subcontainers}"
|
||||
|
||||
service.mod.multiple-subcontainers-found:
|
||||
en_US: "multiple subcontainers found for %{id}"
|
||||
de_DE: "mehrere Subcontainer für %{id} gefunden"
|
||||
es_ES: "se encontraron múltiples subcontenedores para %{id}"
|
||||
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id}"
|
||||
pl_PL: "znaleziono wiele podkontenerów dla %{id}"
|
||||
en_US: "multiple subcontainers found for %{id}: \n%{subcontainer_ids}"
|
||||
de_DE: "mehrere Subcontainer für %{id} gefunden: \n%{subcontainer_ids}"
|
||||
es_ES: "se encontraron múltiples subcontenedores para %{id}: \n%{subcontainer_ids}"
|
||||
fr_FR: "plusieurs sous-conteneurs trouvés pour %{id} : \n%{subcontainer_ids}"
|
||||
pl_PL: "znaleziono wiele podkontenerów dla %{id}: \n%{subcontainer_ids}"
|
||||
|
||||
service.mod.invalid-byte-length-for-signal:
|
||||
en_US: "invalid byte length for signal: %{length}"
|
||||
@@ -3703,20 +3703,6 @@ help.arg.wireguard-config:
|
||||
fr_FR: "Configuration WireGuard"
|
||||
pl_PL: "Konfiguracja WireGuard"
|
||||
|
||||
help.s9pk-s3base:
|
||||
en_US: "Base URL for publishing s9pks"
|
||||
de_DE: "Basis-URL für die Veröffentlichung von s9pks"
|
||||
es_ES: "URL base para publicar s9pks"
|
||||
fr_FR: "URL de base pour publier les s9pks"
|
||||
pl_PL: "Bazowy URL do publikowania s9pks"
|
||||
|
||||
help.s9pk-s3bucket:
|
||||
en_US: "S3 bucket to publish s9pks to (should correspond to s3base)"
|
||||
de_DE: "S3-Bucket zum Veröffentlichen von s9pks (sollte mit s3base übereinstimmen)"
|
||||
es_ES: "Bucket S3 para publicar s9pks (debe corresponder con s3base)"
|
||||
fr_FR: "Bucket S3 pour publier les s9pks (doit correspondre à s3base)"
|
||||
pl_PL: "Bucket S3 do publikowania s9pks (powinien odpowiadać s3base)"
|
||||
|
||||
# CLI command descriptions (about.*)
|
||||
about.add-address-to-host:
|
||||
en_US: "Add an address to this host"
|
||||
@@ -4880,13 +4866,6 @@ about.persist-new-notification:
|
||||
fr_FR: "Persister une nouvelle notification"
|
||||
pl_PL: "Utrwal nowe powiadomienie"
|
||||
|
||||
about.publish-s9pk:
|
||||
en_US: "Publish s9pk to S3 bucket and index on registry"
|
||||
de_DE: "S9pk in S3-Bucket veröffentlichen und in Registry indizieren"
|
||||
es_ES: "Publicar s9pk en bucket S3 e indexar en el registro"
|
||||
fr_FR: "Publier s9pk dans le bucket S3 et indexer dans le registre"
|
||||
pl_PL: "Opublikuj s9pk do bucketu S3 i zindeksuj w rejestrze"
|
||||
|
||||
about.rebuild-service-container:
|
||||
en_US: "Rebuild service container"
|
||||
de_DE: "Dienst-Container neu erstellen"
|
||||
|
||||
@@ -180,13 +180,7 @@ pub async fn update(
|
||||
.as_idx_mut(&id)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"backup.target.cifs.target-not-found",
|
||||
id = BackupTargetId::Cifs { id }
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("backup.target.cifs.target-not-found", id = BackupTargetId::Cifs { id })),
|
||||
ErrorKind::NotFound,
|
||||
)
|
||||
})?
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use rust_i18n::t;
|
||||
|
||||
pub fn renamed(old: &str, new: &str) -> ! {
|
||||
eprintln!("{}", t!("bins.deprecated.renamed", old = old, new = new));
|
||||
eprintln!(
|
||||
"{}",
|
||||
t!("bins.deprecated.renamed", old = old, new = new)
|
||||
);
|
||||
std::process::exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use color_eyre::eyre::eyre;
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use rust_i18n::t;
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use tokio::signal::unix::signal;
|
||||
use tracing::instrument;
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@ pub struct CliContextSeed {
|
||||
pub registry_url: Option<Url>,
|
||||
pub registry_hostname: Vec<InternedString>,
|
||||
pub registry_listen: Option<SocketAddr>,
|
||||
pub s9pk_s3base: Option<Url>,
|
||||
pub s9pk_s3bucket: Option<InternedString>,
|
||||
pub tunnel_addr: Option<SocketAddr>,
|
||||
pub tunnel_listen: Option<SocketAddr>,
|
||||
pub client: Client,
|
||||
@@ -131,8 +129,6 @@ impl CliContext {
|
||||
.transpose()?,
|
||||
registry_hostname: config.registry_hostname.unwrap_or_default(),
|
||||
registry_listen: config.registry_listen,
|
||||
s9pk_s3base: config.s9pk_s3base,
|
||||
s9pk_s3bucket: config.s9pk_s3bucket,
|
||||
tunnel_addr: config.tunnel,
|
||||
tunnel_listen: config.tunnel_listen,
|
||||
client: {
|
||||
@@ -164,23 +160,21 @@ impl CliContext {
|
||||
if !path.exists() {
|
||||
continue;
|
||||
}
|
||||
let pair =
|
||||
<ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem(
|
||||
&std::fs::read_to_string(path)?,
|
||||
let pair = <ed25519::KeypairBytes as ed25519::pkcs8::DecodePrivateKey>::from_pkcs8_pem(
|
||||
&std::fs::read_to_string(path)?,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Pem)?;
|
||||
let secret = ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("context.cli.pkcs8-key-incorrect-length")),
|
||||
ErrorKind::OpenSsl,
|
||||
)
|
||||
.with_kind(crate::ErrorKind::Pem)?;
|
||||
let secret =
|
||||
ed25519_dalek::SecretKey::try_from(&pair.secret_key[..]).map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("context.cli.pkcs8-key-incorrect-length")),
|
||||
ErrorKind::OpenSsl,
|
||||
)
|
||||
})?;
|
||||
return Ok(secret.into());
|
||||
})?;
|
||||
return Ok(secret.into())
|
||||
}
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("context.cli.developer-key-does-not-exist")),
|
||||
crate::ErrorKind::Uninitialized,
|
||||
crate::ErrorKind::Uninitialized
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -201,12 +195,8 @@ impl CliContext {
|
||||
.into());
|
||||
}
|
||||
};
|
||||
url.set_scheme(ws_scheme).map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("context.cli.cannot-set-url-scheme")),
|
||||
crate::ErrorKind::ParseUrl,
|
||||
)
|
||||
})?;
|
||||
url.set_scheme(ws_scheme)
|
||||
.map_err(|_| Error::new(eyre!("{}", t!("context.cli.cannot-set-url-scheme")), crate::ErrorKind::ParseUrl))?;
|
||||
url.path_segments_mut()
|
||||
.map_err(|_| eyre!("Url cannot be base"))
|
||||
.with_kind(crate::ErrorKind::ParseUrl)?
|
||||
|
||||
@@ -68,10 +68,6 @@ pub struct ClientConfig {
|
||||
pub registry_hostname: Option<Vec<InternedString>>,
|
||||
#[arg(skip)]
|
||||
pub registry_listen: Option<SocketAddr>,
|
||||
#[arg(long, help = "help.s9pk-s3base")]
|
||||
pub s9pk_s3base: Option<Url>,
|
||||
#[arg(long, help = "help.s9pk-s3bucket")]
|
||||
pub s9pk_s3bucket: Option<InternedString>,
|
||||
#[arg(short = 't', long, help = "help.arg.tunnel-address")]
|
||||
pub tunnel: Option<SocketAddr>,
|
||||
#[arg(skip)]
|
||||
@@ -93,13 +89,8 @@ impl ContextConfig for ClientConfig {
|
||||
self.host = self.host.take().or(other.host);
|
||||
self.registry = self.registry.take().or(other.registry);
|
||||
self.registry_hostname = self.registry_hostname.take().or(other.registry_hostname);
|
||||
self.registry_listen = self.registry_listen.take().or(other.registry_listen);
|
||||
self.s9pk_s3base = self.s9pk_s3base.take().or(other.s9pk_s3base);
|
||||
self.s9pk_s3bucket = self.s9pk_s3bucket.take().or(other.s9pk_s3bucket);
|
||||
self.tunnel = self.tunnel.take().or(other.tunnel);
|
||||
self.tunnel_listen = self.tunnel_listen.take().or(other.tunnel_listen);
|
||||
self.proxy = self.proxy.take().or(other.proxy);
|
||||
self.socks_listen = self.socks_listen.take().or(other.socks_listen);
|
||||
self.cookie_path = self.cookie_path.take().or(other.cookie_path);
|
||||
self.developer_key_path = self.developer_key_path.take().or(other.developer_key_path);
|
||||
}
|
||||
|
||||
@@ -27,10 +27,7 @@ impl DiagnosticContext {
|
||||
disk_guid: Option<InternedString>,
|
||||
error: Error,
|
||||
) -> Result<Self, Error> {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("context.diagnostic.starting-diagnostic-ui", error = error)
|
||||
);
|
||||
tracing::error!("{}", t!("context.diagnostic.starting-diagnostic-ui", error = error));
|
||||
tracing::debug!("{:?}", error);
|
||||
|
||||
let (shutdown, _) = tokio::sync::broadcast::channel(1);
|
||||
|
||||
@@ -463,10 +463,7 @@ impl RpcContext {
|
||||
.await
|
||||
.result
|
||||
{
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("context.rpc.error-in-session-cleanup-cron", error = e)
|
||||
);
|
||||
tracing::error!("{}", t!("context.rpc.error-in-session-cleanup-cron", error = e));
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
}
|
||||
@@ -579,7 +576,6 @@ impl RpcContext {
|
||||
pub async fn call_remote<RemoteContext>(
|
||||
&self,
|
||||
method: &str,
|
||||
metadata: OrdMap<&'static str, Value>,
|
||||
params: Value,
|
||||
) -> Result<Value, RpcError>
|
||||
where
|
||||
@@ -588,7 +584,7 @@ impl RpcContext {
|
||||
<Self as CallRemote<RemoteContext, Empty>>::call_remote(
|
||||
&self,
|
||||
method,
|
||||
metadata,
|
||||
OrdMap::new(),
|
||||
params,
|
||||
Empty {},
|
||||
)
|
||||
@@ -597,15 +593,20 @@ impl RpcContext {
|
||||
pub async fn call_remote_with<RemoteContext, T>(
|
||||
&self,
|
||||
method: &str,
|
||||
metadata: OrdMap<&'static str, Value>,
|
||||
params: Value,
|
||||
extra: T,
|
||||
) -> Result<Value, RpcError>
|
||||
where
|
||||
Self: CallRemote<RemoteContext, T>,
|
||||
{
|
||||
<Self as CallRemote<RemoteContext, T>>::call_remote(&self, method, metadata, params, extra)
|
||||
.await
|
||||
<Self as CallRemote<RemoteContext, T>>::call_remote(
|
||||
&self,
|
||||
method,
|
||||
OrdMap::new(),
|
||||
params,
|
||||
extra,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
impl AsRef<Client> for RpcContext {
|
||||
|
||||
@@ -87,11 +87,7 @@ pub enum RevisionsRes {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct CliDumpParams {
|
||||
#[arg(
|
||||
long = "include-private",
|
||||
short = 'p',
|
||||
help = "help.arg.include-private-data"
|
||||
)]
|
||||
#[arg(long = "include-private", short = 'p', help = "help.arg.include-private-data")]
|
||||
#[serde(default)]
|
||||
include_private: bool,
|
||||
#[arg(help = "help.arg.db-path")]
|
||||
|
||||
@@ -70,20 +70,12 @@ async fn e2fsck_runner(
|
||||
if code & 4 != 0 {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"disk.fsck.errors-not-corrected",
|
||||
device = logicalname.as_ref().display(),
|
||||
stderr = e2fsck_stderr
|
||||
),
|
||||
t!("disk.fsck.errors-not-corrected", device = logicalname.as_ref().display(), stderr = e2fsck_stderr),
|
||||
);
|
||||
} else if code & 1 != 0 {
|
||||
tracing::warn!(
|
||||
"{}",
|
||||
t!(
|
||||
"disk.fsck.errors-corrected",
|
||||
device = logicalname.as_ref().display(),
|
||||
stderr = e2fsck_stderr
|
||||
),
|
||||
t!("disk.fsck.errors-corrected", device = logicalname.as_ref().display(), stderr = e2fsck_stderr),
|
||||
);
|
||||
}
|
||||
if code < 8 {
|
||||
|
||||
@@ -29,31 +29,25 @@ impl Default for FileType {
|
||||
pub struct Bind<Src: AsRef<Path>> {
|
||||
src: Src,
|
||||
filetype: FileType,
|
||||
recursive: bool,
|
||||
}
|
||||
impl<Src: AsRef<Path>> Bind<Src> {
|
||||
pub fn new(src: Src) -> Self {
|
||||
Self {
|
||||
src,
|
||||
filetype: FileType::Directory,
|
||||
recursive: false,
|
||||
}
|
||||
}
|
||||
pub fn with_type(mut self, filetype: FileType) -> Self {
|
||||
self.filetype = filetype;
|
||||
self
|
||||
}
|
||||
pub fn recursive(mut self, recursive: bool) -> Self {
|
||||
self.recursive = recursive;
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<Src: AsRef<Path> + Send + Sync> FileSystem for Bind<Src> {
|
||||
async fn source(&self) -> Result<Option<impl AsRef<Path>>, Error> {
|
||||
Ok(Some(&self.src))
|
||||
}
|
||||
fn extra_args(&self) -> impl IntoIterator<Item = impl AsRef<std::ffi::OsStr>> {
|
||||
[if self.recursive { "--rbind" } else { "--bind" }]
|
||||
["--bind"]
|
||||
}
|
||||
async fn pre_mount(&self, mountpoint: &Path, mount_type: MountType) -> Result<(), Error> {
|
||||
let from_meta = tokio::fs::metadata(&self.src).await.ok();
|
||||
|
||||
@@ -24,11 +24,7 @@ pub async fn bind<P0: AsRef<Path>, P1: AsRef<Path>>(
|
||||
) -> Result<(), Error> {
|
||||
tracing::info!(
|
||||
"{}",
|
||||
t!(
|
||||
"disk.mount.binding",
|
||||
src = src.as_ref().display(),
|
||||
dst = dst.as_ref().display()
|
||||
)
|
||||
t!("disk.mount.binding", src = src.as_ref().display(), dst = dst.as_ref().display())
|
||||
);
|
||||
if is_mountpoint(&dst).await? {
|
||||
unmount(dst.as_ref(), true).await?;
|
||||
|
||||
@@ -4,7 +4,7 @@ use axum::http::StatusCode;
|
||||
use axum::http::uri::InvalidUri;
|
||||
use color_eyre::eyre::eyre;
|
||||
use num_enum::TryFromPrimitive;
|
||||
use patch_db::Value;
|
||||
use patch_db::Revision;
|
||||
use rpc_toolkit::reqwest;
|
||||
use rpc_toolkit::yajrc::{
|
||||
INVALID_PARAMS_ERROR, INVALID_REQUEST_ERROR, METHOD_NOT_FOUND_ERROR, PARSE_ERROR, RpcError,
|
||||
@@ -16,7 +16,6 @@ use tokio_rustls::rustls;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::InvalidId;
|
||||
use crate::prelude::to_value;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive)]
|
||||
#[repr(i32)]
|
||||
@@ -184,8 +183,7 @@ impl ErrorKind {
|
||||
UpdateFailed => t!("error.update-failed"),
|
||||
Smtp => t!("error.smtp"),
|
||||
SetSysInfo => t!("error.set-sys-info"),
|
||||
}
|
||||
.to_string()
|
||||
}.to_string()
|
||||
}
|
||||
}
|
||||
impl Display for ErrorKind {
|
||||
@@ -198,7 +196,7 @@ pub struct Error {
|
||||
pub source: color_eyre::eyre::Error,
|
||||
pub debug: Option<color_eyre::eyre::Error>,
|
||||
pub kind: ErrorKind,
|
||||
pub info: Value,
|
||||
pub revision: Option<Revision>,
|
||||
pub task: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
@@ -229,7 +227,7 @@ impl Error {
|
||||
source: source.into(),
|
||||
debug,
|
||||
kind,
|
||||
info: Value::Null,
|
||||
revision: None,
|
||||
task: None,
|
||||
}
|
||||
}
|
||||
@@ -238,7 +236,7 @@ impl Error {
|
||||
source: eyre!("{}", self.source),
|
||||
debug: self.debug.as_ref().map(|e| eyre!("{e}")),
|
||||
kind: self.kind,
|
||||
info: self.info.clone(),
|
||||
revision: self.revision.clone(),
|
||||
task: None,
|
||||
}
|
||||
}
|
||||
@@ -246,10 +244,6 @@ impl Error {
|
||||
self.task = Some(task);
|
||||
self
|
||||
}
|
||||
pub fn with_info(mut self, info: Value) -> Self {
|
||||
self.info = info;
|
||||
self
|
||||
}
|
||||
pub async fn wait(mut self) -> Self {
|
||||
if let Some(task) = &mut self.task {
|
||||
task.await.log_err();
|
||||
@@ -428,8 +422,6 @@ impl From<patch_db::value::Error> for Error {
|
||||
pub struct ErrorData {
|
||||
pub details: String,
|
||||
pub debug: String,
|
||||
#[serde(default)]
|
||||
pub info: Value,
|
||||
}
|
||||
impl Display for ErrorData {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
@@ -447,7 +439,6 @@ impl From<Error> for ErrorData {
|
||||
Self {
|
||||
details: value.to_string(),
|
||||
debug: format!("{:?}", value),
|
||||
info: value.info,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,31 +469,40 @@ impl From<&RpcError> for ErrorData {
|
||||
.or_else(|| d.as_str().map(|s| s.to_owned()))
|
||||
})
|
||||
.unwrap_or_else(|| value.message.clone().into_owned()),
|
||||
info: to_value(
|
||||
&value
|
||||
.data
|
||||
.as_ref()
|
||||
.and_then(|d| d.as_object().and_then(|d| d.get("info"))),
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for RpcError {
|
||||
fn from(e: Error) -> Self {
|
||||
let kind = e.kind;
|
||||
let data = ErrorData::from(e);
|
||||
RpcError {
|
||||
code: kind as i32,
|
||||
message: kind.as_str().into(),
|
||||
data: Some(match serde_json::to_value(&data) {
|
||||
let mut data_object = serde_json::Map::with_capacity(3);
|
||||
data_object.insert("details".to_owned(), format!("{}", e.source).into());
|
||||
data_object.insert("debug".to_owned(), format!("{:?}", e.source).into());
|
||||
data_object.insert(
|
||||
"revision".to_owned(),
|
||||
match serde_json::to_value(&e.revision) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error serializing ErrorData object: {}", e);
|
||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
}),
|
||||
},
|
||||
);
|
||||
RpcError {
|
||||
code: e.kind as i32,
|
||||
message: e.kind.as_str().into(),
|
||||
data: Some(
|
||||
match serde_json::to_value(&ErrorData {
|
||||
details: format!("{}", e.source),
|
||||
debug: format!("{:?}", e.source),
|
||||
}) {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
tracing::warn!("Error serializing revision for Error object: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -605,7 +605,7 @@ where
|
||||
kind,
|
||||
source,
|
||||
debug,
|
||||
info: Value::Null,
|
||||
revision: None,
|
||||
task: None,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -131,9 +131,6 @@ pub async fn install(
|
||||
let package: GetPackageResponse = from_value(
|
||||
ctx.call_remote_with::<RegistryContext, _>(
|
||||
"package.get",
|
||||
[("get_device_info", Value::Bool(true))]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
json!({
|
||||
"id": id,
|
||||
"targetVersion": VersionRange::exactly(version.deref().clone()),
|
||||
|
||||
@@ -540,10 +540,7 @@ pub fn package<C: Context>() -> ParentHandler<C> {
|
||||
.with_about("about.execute-commands-container")
|
||||
.no_cli(),
|
||||
)
|
||||
.subcommand(
|
||||
"attach",
|
||||
from_fn_async_local(service::cli_attach).no_display(),
|
||||
)
|
||||
.subcommand("attach", from_fn_async(service::cli_attach).no_display())
|
||||
.subcommand(
|
||||
"host",
|
||||
net::host::host_api::<C>().with_about("about.manage-network-hosts-package"),
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
|
||||
use axum::extract::ws;
|
||||
use crate::util::net::WebSocket;
|
||||
use chrono::{DateTime, Utc};
|
||||
use clap::builder::ValueParserFactory;
|
||||
use clap::{Args, FromArgMatches, Parser};
|
||||
@@ -30,7 +31,6 @@ use crate::context::{CliContext, RpcContext};
|
||||
use crate::error::ResultExt;
|
||||
use crate::prelude::*;
|
||||
use crate::rpc_continuations::{Guid, RpcContinuation, RpcContinuations};
|
||||
use crate::util::net::WebSocket;
|
||||
use crate::util::serde::Reversible;
|
||||
use crate::util::{FromStrParser, Invoke};
|
||||
|
||||
@@ -330,22 +330,12 @@ pub struct LogsParams<Extra: FromArgMatches + Args = Empty> {
|
||||
extra: Extra,
|
||||
#[arg(short = 'l', long = "limit", help = "help.arg.log-limit")]
|
||||
limit: Option<usize>,
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "cursor",
|
||||
conflicts_with = "follow",
|
||||
help = "help.arg.log-cursor"
|
||||
)]
|
||||
#[arg(short = 'c', long = "cursor", conflicts_with = "follow", help = "help.arg.log-cursor")]
|
||||
cursor: Option<String>,
|
||||
#[arg(short = 'b', long = "boot", help = "help.arg.log-boot")]
|
||||
#[serde(default)]
|
||||
boot: Option<BootIdentifier>,
|
||||
#[arg(
|
||||
short = 'B',
|
||||
long = "before",
|
||||
conflicts_with = "follow",
|
||||
help = "help.arg.log-before"
|
||||
)]
|
||||
#[arg(short = 'B', long = "before", conflicts_with = "follow", help = "help.arg.log-before")]
|
||||
#[serde(default)]
|
||||
before: bool,
|
||||
}
|
||||
@@ -563,12 +553,10 @@ pub async fn journalctl(
|
||||
follow_cmd.arg("--lines=0");
|
||||
}
|
||||
let mut child = follow_cmd.stdout(Stdio::piped()).spawn()?;
|
||||
let out = BufReader::new(child.stdout.take().ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("logs.no-stdout-available")),
|
||||
crate::ErrorKind::Journald,
|
||||
)
|
||||
})?);
|
||||
let out =
|
||||
BufReader::new(child.stdout.take().ok_or_else(|| {
|
||||
Error::new(eyre!("{}", t!("logs.no-stdout-available")), crate::ErrorKind::Journald)
|
||||
})?);
|
||||
|
||||
let journalctl_entries = LinesStream::new(out.lines());
|
||||
|
||||
@@ -713,10 +701,7 @@ pub async fn follow_logs<Context: AsRef<RpcContinuations>>(
|
||||
RpcContinuation::ws(
|
||||
move |socket| async move {
|
||||
if let Err(e) = ws_handler(first_entry, stream, socket).await {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("logs.error-in-log-stream", error = e.to_string())
|
||||
);
|
||||
tracing::error!("{}", t!("logs.error-in-log-stream", error = e.to_string()));
|
||||
}
|
||||
},
|
||||
Duration::from_secs(30),
|
||||
|
||||
@@ -40,10 +40,7 @@ impl LocalAuthContext for RpcContext {
|
||||
}
|
||||
|
||||
fn unauthorized() -> Error {
|
||||
Error::new(
|
||||
eyre!("{}", t!("middleware.auth.unauthorized")),
|
||||
crate::ErrorKind::Authorization,
|
||||
)
|
||||
Error::new(eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization)
|
||||
}
|
||||
|
||||
async fn check_from_header<C: LocalAuthContext>(header: Option<&HeaderValue>) -> Result<(), Error> {
|
||||
|
||||
@@ -244,10 +244,7 @@ impl ValidSessionToken {
|
||||
C::access_sessions(db)
|
||||
.as_idx_mut(session_hash)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("middleware.auth.unauthorized")),
|
||||
crate::ErrorKind::Authorization,
|
||||
)
|
||||
Error::new(eyre!("{}", t!("middleware.auth.unauthorized")), crate::ErrorKind::Authorization)
|
||||
})?
|
||||
.mutate(|s| {
|
||||
s.last_active = Utc::now();
|
||||
|
||||
@@ -347,10 +347,6 @@ pub async fn call_remote<Ctx: SigningContext + AsRef<Client>>(
|
||||
.with_kind(ErrorKind::Deserialization)?
|
||||
.result
|
||||
}
|
||||
_ => Err(Error::new(
|
||||
eyre!("{}", t!("middleware.auth.unknown-content-type")),
|
||||
ErrorKind::Network,
|
||||
)
|
||||
.into()),
|
||||
_ => Err(Error::new(eyre!("{}", t!("middleware.auth.unknown-content-type")), ErrorKind::Network).into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +47,7 @@ impl Middleware<RpcContext> for SyncDb {
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"middleware.db.error-writing-patch-sequence-header",
|
||||
error = e
|
||||
)
|
||||
);
|
||||
tracing::error!("{}", t!("middleware.db.error-writing-patch-sequence-header", error = e));
|
||||
tracing::debug!("{e:?}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,13 +240,7 @@ impl PortForwardController {
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"net.forward.error-initializing-controller",
|
||||
error = format!("{e:#}")
|
||||
)
|
||||
);
|
||||
tracing::error!("{}", t!("net.forward.error-initializing-controller", error = format!("{e:#}")));
|
||||
tracing::debug!("{e:?}");
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
}
|
||||
|
||||
@@ -171,13 +171,16 @@ where
|
||||
let mut tls_handler = self.tls_handler.clone();
|
||||
let mut fut = async move {
|
||||
let res = async {
|
||||
let mut acceptor =
|
||||
LazyConfigAcceptor::new(Acceptor::default(), BackTrackingIO::new(stream));
|
||||
let mut acceptor = LazyConfigAcceptor::new(
|
||||
Acceptor::default(),
|
||||
BackTrackingIO::new(stream),
|
||||
);
|
||||
let mut mid: tokio_rustls::StartHandshake<BackTrackingIO<AcceptStream>> =
|
||||
match (&mut acceptor).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let mut stream = acceptor.take_io().or_not_found("acceptor io")?;
|
||||
let mut stream =
|
||||
acceptor.take_io().or_not_found("acceptor io")?;
|
||||
let (_, buf) = stream.rewind();
|
||||
if std::str::from_utf8(buf)
|
||||
.ok()
|
||||
|
||||
@@ -324,12 +324,7 @@ pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error>
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct ResetParams {
|
||||
#[arg(
|
||||
name = "wipe-state",
|
||||
short = 'w',
|
||||
long = "wipe-state",
|
||||
help = "help.arg.wipe-tor-state"
|
||||
)]
|
||||
#[arg(name = "wipe-state", short = 'w', long = "wipe-state", help = "help.arg.wipe-tor-state")]
|
||||
wipe_state: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -351,12 +351,7 @@ pub async fn list_keys(ctx: RpcContext) -> Result<BTreeSet<OnionAddress>, Error>
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub struct ResetParams {
|
||||
#[arg(
|
||||
name = "wipe-state",
|
||||
short = 'w',
|
||||
long = "wipe-state",
|
||||
help = "help.arg.wipe-tor-state"
|
||||
)]
|
||||
#[arg(name = "wipe-state", short = 'w', long = "wipe-state", help = "help.arg.wipe-tor-state")]
|
||||
wipe_state: bool,
|
||||
#[arg(help = "help.arg.reset-reason")]
|
||||
reason: String,
|
||||
|
||||
@@ -94,12 +94,7 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
|
||||
.next()
|
||||
.transpose()?
|
||||
.map(|(a, _)| a)
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("registry.admin.unknown-signer")),
|
||||
ErrorKind::Authorization,
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.admin.unknown-signer")), ErrorKind::Authorization))
|
||||
}
|
||||
|
||||
pub fn get_signer_info(&self, key: &AnyVerifyingKey) -> Result<(Guid, SignerInfo), Error> {
|
||||
@@ -109,12 +104,7 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
|
||||
.filter_ok(|(_, s)| s.keys.contains(key))
|
||||
.next()
|
||||
.transpose()?
|
||||
.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("registry.admin.unknown-signer")),
|
||||
ErrorKind::Authorization,
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.admin.unknown-signer")), ErrorKind::Authorization))
|
||||
}
|
||||
|
||||
pub fn add_signer(&mut self, signer: &SignerInfo) -> Result<Guid, Error> {
|
||||
@@ -129,11 +119,7 @@ impl Model<BTreeMap<Guid, SignerInfo>> {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"registry.admin.signer-already-exists",
|
||||
guid = guid,
|
||||
name = s.name
|
||||
)
|
||||
t!("registry.admin.signer-already-exists", guid = guid, name = s.name)
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
|
||||
@@ -44,11 +44,7 @@ const DEFAULT_REGISTRY_LISTEN: SocketAddr =
|
||||
pub struct RegistryConfig {
|
||||
#[arg(short = 'c', long = "config", help = "help.arg.config-file-path")]
|
||||
pub config: Option<PathBuf>,
|
||||
#[arg(
|
||||
short = 'l',
|
||||
long = "listen",
|
||||
help = "help.arg.registry-listen-address"
|
||||
)]
|
||||
#[arg(short = 'l', long = "listen", help = "help.arg.registry-listen-address")]
|
||||
pub registry_listen: Option<SocketAddr>,
|
||||
#[arg(short = 'H', long = "hostname", help = "help.arg.registry-hostname")]
|
||||
pub registry_hostname: Vec<InternedString>,
|
||||
@@ -56,11 +52,7 @@ pub struct RegistryConfig {
|
||||
pub tor_proxy: Option<Url>,
|
||||
#[arg(short = 'd', long = "datadir", help = "help.arg.data-directory")]
|
||||
pub datadir: Option<PathBuf>,
|
||||
#[arg(
|
||||
short = 'u',
|
||||
long = "pg-connection-url",
|
||||
help = "help.arg.postgres-connection-url"
|
||||
)]
|
||||
#[arg(short = 'u', long = "pg-connection-url", help = "help.arg.postgres-connection-url")]
|
||||
pub pg_connection_url: Option<String>,
|
||||
}
|
||||
impl ContextConfig for RegistryConfig {
|
||||
@@ -203,11 +195,9 @@ impl CallRemote<RegistryContext> for CliContext {
|
||||
.push("v0");
|
||||
url
|
||||
} else {
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("registry.context.registry-required")),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
.into());
|
||||
return Err(
|
||||
Error::new(eyre!("{}", t!("registry.context.registry-required")), ErrorKind::InvalidRequest).into(),
|
||||
);
|
||||
};
|
||||
|
||||
if let Ok(local) = cookie {
|
||||
@@ -341,10 +331,7 @@ impl SignatureAuthContext for RegistryContext {
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.context.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.context.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
async fn post_auth_hook(
|
||||
&self,
|
||||
|
||||
@@ -154,10 +154,7 @@ async fn add_asset(
|
||||
})?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.os.asset.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.os.asset.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -234,12 +231,10 @@ pub async fn cli_add_asset(
|
||||
|
||||
sign_phase.start();
|
||||
let blake3 = file.blake3_mmap().await?;
|
||||
let size = file.size().await.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("registry.os.asset.failed-read-metadata")),
|
||||
ErrorKind::Filesystem,
|
||||
)
|
||||
})?;
|
||||
let size = file
|
||||
.size()
|
||||
.await
|
||||
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.os.asset.failed-read-metadata")), ErrorKind::Filesystem))?;
|
||||
let commitment = Blake3Commitment {
|
||||
hash: Base64(*blake3.as_bytes()),
|
||||
size,
|
||||
@@ -341,10 +336,7 @@ async fn remove_asset(
|
||||
.remove(&platform)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.os.asset.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.os.asset.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -125,9 +125,17 @@ pub struct CliGetOsAssetParams {
|
||||
pub version: Version,
|
||||
#[arg(help = "help.arg.platform")]
|
||||
pub platform: InternedString,
|
||||
#[arg(long = "download", short = 'd', help = "help.arg.download-directory")]
|
||||
#[arg(
|
||||
long = "download",
|
||||
short = 'd',
|
||||
help = "help.arg.download-directory"
|
||||
)]
|
||||
pub download: Option<PathBuf>,
|
||||
#[arg(long = "reverify", short = 'r', help = "help.arg.reverify-hash")]
|
||||
#[arg(
|
||||
long = "reverify",
|
||||
short = 'r',
|
||||
help = "help.arg.reverify-hash"
|
||||
)]
|
||||
pub reverify: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -89,10 +89,7 @@ async fn sign_asset(
|
||||
.contains(&guid)
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("registry.os.asset.signer-not-authorized", guid = guid)
|
||||
),
|
||||
eyre!("{}", t!("registry.os.asset.signer-not-authorized", guid = guid)),
|
||||
ErrorKind::Authorization,
|
||||
));
|
||||
}
|
||||
@@ -187,12 +184,10 @@ pub async fn cli_sign_asset(
|
||||
|
||||
sign_phase.start();
|
||||
let blake3 = file.blake3_mmap().await?;
|
||||
let size = file.size().await.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("registry.os.asset.failed-read-metadata")),
|
||||
ErrorKind::Filesystem,
|
||||
)
|
||||
})?;
|
||||
let size = file
|
||||
.size()
|
||||
.await
|
||||
.ok_or_else(|| Error::new(eyre!("{}", t!("registry.os.asset.failed-read-metadata")), ErrorKind::Filesystem))?;
|
||||
let commitment = Blake3Commitment {
|
||||
hash: Base64(*blake3.as_bytes()),
|
||||
size,
|
||||
|
||||
@@ -26,6 +26,7 @@ pub fn os_api<C: Context>() -> ParentHandler<C> {
|
||||
)
|
||||
.subcommand(
|
||||
"version",
|
||||
version::version_api::<C>().with_about("about.commands-add-remove-list-versions"),
|
||||
version::version_api::<C>()
|
||||
.with_about("about.commands-add-remove-list-versions"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,14 +95,7 @@ pub async fn remove_version_signer(
|
||||
.mutate(|s| Ok(s.remove(&signer)))?
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"registry.os.version.signer-not-authorized",
|
||||
signer = signer,
|
||||
version = version
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("registry.os.version.signer-not-authorized", signer = signer, version = version)),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -112,10 +112,7 @@ pub async fn add_package(
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.package.add.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.package.add.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -135,24 +132,20 @@ pub struct CliAddPackageParams {
|
||||
}
|
||||
|
||||
pub async fn cli_add_package(
|
||||
ctx: CliContext,
|
||||
CliAddPackageParams {
|
||||
file,
|
||||
url,
|
||||
no_verify,
|
||||
}: CliAddPackageParams,
|
||||
HandlerArgs {
|
||||
context: ctx,
|
||||
parent_method,
|
||||
method,
|
||||
params:
|
||||
CliAddPackageParams {
|
||||
file,
|
||||
url,
|
||||
no_verify,
|
||||
},
|
||||
..
|
||||
}: HandlerArgs<CliContext, CliAddPackageParams>,
|
||||
) -> Result<(), Error> {
|
||||
let s9pk = S9pk::open(&file, None).await?;
|
||||
cli_add_package_impl(ctx, s9pk, url, no_verify).await
|
||||
}
|
||||
|
||||
pub async fn cli_add_package_impl(
|
||||
ctx: CliContext,
|
||||
s9pk: S9pk,
|
||||
url: Vec<Url>,
|
||||
no_verify: bool,
|
||||
) -> Result<(), Error> {
|
||||
let manifest = s9pk.as_manifest();
|
||||
|
||||
let progress = FullProgressTracker::new();
|
||||
let mut sign_phase = progress.add_phase(InternedString::intern("Signing File"), Some(1));
|
||||
@@ -174,16 +167,8 @@ pub async fn cli_add_package_impl(
|
||||
Some(1),
|
||||
);
|
||||
|
||||
let progress_task = progress.progress_bar_task(&format!(
|
||||
"Adding {}@{}{} to registry...",
|
||||
manifest.id,
|
||||
manifest.version,
|
||||
manifest
|
||||
.hardware_requirements
|
||||
.arch
|
||||
.as_ref()
|
||||
.map_or(String::new(), |a| format!(" ({})", a.iter().join("/")))
|
||||
));
|
||||
let progress_task =
|
||||
progress.progress_bar_task(&format!("Adding {} to registry...", file.display()));
|
||||
|
||||
sign_phase.start();
|
||||
let commitment = s9pk.as_archive().commitment().await?;
|
||||
@@ -200,7 +185,7 @@ pub async fn cli_add_package_impl(
|
||||
|
||||
index_phase.start();
|
||||
ctx.call_remote::<RegistryContext>(
|
||||
"package.add",
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
imbl_value::json!({
|
||||
"urls": &url,
|
||||
"signature": AnySignature::Ed25519(signature),
|
||||
@@ -243,12 +228,8 @@ pub async fn remove_package(
|
||||
}: RemovePackageParams,
|
||||
) -> Result<bool, Error> {
|
||||
let peek = ctx.db.peek().await;
|
||||
let signer = signer.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("registry.package.missing-signer")),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
})?;
|
||||
let signer =
|
||||
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
|
||||
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
|
||||
|
||||
let rev = ctx
|
||||
@@ -289,10 +270,7 @@ pub async fn remove_package(
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.package.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.package.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await;
|
||||
@@ -367,10 +345,7 @@ pub async fn add_mirror(
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.package.add-mirror.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.package.add-mirror.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -486,12 +461,8 @@ pub async fn remove_mirror(
|
||||
}: RemoveMirrorParams,
|
||||
) -> Result<(), Error> {
|
||||
let peek = ctx.db.peek().await;
|
||||
let signer = signer.ok_or_else(|| {
|
||||
Error::new(
|
||||
eyre!("{}", t!("registry.package.missing-signer")),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
})?;
|
||||
let signer =
|
||||
signer.ok_or_else(|| Error::new(eyre!("{}", t!("registry.package.missing-signer")), ErrorKind::InvalidRequest))?;
|
||||
let signer_guid = peek.as_index().as_signers().get_signer(&signer)?;
|
||||
|
||||
ctx.db
|
||||
@@ -530,10 +501,7 @@ pub async fn remove_mirror(
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("registry.package.remove-mirror.unauthorized")),
|
||||
ErrorKind::Authorization,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("registry.package.remove-mirror.unauthorized")), ErrorKind::Authorization))
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -52,14 +52,10 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
|
||||
if !changed {
|
||||
tracing::warn!(
|
||||
"{}",
|
||||
t!(
|
||||
"registry.package.remove-not-exist",
|
||||
t!("registry.package.remove-not-exist",
|
||||
id = args.params.id,
|
||||
version = args.params.version,
|
||||
sighash = args
|
||||
.params
|
||||
.sighash
|
||||
.map_or(String::new(), |h| format!("#{h}"))
|
||||
sighash = args.params.sighash.map_or(String::new(), |h| format!("#{h}"))
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -100,6 +96,7 @@ pub fn package_api<C: Context>() -> ParentHandler<C> {
|
||||
)
|
||||
.subcommand(
|
||||
"category",
|
||||
category::category_api::<C>().with_about("about.update-categories-registry"),
|
||||
category::category_api::<C>()
|
||||
.with_about("about.update-categories-registry"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -118,14 +118,7 @@ pub async fn remove_package_signer(
|
||||
.is_some()
|
||||
{
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"registry.package.signer.not-authorized",
|
||||
signer = signer,
|
||||
id = id
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("registry.package.signer.not-authorized", signer = signer, id = id)),
|
||||
ErrorKind::NotFound,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use rpc_toolkit::{Empty, HandlerExt, ParentHandler, from_fn_async};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
use crate::ImageId;
|
||||
use crate::context::CliContext;
|
||||
@@ -16,9 +13,9 @@ use crate::s9pk::manifest::Manifest;
|
||||
use crate::s9pk::merkle_archive::source::multi_cursor_file::MultiCursorFile;
|
||||
use crate::s9pk::v2::SIG_CONTEXT;
|
||||
use crate::s9pk::v2::pack::ImageConfig;
|
||||
use crate::util::Apply;
|
||||
use crate::util::io::{TmpDir, create_file, open_file};
|
||||
use crate::util::serde::{HandlerExtSerde, apply_expr};
|
||||
use crate::util::{Apply, Invoke};
|
||||
|
||||
pub const SKIP_ENV: &[&str] = &["TERM", "container", "HOME", "HOSTNAME"];
|
||||
|
||||
@@ -64,12 +61,6 @@ pub fn s9pk() -> ParentHandler<CliContext> {
|
||||
.no_display()
|
||||
.with_about("about.convert-s9pk-v1-to-v2"),
|
||||
)
|
||||
.subcommand(
|
||||
"publish",
|
||||
from_fn_async(publish)
|
||||
.no_display()
|
||||
.with_about("about.publish-s9pk"),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Parser)]
|
||||
@@ -265,61 +256,3 @@ async fn convert(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Res
|
||||
tokio::fs::rename(tmp_path, s9pk_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn publish(ctx: CliContext, S9pkPath { s9pk: s9pk_path }: S9pkPath) -> Result<(), Error> {
|
||||
let filename = s9pk_path.file_name().unwrap().to_string_lossy();
|
||||
let s9pk = super::S9pk::open(&s9pk_path, None).await?;
|
||||
let manifest = s9pk.as_manifest();
|
||||
let path = [
|
||||
manifest.id.deref(),
|
||||
manifest.version.as_str(),
|
||||
filename.deref(),
|
||||
];
|
||||
let mut s3url = ctx
|
||||
.s9pk_s3base
|
||||
.as_ref()
|
||||
.ok_or_else(|| Error::new(eyre!("--s9pk-s3base required"), ErrorKind::InvalidRequest))?
|
||||
.clone();
|
||||
s3url
|
||||
.path_segments_mut()
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("s9pk-s3base is invalid (missing protocol?)"),
|
||||
ErrorKind::ParseUrl,
|
||||
)
|
||||
})?
|
||||
.pop_if_empty()
|
||||
.extend(path);
|
||||
|
||||
let mut s3dest = format!(
|
||||
"s3://{}",
|
||||
ctx.s9pk_s3bucket
|
||||
.as_deref()
|
||||
.or_else(|| s3url
|
||||
.host_str()
|
||||
.and_then(|h| h.split_once(".").map(|h| h.0)))
|
||||
.ok_or_else(|| {
|
||||
Error::new(eyre!("--s9pk-s3bucket required"), ErrorKind::InvalidRequest)
|
||||
})?,
|
||||
)
|
||||
.parse::<Url>()?;
|
||||
s3dest
|
||||
.path_segments_mut()
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
eyre!("s9pk-s3base is invalid (missing protocol?)"),
|
||||
ErrorKind::ParseUrl,
|
||||
)
|
||||
})?
|
||||
.pop_if_empty()
|
||||
.extend(path);
|
||||
Command::new("s3cmd")
|
||||
.arg("put")
|
||||
.arg("-P")
|
||||
.arg(s9pk_path)
|
||||
.arg(s3dest.as_str())
|
||||
.capture(false)
|
||||
.invoke(ErrorKind::Network)
|
||||
.await?;
|
||||
crate::registry::package::add::cli_add_package_impl(ctx, s9pk, vec![s3url], false).await
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ use clap::Parser;
|
||||
use futures::future::{BoxFuture, ready};
|
||||
use futures::{FutureExt, TryStreamExt};
|
||||
use imbl_value::InternedString;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::OnceCell;
|
||||
@@ -386,17 +385,13 @@ impl ImageSource {
|
||||
pub fn ingredients(&self) -> Vec<PathBuf> {
|
||||
match self {
|
||||
Self::Packed => Vec::new(),
|
||||
Self::DockerBuild {
|
||||
dockerfile,
|
||||
workdir,
|
||||
..
|
||||
} => {
|
||||
vec![dockerfile.clone().unwrap_or_else(|| {
|
||||
workdir
|
||||
Self::DockerBuild { dockerfile, .. } => {
|
||||
vec![
|
||||
dockerfile
|
||||
.as_deref()
|
||||
.unwrap_or(Path::new("."))
|
||||
.join("Dockerfile")
|
||||
})]
|
||||
.unwrap_or(Path::new("Dockerfile"))
|
||||
.to_owned(),
|
||||
]
|
||||
}
|
||||
Self::DockerTag(_) => Vec::new(),
|
||||
}
|
||||
@@ -687,7 +682,7 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
|
||||
let manifest = s9pk.as_manifest_mut();
|
||||
manifest.git_hash = Some(GitHash::from_path(params.path()).await?);
|
||||
if !params.arch.is_empty() {
|
||||
let arches: BTreeSet<InternedString> = match manifest.hardware_requirements.arch.take() {
|
||||
let arches = match manifest.hardware_requirements.arch.take() {
|
||||
Some(a) => params
|
||||
.arch
|
||||
.iter()
|
||||
@@ -696,41 +691,10 @@ pub async fn pack(ctx: CliContext, params: PackParams) -> Result<(), Error> {
|
||||
.collect(),
|
||||
None => params.arch.iter().cloned().collect(),
|
||||
};
|
||||
if arches.is_empty() {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"none of the requested architectures ({:?}) are supported by this package",
|
||||
params.arch
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
manifest.images.iter_mut().for_each(|(id, c)| {
|
||||
let filtered = c
|
||||
.arch
|
||||
.intersection(&arches)
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
if filtered.is_empty() {
|
||||
if let Some(arch) = &c.emulate_missing_as {
|
||||
tracing::warn!(
|
||||
"ImageId {} is not available for {}, emulating as {}",
|
||||
id,
|
||||
arches.iter().join("/"),
|
||||
arch
|
||||
);
|
||||
c.arch = [arch.clone()].into_iter().collect();
|
||||
} else {
|
||||
tracing::error!(
|
||||
"ImageId {} is not available for {}",
|
||||
id,
|
||||
arches.iter().join("/"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
c.arch = filtered;
|
||||
}
|
||||
});
|
||||
manifest
|
||||
.images
|
||||
.values_mut()
|
||||
.for_each(|c| c.arch = c.arch.intersection(&arches).cloned().collect());
|
||||
manifest.hardware_requirements.arch = Some(arches);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,13 +102,7 @@ pub fn update_tasks(
|
||||
}
|
||||
}
|
||||
None => {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.action.action-request-invalid-state",
|
||||
task = format!("{:?}", v.task)
|
||||
)
|
||||
);
|
||||
tracing::error!("{}", t!("service.action.action-request-invalid-state", task = format!("{:?}", v.task)));
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -157,10 +151,7 @@ impl Handler<RunAction> for ServiceActor {
|
||||
.de()?;
|
||||
if matches!(&action.visibility, ActionVisibility::Disabled(_)) {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("service.action.action-is-disabled", action_id = action_id)
|
||||
),
|
||||
eyre!("{}", t!("service.action.action-is-disabled", action_id = action_id)),
|
||||
ErrorKind::Action,
|
||||
));
|
||||
}
|
||||
@@ -171,13 +162,7 @@ impl Handler<RunAction> for ServiceActor {
|
||||
_ => false,
|
||||
} {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.action.service-not-in-allowed-status",
|
||||
action_id = action_id
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("service.action.service-not-in-allowed-status", action_id = action_id)),
|
||||
ErrorKind::Action,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -181,10 +181,7 @@ async fn run_action(
|
||||
|
||||
if package_id != &context.seed.id {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("service.effects.action.calling-actions-on-other-packages-unsupported")
|
||||
),
|
||||
eyre!("{}", t!("service.effects.action.calling-actions-on-other-packages-unsupported")),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
context
|
||||
@@ -229,10 +226,7 @@ async fn create_task(
|
||||
TaskCondition::InputNotMatches => {
|
||||
let Some(input) = task.input.as_ref() else {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("service.effects.action.input-not-matches-requires-input")
|
||||
),
|
||||
eyre!("{}", t!("service.effects.action.input-not-matches-requires-input")),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
@@ -250,12 +244,7 @@ async fn create_task(
|
||||
else {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.effects.action.action-has-no-input",
|
||||
action_id = task.action_id,
|
||||
package_id = task.package_id
|
||||
)
|
||||
"{}", t!("service.effects.action.action-has-no-input", action_id = task.action_id, package_id = task.package_id)
|
||||
),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
|
||||
@@ -79,7 +79,7 @@ pub async fn mount(
|
||||
}
|
||||
|
||||
IdMapped::new(
|
||||
Bind::new(source).with_type(filetype).recursive(true),
|
||||
Bind::new(source).with_type(filetype),
|
||||
IdMap::stack(
|
||||
vec![IdMap {
|
||||
from_id: 0,
|
||||
|
||||
@@ -10,7 +10,6 @@ use crate::rpc_continuations::Guid;
|
||||
use crate::service::effects::prelude::*;
|
||||
use crate::service::persistent_container::Subcontainer;
|
||||
use crate::util::Invoke;
|
||||
use crate::util::io::write_file_owned_atomic;
|
||||
|
||||
pub const NVIDIA_OVERLAY_PATH: &str = "/var/tmp/startos/nvidia-overlay";
|
||||
pub const NVIDIA_OVERLAY_DEBIAN: &str = "/var/tmp/startos/nvidia-overlay/debian";
|
||||
@@ -95,7 +94,7 @@ pub async fn create_subcontainer_fs(
|
||||
.cloned()
|
||||
{
|
||||
let guid = Guid::new();
|
||||
let lxc_container = context
|
||||
let rootfs_dir = context
|
||||
.seed
|
||||
.persistent_container
|
||||
.lxc_container
|
||||
@@ -105,9 +104,8 @@ pub async fn create_subcontainer_fs(
|
||||
eyre!("PersistentContainer has been destroyed"),
|
||||
ErrorKind::Incoherent,
|
||||
)
|
||||
})?;
|
||||
let container_guid = &lxc_container.guid;
|
||||
let rootfs_dir = lxc_container.rootfs_dir();
|
||||
})?
|
||||
.rootfs_dir();
|
||||
let mountpoint = rootfs_dir
|
||||
.join("media/startos/subcontainers")
|
||||
.join(guid.as_ref());
|
||||
@@ -156,20 +154,6 @@ pub async fn create_subcontainer_fs(
|
||||
.arg(&mountpoint)
|
||||
.invoke(ErrorKind::Filesystem)
|
||||
.await?;
|
||||
write_file_owned_atomic(
|
||||
mountpoint.join("etc/hostname"),
|
||||
format!("{container_guid}\n"),
|
||||
100000,
|
||||
100000,
|
||||
)
|
||||
.await?;
|
||||
write_file_owned_atomic(
|
||||
mountpoint.join("etc/hosts"),
|
||||
format!("127.0.0.1\tlocalhost\n127.0.1.1\t{container_guid}\n::1\tlocalhost ip6-localhost ip6-loopback\n"),
|
||||
100000,
|
||||
100000,
|
||||
)
|
||||
.await?;
|
||||
tracing::info!("Mounted overlay {guid} for {image_id}");
|
||||
context
|
||||
.seed
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::{OsStr, OsString, c_int};
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, IsTerminal, Read};
|
||||
use std::io::{IsTerminal, Read};
|
||||
use std::os::unix::process::{CommandExt, ExitStatusExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command as StdCommand, Stdio};
|
||||
@@ -145,160 +146,95 @@ impl ExecParams {
|
||||
|
||||
let mut cmd = StdCommand::new(command);
|
||||
|
||||
let mut uid = Err(None);
|
||||
let mut gid = Err(None);
|
||||
let mut needs_home = true;
|
||||
let passwd = std::fs::read_to_string(chroot.join("etc/passwd"))
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, "read /etc/passwd"))
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
let mut home = None;
|
||||
|
||||
if let Some(user) = user {
|
||||
if let Some((u, g)) = user.split_once(":") {
|
||||
uid = Err(Some(u));
|
||||
gid = Err(Some(g));
|
||||
if let Some((uid, gid)) =
|
||||
if let Some(uid) = user.as_deref().and_then(|u| u.parse::<u32>().ok()) {
|
||||
Some((uid, uid))
|
||||
} else if let Some((uid, gid)) = user
|
||||
.as_deref()
|
||||
.and_then(|u| u.split_once(":"))
|
||||
.and_then(|(u, g)| Some((u.parse::<u32>().ok()?, g.parse::<u32>().ok()?)))
|
||||
{
|
||||
Some((uid, gid))
|
||||
} else if let Some(user) = user {
|
||||
Some(
|
||||
if let Some((uid, gid)) = passwd.lines().find_map(|l| {
|
||||
let l = l.trim();
|
||||
let mut split = l.split(":");
|
||||
if user != split.next()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
split.next(); // throw away x
|
||||
let uid = split.next()?.parse().ok()?;
|
||||
let gid = split.next()?.parse().ok()?;
|
||||
split.next(); // throw away group name
|
||||
|
||||
home = split.next();
|
||||
|
||||
Some((uid, gid))
|
||||
// uid gid
|
||||
}) {
|
||||
(uid, gid)
|
||||
} else if user == "root" {
|
||||
(0, 0)
|
||||
} else {
|
||||
None.or_not_found(lazy_format!("{user} in /etc/passwd"))?
|
||||
},
|
||||
)
|
||||
} else {
|
||||
uid = Err(Some(user));
|
||||
None
|
||||
}
|
||||
}
|
||||
{
|
||||
if home.is_none() {
|
||||
home = passwd.lines().find_map(|l| {
|
||||
let l = l.trim();
|
||||
let mut split = l.split(":");
|
||||
|
||||
if let Some(u) = uid.err().flatten().and_then(|u| u.parse::<u32>().ok()) {
|
||||
uid = Ok(u);
|
||||
}
|
||||
if let Some(g) = gid.err().flatten().and_then(|g| g.parse::<u32>().ok()) {
|
||||
gid = Ok(g);
|
||||
}
|
||||
split.next(); // throw away user name
|
||||
split.next(); // throw away x
|
||||
if split.next()?.parse::<u32>().ok()? != uid {
|
||||
return None;
|
||||
}
|
||||
split.next(); // throw away gid
|
||||
split.next(); // throw away group name
|
||||
|
||||
let mut update_env = |line: &str| {
|
||||
if let Some((k, v)) = line.split_once("=") {
|
||||
needs_home &= k != "HOME";
|
||||
cmd.env(k, v);
|
||||
} else {
|
||||
tracing::warn!("Invalid line in env: {line}");
|
||||
}
|
||||
split.next()
|
||||
})
|
||||
};
|
||||
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), Some(gid)).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), Some(gid)).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), Some(gid)).ok();
|
||||
cmd.uid(uid);
|
||||
cmd.gid(gid);
|
||||
} else {
|
||||
home = Some("/root");
|
||||
}
|
||||
cmd.env("HOME", home.unwrap_or("/"));
|
||||
|
||||
let env_string = if let Some(env_file) = &env_file {
|
||||
std::fs::read_to_string(env_file)
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("read {env:?}")))?
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
if let Some(f) = env_file {
|
||||
let mut lines = BufReader::new(
|
||||
File::open(&f).with_ctx(|_| (ErrorKind::Filesystem, format!("open r {f:?}")))?,
|
||||
)
|
||||
.lines();
|
||||
while let Some(line) = lines.next().transpose()? {
|
||||
update_env(&line);
|
||||
}
|
||||
}
|
||||
|
||||
for line in env {
|
||||
update_env(&line);
|
||||
}
|
||||
|
||||
let needs_gid = Err(None) == gid;
|
||||
let mut username = InternedString::intern("root");
|
||||
let mut handle_passwd_line = |line: &str| -> Option<()> {
|
||||
let l = line.trim();
|
||||
let mut split = l.split(":");
|
||||
let user = split.next()?;
|
||||
match uid {
|
||||
Err(Some(u)) if u != user => return None,
|
||||
_ => (),
|
||||
}
|
||||
split.next(); // throw away x
|
||||
let u: u32 = split.next()?.parse().ok()?;
|
||||
match uid {
|
||||
Err(Some(_)) => uid = Ok(u),
|
||||
Err(None) if u == 0 => uid = Ok(u),
|
||||
Ok(uid) if uid != u => return None,
|
||||
_ => (),
|
||||
}
|
||||
|
||||
username = user.into();
|
||||
|
||||
if !needs_gid && !needs_home {
|
||||
return Some(());
|
||||
}
|
||||
let g = split.next()?;
|
||||
if needs_gid {
|
||||
gid = Ok(g.parse().ok()?);
|
||||
}
|
||||
|
||||
if needs_home {
|
||||
split.next(); // throw away group name
|
||||
|
||||
let home = split.next()?;
|
||||
|
||||
cmd.env("HOME", home);
|
||||
}
|
||||
|
||||
Some(())
|
||||
};
|
||||
|
||||
let mut lines = BufReader::new(
|
||||
File::open(chroot.join("etc/passwd"))
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/passwd")))?,
|
||||
)
|
||||
.lines();
|
||||
while let Some(line) = lines.next().transpose()? {
|
||||
if handle_passwd_line(&line).is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut groups = Vec::new();
|
||||
let mut handle_group_line = |line: &str| -> Option<()> {
|
||||
let l = line.trim();
|
||||
let mut split = l.split(":");
|
||||
let name = split.next()?;
|
||||
split.next()?; // throw away x
|
||||
let g = split.next()?.parse::<u32>().ok()?;
|
||||
match gid {
|
||||
Err(Some(n)) if n == name => gid = Ok(g),
|
||||
_ => (),
|
||||
}
|
||||
let users = split.next()?;
|
||||
if users.split(",").any(|u| u == &*username) {
|
||||
groups.push(nix::unistd::Gid::from_raw(g));
|
||||
}
|
||||
Some(())
|
||||
};
|
||||
let mut lines = BufReader::new(
|
||||
File::open(chroot.join("etc/group"))
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, format!("open r /etc/group")))?,
|
||||
)
|
||||
.lines();
|
||||
while let Some(line) = lines.next().transpose()? {
|
||||
if handle_group_line(&line).is_none() {
|
||||
tracing::warn!("Invalid /etc/group line: {line}");
|
||||
}
|
||||
}
|
||||
|
||||
let env = env_string
|
||||
.lines()
|
||||
.chain(env.iter().map(|l| l.as_str()))
|
||||
.map(|l| l.trim())
|
||||
.filter_map(|l| l.split_once("="))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
std::os::unix::fs::chroot(chroot)
|
||||
.with_ctx(|_| (ErrorKind::Filesystem, lazy_format!("chroot {chroot:?}")))?;
|
||||
if let Ok(uid) = uid {
|
||||
if uid != 0 {
|
||||
std::os::unix::fs::chown("/proc/self/fd/0", Some(uid), gid.ok()).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/1", Some(uid), gid.ok()).ok();
|
||||
std::os::unix::fs::chown("/proc/self/fd/2", Some(uid), gid.ok()).ok();
|
||||
}
|
||||
}
|
||||
// Handle credential changes in pre_exec to control the order:
|
||||
// setgroups must happen before setgid/setuid (requires CAP_SETGID)
|
||||
{
|
||||
let set_uid = uid.ok();
|
||||
let set_gid = gid.ok();
|
||||
unsafe {
|
||||
cmd.pre_exec(move || {
|
||||
if !groups.is_empty() {
|
||||
nix::unistd::setgroups(&groups)
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||
}
|
||||
if let Some(gid) = set_gid {
|
||||
nix::unistd::setgid(nix::unistd::Gid::from_raw(gid))
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||
}
|
||||
if let Some(uid) = set_uid {
|
||||
nix::unistd::setuid(nix::unistd::Uid::from_raw(uid))
|
||||
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
}
|
||||
cmd.args(args);
|
||||
for (k, v) in env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
|
||||
if let Some(workdir) = workdir {
|
||||
cmd.current_dir(workdir);
|
||||
|
||||
@@ -28,6 +28,7 @@ use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode;
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
|
||||
use crate::context::{CliContext, RpcContext};
|
||||
use crate::db::model::package::{
|
||||
InstalledState, ManifestPreference, PackageState, PackageStateMatchModelRef, TaskSeverity,
|
||||
@@ -50,7 +51,6 @@ use crate::util::io::{AsyncReadStream, AtomicFile, TermSize, delete_file};
|
||||
use crate::util::net::WebSocket;
|
||||
use crate::util::serde::Pem;
|
||||
use crate::util::sync::SyncMutex;
|
||||
use crate::util::tui::choose;
|
||||
use crate::volume::data_dir;
|
||||
use crate::{ActionId, CAP_1_KiB, DATA_DIR, HostId, ImageId, PackageId};
|
||||
|
||||
@@ -184,10 +184,7 @@ impl ServiceRef {
|
||||
Arc::try_unwrap(service.seed)
|
||||
.map_err(|_| {
|
||||
Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("service.mod.service-actor-seed-held-after-shutdown")
|
||||
),
|
||||
eyre!("{}", t!("service.mod.service-actor-seed-held-after-shutdown")),
|
||||
ErrorKind::Unknown,
|
||||
)
|
||||
})?
|
||||
@@ -379,16 +376,12 @@ impl Service {
|
||||
{
|
||||
Ok(PackageState::Installed(InstalledState { manifest }))
|
||||
} else {
|
||||
Err(Error::new(
|
||||
eyre!("{}", t!("service.mod.race-condition-detected")),
|
||||
ErrorKind::Database,
|
||||
))
|
||||
Err(Error::new(eyre!("{}", t!("service.mod.race-condition-detected")), ErrorKind::Database))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
.await.result?;
|
||||
handle_installed(s9pk).await
|
||||
}
|
||||
PackageStateMatchModelRef::Removing(_) | PackageStateMatchModelRef::Restoring(_) => {
|
||||
@@ -454,13 +447,7 @@ impl Service {
|
||||
handle_installed(S9pk::open(s9pk_path, Some(id)).await?).await
|
||||
}
|
||||
PackageStateMatchModelRef::Error(e) => Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.mod.failed-to-parse-package-data-entry",
|
||||
error = format!("{e:?}")
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("service.mod.failed-to-parse-package-data-entry", error = format!("{e:?}"))),
|
||||
ErrorKind::Deserialization,
|
||||
)),
|
||||
}
|
||||
@@ -566,11 +553,7 @@ impl Service {
|
||||
true
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.mod.deleting-task-action-no-longer-exists",
|
||||
id = id
|
||||
)
|
||||
"{}", t!("service.mod.deleting-task-action-no-longer-exists", id = id)
|
||||
);
|
||||
false
|
||||
}
|
||||
@@ -710,19 +693,6 @@ pub async fn rebuild(ctx: RpcContext, RebuildParams { id }: RebuildParams) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct SubcontainerInfo {
|
||||
pub id: Guid,
|
||||
pub name: InternedString,
|
||||
pub image_id: ImageId,
|
||||
}
|
||||
impl std::fmt::Display for SubcontainerInfo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let SubcontainerInfo { id, name, image_id } = self;
|
||||
write!(f, "{id} => Name: {name}; Image: {image_id}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AttachParams {
|
||||
@@ -736,7 +706,7 @@ pub struct AttachParams {
|
||||
#[serde(rename = "__Auth_session")]
|
||||
session: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
subcontainer: Option<Guid>,
|
||||
subcontainer: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
name: Option<InternedString>,
|
||||
#[ts(type = "string | null")]
|
||||
@@ -759,7 +729,7 @@ pub async fn attach(
|
||||
user,
|
||||
}: AttachParams,
|
||||
) -> Result<Guid, Error> {
|
||||
let (container_id, subcontainer_id, image_id, user, workdir, root_command) = {
|
||||
let (container_id, subcontainer_id, image_id, workdir, root_command) = {
|
||||
let id = &id;
|
||||
|
||||
let service = ctx.services.get(id).await;
|
||||
@@ -800,6 +770,13 @@ pub async fn attach(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let format_subcontainer_pair = |(guid, wrapper): (&Guid, &Subcontainer)| {
|
||||
format!(
|
||||
"{guid} imageId: {image_id} name: \"{name}\"",
|
||||
name = &wrapper.name,
|
||||
image_id = &wrapper.image_id
|
||||
)
|
||||
};
|
||||
let Some((subcontainer_id, image_id)) = subcontainer_ids
|
||||
.first()
|
||||
.map::<(Guid, ImageId), _>(|&x| (x.0.clone(), x.1.image_id.clone()))
|
||||
@@ -810,17 +787,14 @@ pub async fn attach(
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.map(|(g, s)| SubcontainerInfo {
|
||||
id: g.clone(),
|
||||
name: s.name.clone(),
|
||||
image_id: s.image_id.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.map(format_subcontainer_pair)
|
||||
.join("\n");
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("service.mod.no-matching-subcontainers", id = id)),
|
||||
eyre!(
|
||||
"{}", t!("service.mod.no-matching-subcontainers", id = id, subcontainers = subcontainers)
|
||||
),
|
||||
ErrorKind::NotFound,
|
||||
)
|
||||
.with_info(to_value(&subcontainers)?));
|
||||
));
|
||||
};
|
||||
|
||||
let passwd = root_dir
|
||||
@@ -840,39 +814,31 @@ pub async fn attach(
|
||||
)
|
||||
.with_kind(ErrorKind::Deserialization)?;
|
||||
|
||||
let user = user
|
||||
.clone()
|
||||
.or_else(|| image_meta["user"].as_str().map(InternedString::intern))
|
||||
.unwrap_or_else(|| InternedString::intern("root"));
|
||||
|
||||
let root_command = get_passwd_command(passwd, &*user).await;
|
||||
let root_command = get_passwd_command(
|
||||
passwd,
|
||||
user.as_deref()
|
||||
.or_else(|| image_meta["user"].as_str())
|
||||
.unwrap_or("root"),
|
||||
)
|
||||
.await;
|
||||
|
||||
let workdir = image_meta["workdir"].as_str().map(|s| s.to_owned());
|
||||
|
||||
if subcontainer_ids.len() > 1 {
|
||||
let subcontainers = subcontainer_ids
|
||||
let subcontainer_ids = subcontainer_ids
|
||||
.into_iter()
|
||||
.map(|(g, s)| SubcontainerInfo {
|
||||
id: g.clone(),
|
||||
name: s.name.clone(),
|
||||
image_id: s.image_id.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.map(format_subcontainer_pair)
|
||||
.join("\n");
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("service.mod.multiple-subcontainers-found", id = id,)
|
||||
),
|
||||
eyre!("{}", t!("service.mod.multiple-subcontainers-found", id = id, subcontainer_ids = subcontainer_ids)),
|
||||
ErrorKind::InvalidRequest,
|
||||
)
|
||||
.with_info(to_value(&subcontainers)?));
|
||||
));
|
||||
}
|
||||
|
||||
(
|
||||
service_ref.container_id()?,
|
||||
subcontainer_id,
|
||||
image_id,
|
||||
user.into(),
|
||||
workdir,
|
||||
root_command,
|
||||
)
|
||||
@@ -889,7 +855,7 @@ pub async fn attach(
|
||||
pty_size: Option<TermSize>,
|
||||
image_id: ImageId,
|
||||
workdir: Option<String>,
|
||||
user: InternedString,
|
||||
user: Option<InternedString>,
|
||||
root_command: &RootCommand,
|
||||
) -> Result<(), Error> {
|
||||
use axum::extract::ws::Message;
|
||||
@@ -910,9 +876,11 @@ pub async fn attach(
|
||||
Path::new("/media/startos/images")
|
||||
.join(image_id)
|
||||
.with_extension("env"),
|
||||
)
|
||||
.arg("--user")
|
||||
.arg(&*user);
|
||||
);
|
||||
|
||||
if let Some(user) = user {
|
||||
cmd.arg("--user").arg(&*user);
|
||||
}
|
||||
|
||||
if let Some(workdir) = workdir {
|
||||
cmd.arg("--workdir").arg(workdir);
|
||||
@@ -1095,6 +1063,45 @@ pub async fn attach(
|
||||
Ok(guid)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListSubcontainersParams {
|
||||
pub id: PackageId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubcontainerInfo {
|
||||
pub name: InternedString,
|
||||
pub image_id: ImageId,
|
||||
}
|
||||
|
||||
pub async fn list_subcontainers(
|
||||
ctx: RpcContext,
|
||||
ListSubcontainersParams { id }: ListSubcontainersParams,
|
||||
) -> Result<BTreeMap<Guid, SubcontainerInfo>, Error> {
|
||||
let service = ctx.services.get(&id).await;
|
||||
let service_ref = service.as_ref().or_not_found(&id)?;
|
||||
let container = &service_ref.seed.persistent_container;
|
||||
|
||||
let subcontainers = container.subcontainers.lock().await;
|
||||
|
||||
let result: BTreeMap<Guid, SubcontainerInfo> = subcontainers
|
||||
.iter()
|
||||
.map(|(guid, subcontainer)| {
|
||||
(
|
||||
guid.clone(),
|
||||
SubcontainerInfo {
|
||||
name: subcontainer.name.clone(),
|
||||
image_id: subcontainer.image_id.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand {
|
||||
async {
|
||||
let mut file = tokio::fs::File::open(etc_passwd_path).await?;
|
||||
@@ -1113,13 +1120,7 @@ async fn get_passwd_command(etc_passwd_path: PathBuf, user: &str) -> RootCommand
|
||||
}
|
||||
}
|
||||
Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.mod.could-not-parse-etc-passwd",
|
||||
contents = contents
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("service.mod.could-not-parse-etc-passwd", contents = contents)),
|
||||
ErrorKind::Filesystem,
|
||||
))
|
||||
}
|
||||
@@ -1175,34 +1176,23 @@ pub async fn cli_attach(
|
||||
None
|
||||
};
|
||||
|
||||
let method = parent_method.into_iter().chain(method).join(".");
|
||||
let mut params = json!({
|
||||
"id": params.id,
|
||||
"command": params.command,
|
||||
"tty": tty,
|
||||
"stderrTty": stderr.is_terminal(),
|
||||
"ptySize": if tty { TermSize::get_current() } else { None },
|
||||
"subcontainer": params.subcontainer,
|
||||
"imageId": params.image_id,
|
||||
"name": params.name,
|
||||
"user": params.user,
|
||||
});
|
||||
let guid: Guid = from_value(
|
||||
match context
|
||||
.call_remote::<RpcContext>(&method, params.clone())
|
||||
.await
|
||||
{
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let prompt = e.to_string();
|
||||
let options: Vec<SubcontainerInfo> = from_value(e.info)?;
|
||||
let choice = choose(&prompt, &options).await?;
|
||||
params["subcontainer"] = to_value(&choice.id)?;
|
||||
context
|
||||
.call_remote::<RpcContext>(&method, params.clone())
|
||||
.await?
|
||||
}
|
||||
},
|
||||
context
|
||||
.call_remote::<RpcContext>(
|
||||
&parent_method.into_iter().chain(method).join("."),
|
||||
json!({
|
||||
"id": params.id,
|
||||
"command": params.command,
|
||||
"tty": tty,
|
||||
"stderrTty": stderr.is_terminal(),
|
||||
"ptySize": if tty { TermSize::get_current() } else { None },
|
||||
"subcontainer": params.subcontainer,
|
||||
"imageId": params.image_id,
|
||||
"name": params.name,
|
||||
"user": params.user,
|
||||
}),
|
||||
)
|
||||
.await?,
|
||||
)?;
|
||||
let mut ws = context.ws_continuation(guid).await?;
|
||||
|
||||
|
||||
@@ -364,14 +364,7 @@ impl PersistentContainer {
|
||||
let handle = NonDetachingJoinHandle::from(tokio::spawn(async move {
|
||||
let chown_status = async {
|
||||
let res = server.run_unix(&path, |err| {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.persistent-container.error-on-unix-socket",
|
||||
path = path.display(),
|
||||
error = err
|
||||
)
|
||||
)
|
||||
tracing::error!("{}", t!("service.persistent-container.error-on-unix-socket", path = path.display(), error = err))
|
||||
})?;
|
||||
Command::new("chown")
|
||||
.arg("100000:100000")
|
||||
@@ -393,10 +386,7 @@ impl PersistentContainer {
|
||||
}));
|
||||
let shutdown = recv.await.map_err(|_| {
|
||||
Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!("service.persistent-container.unix-socket-server-panicked")
|
||||
),
|
||||
eyre!("{}", t!("service.persistent-container.unix-socket-server-panicked")),
|
||||
ErrorKind::Unknown,
|
||||
)
|
||||
})??;
|
||||
@@ -483,13 +473,7 @@ impl PersistentContainer {
|
||||
if let Some(destroy) = self.destroy(uninit) {
|
||||
destroy.await?;
|
||||
}
|
||||
tracing::info!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.persistent-container.service-exited",
|
||||
id = self.s9pk.as_manifest().id
|
||||
)
|
||||
);
|
||||
tracing::info!("{}", t!("service.persistent-container.service-exited", id = self.s9pk.as_manifest().id));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -47,18 +47,9 @@ impl Actor for ServiceActor {
|
||||
}
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("service.service-actor.error-synchronizing-state", error = e)
|
||||
);
|
||||
tracing::error!("{}", t!("service.service-actor.error-synchronizing-state", error = e));
|
||||
tracing::debug!("{e:?}");
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.service-actor.retrying-in-seconds",
|
||||
seconds = SYNC_RETRY_COOLDOWN_SECONDS
|
||||
)
|
||||
);
|
||||
tracing::error!("{}", t!("service.service-actor.retrying-in-seconds", seconds = SYNC_RETRY_COOLDOWN_SECONDS));
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(SYNC_RETRY_COOLDOWN_SECONDS),
|
||||
async {
|
||||
|
||||
@@ -62,13 +62,7 @@ pub async fn cleanup(ctx: &RpcContext, id: &PackageId, soft: bool) -> Result<(),
|
||||
| PackageState::Removing(InstalledState { manifest }) => manifest,
|
||||
s => {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"service.uninstall.invalid-package-state-for-cleanup",
|
||||
state = format!("{s:?}")
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("service.uninstall.invalid-package-state-for-cleanup", state = format!("{s:?}"))),
|
||||
ErrorKind::InvalidRequest,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
use crate::PLATFORM;
|
||||
use crate::context::RpcContext;
|
||||
use crate::disk::main::export;
|
||||
@@ -35,33 +36,18 @@ impl Shutdown {
|
||||
.invoke(crate::ErrorKind::Journald)
|
||||
.await
|
||||
{
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!("shutdown.error-stopping-journald", error = e.to_string())
|
||||
);
|
||||
tracing::error!("{}", t!("shutdown.error-stopping-journald", error = e.to_string()));
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
if let Some(guid) = &self.disk_guid {
|
||||
if let Err(e) = export(guid, crate::DATA_DIR).await {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"shutdown.error-exporting-volume-group",
|
||||
error = e.to_string()
|
||||
)
|
||||
);
|
||||
tracing::error!("{}", t!("shutdown.error-exporting-volume-group", error = e.to_string()));
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
}
|
||||
if &*PLATFORM != "raspberrypi" || self.restart {
|
||||
if let Err(e) = SHUTDOWN.play().await {
|
||||
tracing::error!(
|
||||
"{}",
|
||||
t!(
|
||||
"shutdown.error-playing-shutdown-song",
|
||||
error = e.to_string()
|
||||
)
|
||||
);
|
||||
tracing::error!("{}", t!("shutdown.error-playing-shutdown-song", error = e.to_string()));
|
||||
tracing::debug!("{:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ pub fn tunnel_api<C: Context>() -> ParentHandler<C> {
|
||||
.subcommand("web", super::web::web_api::<C>())
|
||||
.subcommand(
|
||||
"db",
|
||||
super::db::db_api::<C>().with_about("about.commands-interact-with-db-dump-apply"),
|
||||
super::db::db_api::<C>()
|
||||
.with_about("about.commands-interact-with-db-dump-apply"),
|
||||
)
|
||||
.subcommand(
|
||||
"auth",
|
||||
|
||||
@@ -6,7 +6,6 @@ use clap::{ArgAction, Parser};
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use exver::{Version, VersionRange};
|
||||
use futures::TryStreamExt;
|
||||
use imbl::OrdMap;
|
||||
use imbl_value::json;
|
||||
use itertools::Itertools;
|
||||
use patch_db::json_ptr::JsonPointer;
|
||||
@@ -180,10 +179,7 @@ pub async fn cli_update_system(
|
||||
Some(v) => {
|
||||
if let Some(progress) = res.progress {
|
||||
let mut ws = context.ws_continuation(progress).await?;
|
||||
let mut progress = PhasedProgressBar::new(&t!(
|
||||
"update.updating-to-version",
|
||||
version = v.to_string()
|
||||
));
|
||||
let mut progress = PhasedProgressBar::new(&t!("update.updating-to-version", version = v.to_string()));
|
||||
let mut prev = None;
|
||||
while let Some(msg) = ws.try_next().await.with_kind(ErrorKind::Network)? {
|
||||
if let tokio_tungstenite::tungstenite::Message::Text(msg) = msg {
|
||||
@@ -206,10 +202,7 @@ pub async fn cli_update_system(
|
||||
}
|
||||
println!("{}", t!("update.complete-restart-to-apply"))
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
t!("update.updating-to-version", version = v.to_string())
|
||||
)
|
||||
println!("{}", t!("update.updating-to-version", version = v.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +239,6 @@ async fn maybe_do_update(
|
||||
let mut available = from_value::<BTreeMap<Version, OsVersionInfo>>(
|
||||
ctx.call_remote_with::<RegistryContext, _>(
|
||||
"os.version.get",
|
||||
OrdMap::new(),
|
||||
json!({
|
||||
"source": current_version,
|
||||
"target": target,
|
||||
|
||||
@@ -248,7 +248,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
|
||||
.or(Some(&res.stdout))
|
||||
.filter(|a| !a.is_empty())
|
||||
.and_then(|a| std::str::from_utf8(a).ok())
|
||||
.unwrap_or(&format!("{} exited with {}", cmd_str, res.status))
|
||||
.unwrap_or(&format!("{} exited with code {}", cmd_str, res.status))
|
||||
);
|
||||
Ok(res.stdout)
|
||||
} else {
|
||||
@@ -309,7 +309,7 @@ impl<'a> Invoke<'a> for ExtendedCommand<'a> {
|
||||
.filter(|a| !a.is_empty())
|
||||
.and_then(|a| std::str::from_utf8(a).ok())
|
||||
.unwrap_or(&format!(
|
||||
"{} exited with {}",
|
||||
"{} exited with code {}",
|
||||
cmd.as_std().get_program().to_string_lossy(),
|
||||
res.status
|
||||
))
|
||||
|
||||
@@ -97,11 +97,7 @@ impl WebSocket {
|
||||
if self.ping_state.is_some() {
|
||||
self.fused = true;
|
||||
break Poll::Ready(Some(Err(axum::Error::new(eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"util.net.websocket-ping-timeout",
|
||||
timeout = format!("{PING_TIMEOUT:?}")
|
||||
)
|
||||
"{}", t!("util.net.websocket-ping-timeout", timeout = format!("{PING_TIMEOUT:?}"))
|
||||
)))));
|
||||
}
|
||||
self.ping_state = Some((false, rand::random()));
|
||||
|
||||
@@ -1151,13 +1151,7 @@ pub fn apply_expr(input: jaq_core::Val, expr: &str) -> Result<jaq_core::Val, Err
|
||||
|
||||
let Some(expr) = expr else {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"util.serde.failed-to-parse-expression",
|
||||
errors = format!("{:?}", errs)
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("util.serde.failed-to-parse-expression", errors = format!("{:?}", errs))),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
@@ -1173,13 +1167,7 @@ pub fn apply_expr(input: jaq_core::Val, expr: &str) -> Result<jaq_core::Val, Err
|
||||
|
||||
if !errs.is_empty() {
|
||||
return Err(Error::new(
|
||||
eyre!(
|
||||
"{}",
|
||||
t!(
|
||||
"util.serde.failed-to-compile-expression",
|
||||
errors = format!("{:?}", errs)
|
||||
)
|
||||
),
|
||||
eyre!("{}", t!("util.serde.failed-to-compile-expression", errors = format!("{:?}", errs))),
|
||||
crate::ErrorKind::InvalidRequest,
|
||||
));
|
||||
};
|
||||
|
||||
@@ -50,10 +50,7 @@ pub async fn prompt<T, E: std::fmt::Display, Parse: FnMut(&str) -> Result<T, E>>
|
||||
}
|
||||
}
|
||||
ReadlineEvent::Eof | ReadlineEvent::Interrupted => {
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("util.tui.aborted")),
|
||||
ErrorKind::Cancelled,
|
||||
));
|
||||
return Err(Error::new(eyre!("{}", t!("util.tui.aborted")), ErrorKind::Cancelled));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -86,10 +83,7 @@ pub async fn prompt_multiline<
|
||||
Err(e) => writeln!(&mut rl_ctx.shared_writer, "{e}")?,
|
||||
},
|
||||
ReadlineEvent::Eof | ReadlineEvent::Interrupted => {
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("util.tui.aborted")),
|
||||
ErrorKind::Cancelled,
|
||||
));
|
||||
return Err(Error::new(eyre!("{}", t!("util.tui.aborted")), ErrorKind::Cancelled));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -125,10 +119,7 @@ pub async fn choose_custom_display<'t, T>(
|
||||
.await
|
||||
.map_err(map_miette)?;
|
||||
if choice.len() < 1 {
|
||||
return Err(Error::new(
|
||||
eyre!("{}", t!("util.tui.aborted")),
|
||||
ErrorKind::Cancelled,
|
||||
));
|
||||
return Err(Error::new(eyre!("{}", t!("util.tui.aborted")), ErrorKind::Cancelled));
|
||||
}
|
||||
let (idx, choice_str) = string_choices
|
||||
.iter()
|
||||
|
||||
@@ -58,9 +58,8 @@ mod v0_4_0_alpha_15;
|
||||
mod v0_4_0_alpha_16;
|
||||
mod v0_4_0_alpha_17;
|
||||
mod v0_4_0_alpha_18;
|
||||
mod v0_4_0_alpha_19;
|
||||
|
||||
pub type Current = v0_4_0_alpha_19::Version; // VERSION_BUMP
|
||||
pub type Current = v0_4_0_alpha_18::Version; // VERSION_BUMP
|
||||
|
||||
impl Current {
|
||||
#[instrument(skip(self, db))]
|
||||
@@ -180,8 +179,7 @@ enum Version {
|
||||
V0_4_0_alpha_15(Wrapper<v0_4_0_alpha_15::Version>),
|
||||
V0_4_0_alpha_16(Wrapper<v0_4_0_alpha_16::Version>),
|
||||
V0_4_0_alpha_17(Wrapper<v0_4_0_alpha_17::Version>),
|
||||
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>),
|
||||
V0_4_0_alpha_19(Wrapper<v0_4_0_alpha_19::Version>), // VERSION_BUMP
|
||||
V0_4_0_alpha_18(Wrapper<v0_4_0_alpha_18::Version>), // VERSION_BUMP
|
||||
Other(exver::Version),
|
||||
}
|
||||
|
||||
@@ -242,8 +240,7 @@ impl Version {
|
||||
Self::V0_4_0_alpha_15(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_16(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_17(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)),
|
||||
Self::V0_4_0_alpha_19(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||
Self::V0_4_0_alpha_18(v) => DynVersion(Box::new(v.0)), // VERSION_BUMP
|
||||
Self::Other(v) => {
|
||||
return Err(Error::new(
|
||||
eyre!("unknown version {v}"),
|
||||
@@ -296,8 +293,7 @@ impl Version {
|
||||
Version::V0_4_0_alpha_15(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_16(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_17(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(),
|
||||
Version::V0_4_0_alpha_19(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||
Version::V0_4_0_alpha_18(Wrapper(x)) => x.semver(), // VERSION_BUMP
|
||||
Version::Other(x) => x.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
use exver::{PreReleaseSegment, VersionRange};
|
||||
|
||||
use super::v0_3_5::V0_3_0_COMPAT;
|
||||
use super::{VersionT, v0_4_0_alpha_18};
|
||||
use crate::prelude::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref V0_4_0_alpha_19: exver::Version = exver::Version::new(
|
||||
[0, 4, 0],
|
||||
[PreReleaseSegment::String("alpha".into()), 19.into()]
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct Version;
|
||||
|
||||
impl VersionT for Version {
|
||||
type Previous = v0_4_0_alpha_18::Version;
|
||||
type PreUpRes = ();
|
||||
|
||||
async fn pre_up(self) -> Result<Self::PreUpRes, Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn semver(self) -> exver::Version {
|
||||
V0_4_0_alpha_19.clone()
|
||||
}
|
||||
fn compat(self) -> &'static VersionRange {
|
||||
&V0_3_0_COMPAT
|
||||
}
|
||||
#[instrument(skip_all)]
|
||||
fn up(self, _db: &mut Value, _: Self::PreUpRes) -> Result<Value, Error> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
fn down(self, _db: &mut Value) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -16,14 +16,14 @@ import {
|
||||
MountParams,
|
||||
StatusInfo,
|
||||
Manifest,
|
||||
} from './osBindings'
|
||||
} from "./osBindings"
|
||||
import {
|
||||
PackageId,
|
||||
Dependencies,
|
||||
ServiceInterfaceId,
|
||||
SmtpValue,
|
||||
ActionResult,
|
||||
} from './types'
|
||||
} from "./types"
|
||||
|
||||
/** Used to reach out from the pure js runtime */
|
||||
|
||||
@@ -155,13 +155,13 @@ export type Effects = {
|
||||
/** Returns a PEM encoded fullchain for the hostnames specified */
|
||||
getSslCertificate: (options: {
|
||||
hostnames: string[]
|
||||
algorithm?: 'ecdsa' | 'ed25519'
|
||||
algorithm?: "ecdsa" | "ed25519"
|
||||
callback?: () => void
|
||||
}) => Promise<[string, string, string]>
|
||||
/** Returns a PEM encoded private key corresponding to the certificate for the hostnames specified */
|
||||
getSslKey: (options: {
|
||||
hostnames: string[]
|
||||
algorithm?: 'ecdsa' | 'ed25519'
|
||||
algorithm?: "ecdsa" | "ed25519"
|
||||
}) => Promise<string>
|
||||
|
||||
/** sets the version that this service's data has been migrated to */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as T from '../types'
|
||||
import * as IST from '../actions/input/inputSpecTypes'
|
||||
import { Action, ActionInfo } from './setupActions'
|
||||
import { ExtractInputSpecType } from './input/builder/inputSpec'
|
||||
import * as T from "../types"
|
||||
import * as IST from "../actions/input/inputSpecTypes"
|
||||
import { Action, ActionInfo } from "./setupActions"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
|
||||
export type RunActionInput<Input> =
|
||||
| Input
|
||||
@@ -53,17 +53,17 @@ type TaskBase = {
|
||||
replayId?: string
|
||||
}
|
||||
type TaskInput<T extends ActionInfo<T.ActionId, any>> = {
|
||||
kind: 'partial'
|
||||
kind: "partial"
|
||||
value: T.DeepPartial<GetActionInputType<T>>
|
||||
}
|
||||
export type TaskOptions<T extends ActionInfo<T.ActionId, any>> = TaskBase &
|
||||
(
|
||||
| {
|
||||
when?: Exclude<T.TaskTrigger, { condition: 'input-not-matches' }>
|
||||
when?: Exclude<T.TaskTrigger, { condition: "input-not-matches" }>
|
||||
input?: TaskInput<T>
|
||||
}
|
||||
| {
|
||||
when: T.TaskTrigger & { condition: 'input-not-matches' }
|
||||
when: T.TaskTrigger & { condition: "input-not-matches" }
|
||||
input: TaskInput<T>
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InputSpec } from './inputSpec'
|
||||
import { List } from './list'
|
||||
import { Value } from './value'
|
||||
import { Variants } from './variants'
|
||||
import { InputSpec } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { Value } from "./value"
|
||||
import { Variants } from "./variants"
|
||||
|
||||
export { InputSpec as InputSpec, List, Value, Variants }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ValueSpec } from '../inputSpecTypes'
|
||||
import { Value } from './value'
|
||||
import { _ } from '../../../util'
|
||||
import { Effects } from '../../../Effects'
|
||||
import { Parser, object } from 'ts-matches'
|
||||
import { DeepPartial } from '../../../types'
|
||||
import { ValueSpec } from "../inputSpecTypes"
|
||||
import { Value } from "./value"
|
||||
import { _ } from "../../../util"
|
||||
import { Effects } from "../../../Effects"
|
||||
import { Parser, object } from "ts-matches"
|
||||
import { DeepPartial } from "../../../types"
|
||||
|
||||
export type LazyBuildOptions = {
|
||||
effects: Effects
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { InputSpec, LazyBuild } from './inputSpec'
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import {
|
||||
ListValueSpecText,
|
||||
Pattern,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
UniqueBy,
|
||||
ValueSpecList,
|
||||
ValueSpecListOf,
|
||||
} from '../inputSpecTypes'
|
||||
import { Parser, arrayOf, string } from 'ts-matches'
|
||||
} from "../inputSpecTypes"
|
||||
import { Parser, arrayOf, string } from "ts-matches"
|
||||
|
||||
export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
private constructor(
|
||||
@@ -55,7 +55,7 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
* @description Informs the browser how to behave and which keyboard to display on mobile
|
||||
* @default "text"
|
||||
*/
|
||||
inputmode?: ListValueSpecText['inputmode']
|
||||
inputmode?: ListValueSpecText["inputmode"]
|
||||
/**
|
||||
* @description Displays a button that will generate a random string according to the provided charset and len attributes.
|
||||
*/
|
||||
@@ -65,21 +65,21 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const validator = arrayOf(string)
|
||||
return new List<string[]>(() => {
|
||||
const spec = {
|
||||
type: 'text' as const,
|
||||
type: "text" as const,
|
||||
placeholder: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: false,
|
||||
inputmode: 'text' as const,
|
||||
inputmode: "text" as const,
|
||||
generate: null,
|
||||
patterns: aSpec.patterns || [],
|
||||
...aSpec,
|
||||
}
|
||||
const built: ValueSpecListOf<'text'> = {
|
||||
const built: ValueSpecListOf<"text"> = {
|
||||
description: null,
|
||||
warning: null,
|
||||
default: [],
|
||||
type: 'list' as const,
|
||||
type: "list" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
disabled: false,
|
||||
@@ -106,7 +106,7 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ListValueSpecText['inputmode']
|
||||
inputmode?: ListValueSpecText["inputmode"]
|
||||
}
|
||||
}>,
|
||||
) {
|
||||
@@ -114,21 +114,21 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new List<string[]>(async (options) => {
|
||||
const { spec: aSpec, ...a } = await getA(options)
|
||||
const spec = {
|
||||
type: 'text' as const,
|
||||
type: "text" as const,
|
||||
placeholder: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
masked: false,
|
||||
inputmode: 'text' as const,
|
||||
inputmode: "text" as const,
|
||||
generate: null,
|
||||
patterns: aSpec.patterns || [],
|
||||
...aSpec,
|
||||
}
|
||||
const built: ValueSpecListOf<'text'> = {
|
||||
const built: ValueSpecListOf<"text"> = {
|
||||
description: null,
|
||||
warning: null,
|
||||
default: [],
|
||||
type: 'list' as const,
|
||||
type: "list" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
disabled: false,
|
||||
@@ -162,7 +162,7 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const { spec: previousSpecSpec, ...restSpec } = aSpec
|
||||
const built = await previousSpecSpec.build(options)
|
||||
const spec = {
|
||||
type: 'object' as const,
|
||||
type: "object" as const,
|
||||
displayAs: null,
|
||||
uniqueBy: null,
|
||||
...restSpec,
|
||||
@@ -179,7 +179,7 @@ export class List<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
warning: null,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
type: 'list' as const,
|
||||
type: "list" as const,
|
||||
disabled: false,
|
||||
...value,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InputSpec, LazyBuild } from './inputSpec'
|
||||
import { List } from './list'
|
||||
import { UnionRes, UnionResStaticValidatedAs, Variants } from './variants'
|
||||
import { InputSpec, LazyBuild } from "./inputSpec"
|
||||
import { List } from "./list"
|
||||
import { UnionRes, UnionResStaticValidatedAs, Variants } from "./variants"
|
||||
import {
|
||||
Pattern,
|
||||
RandomString,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
ValueSpecHidden,
|
||||
ValueSpecText,
|
||||
ValueSpecTextarea,
|
||||
} from '../inputSpecTypes'
|
||||
import { DefaultString } from '../inputSpecTypes'
|
||||
import { _, once } from '../../../util'
|
||||
} from "../inputSpecTypes"
|
||||
import { DefaultString } from "../inputSpecTypes"
|
||||
import { _, once } from "../../../util"
|
||||
import {
|
||||
Parser,
|
||||
any,
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
number,
|
||||
object,
|
||||
string,
|
||||
} from 'ts-matches'
|
||||
import { DeepPartial } from '../../../types'
|
||||
} from "ts-matches"
|
||||
import { DeepPartial } from "../../../types"
|
||||
|
||||
export const fileInfoParser = object({
|
||||
path: string,
|
||||
@@ -42,7 +42,7 @@ const testForAsRequiredParser = once(
|
||||
function asRequiredParser<Type, Input extends { required: boolean }>(
|
||||
parser: Parser<unknown, Type>,
|
||||
input: Input,
|
||||
): Parser<unknown, AsRequired<Type, Input['required']>> {
|
||||
): Parser<unknown, AsRequired<Type, Input["required"]>> {
|
||||
if (testForAsRequiredParser()(input)) return parser as any
|
||||
return parser.nullable() as any
|
||||
}
|
||||
@@ -92,7 +92,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: 'toggle' as const,
|
||||
type: "toggle" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
@@ -117,7 +117,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: 'toggle' as const,
|
||||
type: "toggle" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...(await a(options)),
|
||||
@@ -191,7 +191,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
* @description Informs the browser how to behave and which keyboard to display on mobile
|
||||
* @default "text"
|
||||
*/
|
||||
inputmode?: ValueSpecText['inputmode']
|
||||
inputmode?: ValueSpecText["inputmode"]
|
||||
/**
|
||||
* @description Once set, the value can never be changed.
|
||||
* @default false
|
||||
@@ -206,7 +206,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
async () => ({
|
||||
spec: {
|
||||
type: 'text' as const,
|
||||
type: "text" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
masked: false,
|
||||
@@ -214,7 +214,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
inputmode: 'text',
|
||||
inputmode: "text",
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
generate: a.generate ?? null,
|
||||
@@ -237,7 +237,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
minLength?: number | null
|
||||
maxLength?: number | null
|
||||
patterns?: Pattern[]
|
||||
inputmode?: ValueSpecText['inputmode']
|
||||
inputmode?: ValueSpecText["inputmode"]
|
||||
disabled?: string | false
|
||||
generate?: null | RandomString
|
||||
}>,
|
||||
@@ -247,7 +247,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: 'text' as const,
|
||||
type: "text" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
masked: false,
|
||||
@@ -255,7 +255,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
patterns: [],
|
||||
inputmode: 'text',
|
||||
inputmode: "text",
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
generate: a.generate ?? null,
|
||||
@@ -334,7 +334,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
minRows: 3,
|
||||
maxRows: 6,
|
||||
placeholder: null,
|
||||
type: 'textarea' as const,
|
||||
type: "textarea" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
@@ -371,7 +371,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
minRows: 3,
|
||||
maxRows: 6,
|
||||
placeholder: null,
|
||||
type: 'textarea' as const,
|
||||
type: "textarea" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
@@ -444,7 +444,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<AsRequired<number, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
type: 'number' as const,
|
||||
type: "number" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
@@ -482,7 +482,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: 'number' as const,
|
||||
type: "number" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
min: null,
|
||||
@@ -540,7 +540,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
type: 'color' as const,
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
@@ -568,7 +568,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: 'color' as const,
|
||||
type: "color" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
@@ -618,7 +618,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
* @description Informs the browser how to behave and which date/time component to display.
|
||||
* @default "datetime-local"
|
||||
*/
|
||||
inputmode?: ValueSpecDatetime['inputmode']
|
||||
inputmode?: ValueSpecDatetime["inputmode"]
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
/**
|
||||
@@ -631,10 +631,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<AsRequired<string, Required>>(
|
||||
() => ({
|
||||
spec: {
|
||||
type: 'datetime' as const,
|
||||
type: "datetime" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: 'datetime-local',
|
||||
inputmode: "datetime-local",
|
||||
min: null,
|
||||
max: null,
|
||||
step: null,
|
||||
@@ -654,7 +654,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
warning?: string | null
|
||||
default: string | null
|
||||
required: Required
|
||||
inputmode?: ValueSpecDatetime['inputmode']
|
||||
inputmode?: ValueSpecDatetime["inputmode"]
|
||||
min?: string | null
|
||||
max?: string | null
|
||||
disabled?: false | string
|
||||
@@ -665,10 +665,10 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: 'datetime' as const,
|
||||
type: "datetime" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
inputmode: 'datetime-local',
|
||||
inputmode: "datetime-local",
|
||||
min: null,
|
||||
max: null,
|
||||
disabled: false,
|
||||
@@ -740,7 +740,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: 'select' as const,
|
||||
type: "select" as const,
|
||||
disabled: false,
|
||||
immutable: a.immutable ?? false,
|
||||
...a,
|
||||
@@ -766,7 +766,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
spec: {
|
||||
description: null,
|
||||
warning: null,
|
||||
type: 'select' as const,
|
||||
type: "select" as const,
|
||||
disabled: false,
|
||||
immutable: false,
|
||||
...a,
|
||||
@@ -837,7 +837,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<(keyof Values & string)[]>(
|
||||
() => ({
|
||||
spec: {
|
||||
type: 'multiselect' as const,
|
||||
type: "multiselect" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
warning: null,
|
||||
@@ -867,7 +867,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const a = await getA(options)
|
||||
return {
|
||||
spec: {
|
||||
type: 'multiselect' as const,
|
||||
type: "multiselect" as const,
|
||||
minLength: null,
|
||||
maxLength: null,
|
||||
warning: null,
|
||||
@@ -915,7 +915,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const built = await spec.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: 'object' as const,
|
||||
type: "object" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
@@ -933,7 +933,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
required: Required
|
||||
}) {
|
||||
const buildValue = {
|
||||
type: 'file' as const,
|
||||
type: "file" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...a,
|
||||
@@ -960,7 +960,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<AsRequired<FileInfo, Required>, FileInfo | null>(
|
||||
async (options) => {
|
||||
const spec = {
|
||||
type: 'file' as const,
|
||||
type: "file" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...(await a(options)),
|
||||
@@ -1034,7 +1034,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const built = await a.variants.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: 'union' as const,
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
disabled: false,
|
||||
@@ -1109,7 +1109,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const built = await newValues.variants.build(options as any)
|
||||
return {
|
||||
spec: {
|
||||
type: 'union' as const,
|
||||
type: "union" as const,
|
||||
description: null,
|
||||
warning: null,
|
||||
...newValues,
|
||||
@@ -1202,7 +1202,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
return new Value<T, typeof parser._TYPE>(async () => {
|
||||
return {
|
||||
spec: {
|
||||
type: 'hidden' as const,
|
||||
type: "hidden" as const,
|
||||
} as ValueSpecHidden,
|
||||
validator: parser,
|
||||
}
|
||||
@@ -1221,7 +1221,7 @@ export class Value<Type extends StaticValidatedAs, StaticValidatedAs = Type> {
|
||||
const validator = await getParser(options)
|
||||
return {
|
||||
spec: {
|
||||
type: 'hidden' as const,
|
||||
type: "hidden" as const,
|
||||
} as ValueSpecHidden,
|
||||
validator,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DeepPartial } from '../../../types'
|
||||
import { ValueSpec, ValueSpecUnion } from '../inputSpecTypes'
|
||||
import { DeepPartial } from "../../../types"
|
||||
import { ValueSpec, ValueSpecUnion } from "../inputSpecTypes"
|
||||
import {
|
||||
LazyBuild,
|
||||
InputSpec,
|
||||
ExtractInputSpecType,
|
||||
ExtractInputSpecStaticValidatedAs,
|
||||
} from './inputSpec'
|
||||
import { Parser, any, anyOf, literal, object } from 'ts-matches'
|
||||
} from "./inputSpec"
|
||||
import { Parser, any, anyOf, literal, object } from "ts-matches"
|
||||
|
||||
export type UnionRes<
|
||||
VariantValues extends {
|
||||
@@ -19,10 +19,10 @@ export type UnionRes<
|
||||
> = {
|
||||
[key in keyof VariantValues]: {
|
||||
selection: key
|
||||
value: ExtractInputSpecType<VariantValues[key]['spec']>
|
||||
value: ExtractInputSpecType<VariantValues[key]["spec"]>
|
||||
other?: {
|
||||
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
|
||||
ExtractInputSpecType<VariantValues[key2]['spec']>
|
||||
ExtractInputSpecType<VariantValues[key2]["spec"]>
|
||||
>
|
||||
}
|
||||
}
|
||||
@@ -39,10 +39,10 @@ export type UnionResStaticValidatedAs<
|
||||
> = {
|
||||
[key in keyof VariantValues]: {
|
||||
selection: key
|
||||
value: ExtractInputSpecStaticValidatedAs<VariantValues[key]['spec']>
|
||||
value: ExtractInputSpecStaticValidatedAs<VariantValues[key]["spec"]>
|
||||
other?: {
|
||||
[key2 in Exclude<keyof VariantValues & string, key>]?: DeepPartial<
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[key2]['spec']>
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[key2]["spec"]>
|
||||
>
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export class Variants<
|
||||
> {
|
||||
private constructor(
|
||||
public build: LazyBuild<{
|
||||
spec: ValueSpecUnion['variants']
|
||||
spec: ValueSpecUnion["variants"]
|
||||
validator: Parser<unknown, UnionRes<VariantValues>>
|
||||
}>,
|
||||
public readonly validator: Parser<
|
||||
@@ -126,7 +126,7 @@ export class Variants<
|
||||
const staticValidators = {} as {
|
||||
[K in keyof VariantValues]: Parser<
|
||||
unknown,
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[K]['spec']>
|
||||
ExtractInputSpecStaticValidatedAs<VariantValues[K]["spec"]>
|
||||
>
|
||||
}
|
||||
for (const key in a) {
|
||||
@@ -143,7 +143,7 @@ export class Variants<
|
||||
const validators = {} as {
|
||||
[K in keyof VariantValues]: Parser<
|
||||
unknown,
|
||||
ExtractInputSpecType<VariantValues[K]['spec']>
|
||||
ExtractInputSpecType<VariantValues[K]["spec"]>
|
||||
>
|
||||
}
|
||||
const variants = {} as {
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * as constants from './inputSpecConstants'
|
||||
export * as types from './inputSpecTypes'
|
||||
export * as builder from './builder'
|
||||
export * as constants from "./inputSpecConstants"
|
||||
export * as types from "./inputSpecTypes"
|
||||
export * as builder from "./builder"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SmtpValue } from '../../types'
|
||||
import { GetSystemSmtp, Patterns } from '../../util'
|
||||
import { InputSpec, InputSpecOf } from './builder/inputSpec'
|
||||
import { Value } from './builder/value'
|
||||
import { Variants } from './builder/variants'
|
||||
import { SmtpValue } from "../../types"
|
||||
import { GetSystemSmtp, Patterns } from "../../util"
|
||||
import { InputSpec, InputSpecOf } from "./builder/inputSpec"
|
||||
import { Value } from "./builder/value"
|
||||
import { Variants } from "./builder/variants"
|
||||
|
||||
/**
|
||||
* Base SMTP settings, to be used by StartOS for system wide SMTP
|
||||
@@ -11,12 +11,12 @@ export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
|
||||
InputSpecOf<SmtpValue>
|
||||
>({
|
||||
server: Value.text({
|
||||
name: 'SMTP Server',
|
||||
name: "SMTP Server",
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
port: Value.number({
|
||||
name: 'Port',
|
||||
name: "Port",
|
||||
required: true,
|
||||
default: 587,
|
||||
min: 1,
|
||||
@@ -24,20 +24,20 @@ export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
|
||||
integer: true,
|
||||
}),
|
||||
from: Value.text({
|
||||
name: 'From Address',
|
||||
name: "From Address",
|
||||
required: true,
|
||||
default: null,
|
||||
placeholder: 'Example Name <test@example.com>',
|
||||
inputmode: 'email',
|
||||
placeholder: "Example Name <test@example.com>",
|
||||
inputmode: "email",
|
||||
patterns: [Patterns.emailWithName],
|
||||
}),
|
||||
login: Value.text({
|
||||
name: 'Login',
|
||||
name: "Login",
|
||||
required: true,
|
||||
default: null,
|
||||
}),
|
||||
password: Value.text({
|
||||
name: 'Password',
|
||||
name: "Password",
|
||||
required: false,
|
||||
default: null,
|
||||
masked: true,
|
||||
@@ -45,24 +45,24 @@ export const customSmtp: InputSpec<SmtpValue> = InputSpec.of<
|
||||
})
|
||||
|
||||
const smtpVariants = Variants.of({
|
||||
disabled: { name: 'Disabled', spec: InputSpec.of({}) },
|
||||
disabled: { name: "Disabled", spec: InputSpec.of({}) },
|
||||
system: {
|
||||
name: 'System Credentials',
|
||||
name: "System Credentials",
|
||||
spec: InputSpec.of({
|
||||
customFrom: Value.text({
|
||||
name: 'Custom From Address',
|
||||
name: "Custom From Address",
|
||||
description:
|
||||
'A custom from address for this service. If not provided, the system from address will be used.',
|
||||
"A custom from address for this service. If not provided, the system from address will be used.",
|
||||
required: false,
|
||||
default: null,
|
||||
placeholder: '<name>test@example.com',
|
||||
inputmode: 'email',
|
||||
placeholder: "<name>test@example.com",
|
||||
inputmode: "email",
|
||||
patterns: [Patterns.email],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom Credentials',
|
||||
name: "Custom Credentials",
|
||||
spec: customSmtp,
|
||||
},
|
||||
})
|
||||
@@ -71,11 +71,11 @@ const smtpVariants = Variants.of({
|
||||
*/
|
||||
export const smtpInputSpec = Value.dynamicUnion(async ({ effects }) => {
|
||||
const smtp = await new GetSystemSmtp(effects).once()
|
||||
const disabled = smtp ? [] : ['system']
|
||||
const disabled = smtp ? [] : ["system"]
|
||||
return {
|
||||
name: 'SMTP',
|
||||
description: 'Optionally provide an SMTP server for sending emails',
|
||||
default: 'disabled',
|
||||
name: "SMTP",
|
||||
description: "Optionally provide an SMTP server for sending emails",
|
||||
default: "disabled",
|
||||
disabled,
|
||||
variants: smtpVariants,
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
export type InputSpec = Record<string, ValueSpec>
|
||||
export type ValueType =
|
||||
| 'text'
|
||||
| 'textarea'
|
||||
| 'number'
|
||||
| 'color'
|
||||
| 'datetime'
|
||||
| 'toggle'
|
||||
| 'select'
|
||||
| 'multiselect'
|
||||
| 'list'
|
||||
| 'object'
|
||||
| 'file'
|
||||
| 'union'
|
||||
| 'hidden'
|
||||
| "text"
|
||||
| "textarea"
|
||||
| "number"
|
||||
| "color"
|
||||
| "datetime"
|
||||
| "toggle"
|
||||
| "select"
|
||||
| "multiselect"
|
||||
| "list"
|
||||
| "object"
|
||||
| "file"
|
||||
| "union"
|
||||
| "hidden"
|
||||
export type ValueSpec = ValueSpecOf<ValueType>
|
||||
/** core spec types. These types provide the metadata for performing validations */
|
||||
// prettier-ignore
|
||||
@@ -37,13 +37,13 @@ export type ValueSpecText = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'text'
|
||||
type: "text"
|
||||
patterns: Pattern[]
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
masked: boolean
|
||||
|
||||
inputmode: 'text' | 'email' | 'tel' | 'url'
|
||||
inputmode: "text" | "email" | "tel" | "url"
|
||||
placeholder: string | null
|
||||
|
||||
required: boolean
|
||||
@@ -57,7 +57,7 @@ export type ValueSpecTextarea = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'textarea'
|
||||
type: "textarea"
|
||||
patterns: Pattern[]
|
||||
placeholder: string | null
|
||||
minLength: number | null
|
||||
@@ -71,7 +71,7 @@ export type ValueSpecTextarea = {
|
||||
}
|
||||
|
||||
export type ValueSpecNumber = {
|
||||
type: 'number'
|
||||
type: "number"
|
||||
min: number | null
|
||||
max: number | null
|
||||
integer: boolean
|
||||
@@ -91,7 +91,7 @@ export type ValueSpecColor = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'color'
|
||||
type: "color"
|
||||
required: boolean
|
||||
default: string | null
|
||||
disabled: false | string
|
||||
@@ -101,9 +101,9 @@ export type ValueSpecDatetime = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'datetime'
|
||||
type: "datetime"
|
||||
required: boolean
|
||||
inputmode: 'date' | 'time' | 'datetime-local'
|
||||
inputmode: "date" | "time" | "datetime-local"
|
||||
min: string | null
|
||||
max: string | null
|
||||
default: string | null
|
||||
@@ -115,7 +115,7 @@ export type ValueSpecSelect = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'select'
|
||||
type: "select"
|
||||
default: string | null
|
||||
disabled: false | string | string[]
|
||||
immutable: boolean
|
||||
@@ -127,7 +127,7 @@ export type ValueSpecMultiselect = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'multiselect'
|
||||
type: "multiselect"
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
disabled: false | string | string[]
|
||||
@@ -139,7 +139,7 @@ export type ValueSpecToggle = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'toggle'
|
||||
type: "toggle"
|
||||
default: boolean | null
|
||||
disabled: false | string
|
||||
immutable: boolean
|
||||
@@ -149,7 +149,7 @@ export type ValueSpecUnion = {
|
||||
description: string | null
|
||||
warning: string | null
|
||||
|
||||
type: 'union'
|
||||
type: "union"
|
||||
variants: Record<
|
||||
string,
|
||||
{
|
||||
@@ -165,7 +165,7 @@ export type ValueSpecFile = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'file'
|
||||
type: "file"
|
||||
extensions: string[]
|
||||
required: boolean
|
||||
}
|
||||
@@ -173,13 +173,13 @@ export type ValueSpecObject = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'object'
|
||||
type: "object"
|
||||
spec: InputSpec
|
||||
}
|
||||
export type ValueSpecHidden = {
|
||||
type: 'hidden'
|
||||
type: "hidden"
|
||||
}
|
||||
export type ListValueSpecType = 'text' | 'object'
|
||||
export type ListValueSpecType = "text" | "object"
|
||||
// prettier-ignore
|
||||
export type ListValueSpecOf<T extends ListValueSpecType> =
|
||||
T extends "text" ? ListValueSpecText :
|
||||
@@ -190,7 +190,7 @@ export type ValueSpecListOf<T extends ListValueSpecType> = {
|
||||
name: string
|
||||
description: string | null
|
||||
warning: string | null
|
||||
type: 'list'
|
||||
type: "list"
|
||||
spec: ListValueSpecOf<T>
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
@@ -208,18 +208,18 @@ export type Pattern = {
|
||||
description: string
|
||||
}
|
||||
export type ListValueSpecText = {
|
||||
type: 'text'
|
||||
type: "text"
|
||||
patterns: Pattern[]
|
||||
minLength: number | null
|
||||
maxLength: number | null
|
||||
masked: boolean
|
||||
|
||||
generate: null | RandomString
|
||||
inputmode: 'text' | 'email' | 'tel' | 'url'
|
||||
inputmode: "text" | "email" | "tel" | "url"
|
||||
placeholder: string | null
|
||||
}
|
||||
export type ListValueSpecObject = {
|
||||
type: 'object'
|
||||
type: "object"
|
||||
spec: InputSpec
|
||||
uniqueBy: UniqueBy
|
||||
displayAs: string | null
|
||||
@@ -244,5 +244,5 @@ export function isValueSpecListOf<S extends ListValueSpecType>(
|
||||
t: ValueSpec,
|
||||
s: S,
|
||||
): t is ValueSpecListOf<S> & { spec: ListValueSpecOf<S> } {
|
||||
return 'spec' in t && t.spec.type === s
|
||||
return "spec" in t && t.spec.type === s
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { InputSpec } from './input/builder'
|
||||
import { ExtractInputSpecType } from './input/builder/inputSpec'
|
||||
import * as T from '../types'
|
||||
import { once } from '../util'
|
||||
import { InitScript } from '../inits'
|
||||
import { Parser } from 'ts-matches'
|
||||
import { InputSpec } from "./input/builder"
|
||||
import { ExtractInputSpecType } from "./input/builder/inputSpec"
|
||||
import * as T from "../types"
|
||||
import { once } from "../util"
|
||||
import { InitScript } from "../inits"
|
||||
import { Parser } from "ts-matches"
|
||||
|
||||
type MaybeInputSpec<Type> = {} extends Type ? null : InputSpec<Type>
|
||||
export type Run<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
input: A
|
||||
spec: T.inputSpecTypes.InputSpec
|
||||
}) => Promise<(T.ActionResult & { version: '1' }) | null | void | undefined>
|
||||
}) => Promise<(T.ActionResult & { version: "1" }) | null | void | undefined>
|
||||
export type GetInput<A extends Record<string, any>> = (options: {
|
||||
effects: T.Effects
|
||||
}) => Promise<null | void | undefined | T.DeepPartial<A>>
|
||||
@@ -65,7 +65,7 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
InputSpecType extends InputSpec<Record<string, any>>,
|
||||
>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, 'hasInput'>>,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
inputSpec: InputSpecType,
|
||||
getInput: GetInput<ExtractInputSpecType<InputSpecType>>,
|
||||
run: Run<ExtractInputSpecType<InputSpecType>>,
|
||||
@@ -80,7 +80,7 @@ export class Action<Id extends T.ActionId, Type extends Record<string, any>>
|
||||
}
|
||||
static withoutInput<Id extends T.ActionId>(
|
||||
id: Id,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, 'hasInput'>>,
|
||||
metadata: MaybeFn<Omit<T.ActionMetadata, "hasInput">>,
|
||||
run: Run<{}>,
|
||||
): Action<Id, {}> {
|
||||
return new Action(
|
||||
@@ -156,7 +156,7 @@ export class Actions<
|
||||
}
|
||||
addAction<A extends Action<T.ActionId, any>>(
|
||||
action: A, // TODO: prevent duplicates
|
||||
): Actions<AllActions & { [id in A['id']]: A }> {
|
||||
): Actions<AllActions & { [id in A["id"]]: A }> {
|
||||
return new Actions({ ...this.actions, [action.id]: action })
|
||||
}
|
||||
async init(effects: T.Effects): Promise<void> {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ExtendedVersion, VersionRange } from '../exver'
|
||||
import { ExtendedVersion, VersionRange } from "../exver"
|
||||
import {
|
||||
PackageId,
|
||||
HealthCheckId,
|
||||
DependencyRequirement,
|
||||
CheckDependenciesResult,
|
||||
} from '../types'
|
||||
import { Effects } from '../Effects'
|
||||
} from "../types"
|
||||
import { Effects } from "../Effects"
|
||||
|
||||
export type CheckDependencies<DependencyId extends PackageId = PackageId> = {
|
||||
infoFor: (packageId: DependencyId) => {
|
||||
@@ -73,11 +73,11 @@ export async function checkDependencies<
|
||||
}
|
||||
const runningSatisfied = (packageId: DependencyId) => {
|
||||
const dep = infoFor(packageId)
|
||||
return dep.requirement.kind !== 'running' || dep.result.isRunning
|
||||
return dep.requirement.kind !== "running" || dep.result.isRunning
|
||||
}
|
||||
const tasksSatisfied = (packageId: DependencyId) =>
|
||||
Object.entries(infoFor(packageId).result.tasks).filter(
|
||||
([_, t]) => t?.active && t.task.severity === 'critical',
|
||||
([_, t]) => t?.active && t.task.severity === "critical",
|
||||
).length === 0
|
||||
const healthCheckSatisfied = (
|
||||
packageId: DependencyId,
|
||||
@@ -86,17 +86,17 @@ export async function checkDependencies<
|
||||
const dep = infoFor(packageId)
|
||||
if (
|
||||
healthCheckId &&
|
||||
(dep.requirement.kind !== 'running' ||
|
||||
(dep.requirement.kind !== "running" ||
|
||||
!dep.requirement.healthChecks.includes(healthCheckId))
|
||||
) {
|
||||
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
|
||||
}
|
||||
const errors =
|
||||
dep.requirement.kind === 'running'
|
||||
dep.requirement.kind === "running"
|
||||
? dep.requirement.healthChecks
|
||||
.map((id) => [id, dep.result.healthChecks[id] ?? null] as const)
|
||||
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
|
||||
.filter(([_, res]) => res?.result !== 'success')
|
||||
.filter(([_, res]) => res?.result !== "success")
|
||||
: []
|
||||
return errors.length === 0
|
||||
}
|
||||
@@ -138,7 +138,7 @@ export async function checkDependencies<
|
||||
}
|
||||
const throwIfRunningNotSatisfied = (packageId: DependencyId) => {
|
||||
const dep = infoFor(packageId)
|
||||
if (dep.requirement.kind === 'running' && !dep.result.isRunning) {
|
||||
if (dep.requirement.kind === "running" && !dep.result.isRunning) {
|
||||
throw new Error(`${dep.result.title || packageId} is not running`)
|
||||
}
|
||||
return null
|
||||
@@ -146,11 +146,11 @@ export async function checkDependencies<
|
||||
const throwIfTasksNotSatisfied = (packageId: DependencyId) => {
|
||||
const dep = infoFor(packageId)
|
||||
const reqs = Object.entries(dep.result.tasks)
|
||||
.filter(([_, t]) => t?.active && t.task.severity === 'critical')
|
||||
.filter(([_, t]) => t?.active && t.task.severity === "critical")
|
||||
.map(([id, _]) => id)
|
||||
if (reqs.length) {
|
||||
throw new Error(
|
||||
`The following action requests have not been fulfilled: ${reqs.join(', ')}`,
|
||||
`The following action requests have not been fulfilled: ${reqs.join(", ")}`,
|
||||
)
|
||||
}
|
||||
return null
|
||||
@@ -162,27 +162,27 @@ export async function checkDependencies<
|
||||
const dep = infoFor(packageId)
|
||||
if (
|
||||
healthCheckId &&
|
||||
(dep.requirement.kind !== 'running' ||
|
||||
(dep.requirement.kind !== "running" ||
|
||||
!dep.requirement.healthChecks.includes(healthCheckId))
|
||||
) {
|
||||
throw new Error(`Unknown HealthCheckId ${healthCheckId}`)
|
||||
}
|
||||
const errors =
|
||||
dep.requirement.kind === 'running'
|
||||
dep.requirement.kind === "running"
|
||||
? dep.requirement.healthChecks
|
||||
.map((id) => [id, dep.result.healthChecks[id] ?? null] as const)
|
||||
.filter(([id, _]) => (healthCheckId ? id === healthCheckId : true))
|
||||
.filter(([_, res]) => res?.result !== 'success')
|
||||
.filter(([_, res]) => res?.result !== "success")
|
||||
: []
|
||||
if (errors.length) {
|
||||
throw new Error(
|
||||
errors
|
||||
.map(([id, e]) =>
|
||||
e
|
||||
? `Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ''}`
|
||||
? `Health Check ${e.name} of ${dep.result.title || packageId} failed with status ${e.result}${e.message ? `: ${e.message}` : ""}`
|
||||
: `Health Check ${id} of ${dep.result.title} does not exist`,
|
||||
)
|
||||
.join('; '),
|
||||
.join("; "),
|
||||
)
|
||||
}
|
||||
return null
|
||||
@@ -209,7 +209,7 @@ export async function checkDependencies<
|
||||
return []
|
||||
})
|
||||
if (err.length) {
|
||||
throw new Error(err.join('; '))
|
||||
throw new Error(err.join("; "))
|
||||
}
|
||||
return null
|
||||
})()
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import * as T from '../types'
|
||||
import { once } from '../util'
|
||||
import * as T from "../types"
|
||||
import { once } from "../util"
|
||||
|
||||
export type RequiredDependenciesOf<Manifest extends T.SDKManifest> = {
|
||||
[K in keyof Manifest['dependencies']]: Exclude<
|
||||
Manifest['dependencies'][K],
|
||||
[K in keyof Manifest["dependencies"]]: Exclude<
|
||||
Manifest["dependencies"][K],
|
||||
undefined
|
||||
>['optional'] extends false
|
||||
>["optional"] extends false
|
||||
? K
|
||||
: never
|
||||
}[keyof Manifest['dependencies']]
|
||||
}[keyof Manifest["dependencies"]]
|
||||
export type OptionalDependenciesOf<Manifest extends T.SDKManifest> = Exclude<
|
||||
keyof Manifest['dependencies'],
|
||||
keyof Manifest["dependencies"],
|
||||
RequiredDependenciesOf<Manifest>
|
||||
>
|
||||
|
||||
type DependencyRequirement =
|
||||
| {
|
||||
kind: 'running'
|
||||
kind: "running"
|
||||
healthChecks: Array<T.HealthCheckId>
|
||||
versionRange: string
|
||||
}
|
||||
| {
|
||||
kind: 'exists'
|
||||
kind: "exists"
|
||||
versionRange: string
|
||||
}
|
||||
type Matches<T, U> = T extends U ? (U extends T ? null : never) : never
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import { DeepMap } from 'deep-equality-data-structures'
|
||||
import * as P from './exver'
|
||||
import { DeepMap } from "deep-equality-data-structures"
|
||||
import * as P from "./exver"
|
||||
|
||||
// prettier-ignore
|
||||
export type ValidateVersion<T extends String> =
|
||||
@@ -22,35 +22,35 @@ export type ValidateExVers<T> =
|
||||
never[]
|
||||
|
||||
type Anchor = {
|
||||
type: 'Anchor'
|
||||
type: "Anchor"
|
||||
operator: P.CmpOp
|
||||
version: ExtendedVersion
|
||||
}
|
||||
|
||||
type And = {
|
||||
type: 'And'
|
||||
type: "And"
|
||||
left: VersionRange
|
||||
right: VersionRange
|
||||
}
|
||||
|
||||
type Or = {
|
||||
type: 'Or'
|
||||
type: "Or"
|
||||
left: VersionRange
|
||||
right: VersionRange
|
||||
}
|
||||
|
||||
type Not = {
|
||||
type: 'Not'
|
||||
type: "Not"
|
||||
value: VersionRange
|
||||
}
|
||||
|
||||
type Flavor = {
|
||||
type: 'Flavor'
|
||||
type: "Flavor"
|
||||
flavor: string | null
|
||||
}
|
||||
|
||||
type FlavorNot = {
|
||||
type: 'FlavorNot'
|
||||
type: "FlavorNot"
|
||||
flavors: Set<string | null>
|
||||
}
|
||||
|
||||
@@ -107,8 +107,8 @@ function adjacentVersionRangePoints(
|
||||
}
|
||||
|
||||
function flavorAnd(a: FlavorAtom, b: FlavorAtom): FlavorAtom | null {
|
||||
if (a.type == 'Flavor') {
|
||||
if (b.type == 'Flavor') {
|
||||
if (a.type == "Flavor") {
|
||||
if (b.type == "Flavor") {
|
||||
if (a.flavor == b.flavor) {
|
||||
return a
|
||||
} else {
|
||||
@@ -122,7 +122,7 @@ function flavorAnd(a: FlavorAtom, b: FlavorAtom): FlavorAtom | null {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (b.type == 'Flavor') {
|
||||
if (b.type == "Flavor") {
|
||||
if (a.flavors.has(b.flavor)) {
|
||||
return null
|
||||
} else {
|
||||
@@ -131,7 +131,7 @@ function flavorAnd(a: FlavorAtom, b: FlavorAtom): FlavorAtom | null {
|
||||
} else {
|
||||
// TODO: use Set.union if targeting esnext or later
|
||||
return {
|
||||
type: 'FlavorNot',
|
||||
type: "FlavorNot",
|
||||
flavors: new Set([...a.flavors, ...b.flavors]),
|
||||
}
|
||||
}
|
||||
@@ -218,12 +218,12 @@ class VersionRangeTable {
|
||||
static eqFlavor(flavor: string | null): VersionRangeTables {
|
||||
return new DeepMap([
|
||||
[
|
||||
{ type: 'Flavor', flavor } as FlavorAtom,
|
||||
{ type: "Flavor", flavor } as FlavorAtom,
|
||||
new VersionRangeTable([], [true]),
|
||||
],
|
||||
// make sure the truth table is exhaustive, or `not` will not work properly.
|
||||
[
|
||||
{ type: 'FlavorNot', flavors: new Set([flavor]) } as FlavorAtom,
|
||||
{ type: "FlavorNot", flavors: new Set([flavor]) } as FlavorAtom,
|
||||
new VersionRangeTable([], [false]),
|
||||
],
|
||||
])
|
||||
@@ -241,12 +241,12 @@ class VersionRangeTable {
|
||||
): VersionRangeTables {
|
||||
return new DeepMap([
|
||||
[
|
||||
{ type: 'Flavor', flavor } as FlavorAtom,
|
||||
{ type: "Flavor", flavor } as FlavorAtom,
|
||||
new VersionRangeTable([point], [left, right]),
|
||||
],
|
||||
// make sure the truth table is exhaustive, or `not` will not work properly.
|
||||
[
|
||||
{ type: 'FlavorNot', flavors: new Set([flavor]) } as FlavorAtom,
|
||||
{ type: "FlavorNot", flavors: new Set([flavor]) } as FlavorAtom,
|
||||
new VersionRangeTable([], [false]),
|
||||
],
|
||||
])
|
||||
@@ -383,7 +383,7 @@ class VersionRangeTable {
|
||||
let sum_terms: VersionRange[] = []
|
||||
for (let [flavor, table] of tables) {
|
||||
let cmp_flavor = null
|
||||
if (flavor.type == 'Flavor') {
|
||||
if (flavor.type == "Flavor") {
|
||||
cmp_flavor = flavor.flavor
|
||||
}
|
||||
for (let i = 0; i < table.values.length; i++) {
|
||||
@@ -392,7 +392,7 @@ class VersionRangeTable {
|
||||
continue
|
||||
}
|
||||
|
||||
if (flavor.type == 'FlavorNot') {
|
||||
if (flavor.type == "FlavorNot") {
|
||||
for (let not_flavor of flavor.flavors) {
|
||||
term.push(VersionRange.flavor(not_flavor).not())
|
||||
}
|
||||
@@ -410,7 +410,7 @@ class VersionRangeTable {
|
||||
if (p != null && q != null && adjacentVersionRangePoints(p, q)) {
|
||||
term.push(
|
||||
VersionRange.anchor(
|
||||
'=',
|
||||
"=",
|
||||
new ExtendedVersion(cmp_flavor, p.upstream, p.downstream),
|
||||
),
|
||||
)
|
||||
@@ -418,7 +418,7 @@ class VersionRangeTable {
|
||||
if (p != null && p.side < 0) {
|
||||
term.push(
|
||||
VersionRange.anchor(
|
||||
'>=',
|
||||
">=",
|
||||
new ExtendedVersion(cmp_flavor, p.upstream, p.downstream),
|
||||
),
|
||||
)
|
||||
@@ -426,7 +426,7 @@ class VersionRangeTable {
|
||||
if (p != null && p.side >= 0) {
|
||||
term.push(
|
||||
VersionRange.anchor(
|
||||
'>',
|
||||
">",
|
||||
new ExtendedVersion(cmp_flavor, p.upstream, p.downstream),
|
||||
),
|
||||
)
|
||||
@@ -434,7 +434,7 @@ class VersionRangeTable {
|
||||
if (q != null && q.side < 0) {
|
||||
term.push(
|
||||
VersionRange.anchor(
|
||||
'<',
|
||||
"<",
|
||||
new ExtendedVersion(cmp_flavor, q.upstream, q.downstream),
|
||||
),
|
||||
)
|
||||
@@ -442,7 +442,7 @@ class VersionRangeTable {
|
||||
if (q != null && q.side >= 0) {
|
||||
term.push(
|
||||
VersionRange.anchor(
|
||||
'<=',
|
||||
"<=",
|
||||
new ExtendedVersion(cmp_flavor, q.upstream, q.downstream),
|
||||
),
|
||||
)
|
||||
@@ -463,26 +463,26 @@ class VersionRangeTable {
|
||||
export class VersionRange {
|
||||
constructor(public atom: Anchor | And | Or | Not | P.Any | P.None | Flavor) {}
|
||||
|
||||
toStringParens(parent: 'And' | 'Or' | 'Not') {
|
||||
toStringParens(parent: "And" | "Or" | "Not") {
|
||||
let needs = true
|
||||
switch (this.atom.type) {
|
||||
case 'And':
|
||||
case 'Or':
|
||||
case "And":
|
||||
case "Or":
|
||||
needs = parent != this.atom.type
|
||||
break
|
||||
case 'Anchor':
|
||||
case 'Any':
|
||||
case 'None':
|
||||
needs = parent == 'Not'
|
||||
case "Anchor":
|
||||
case "Any":
|
||||
case "None":
|
||||
needs = parent == "Not"
|
||||
break
|
||||
case 'Not':
|
||||
case 'Flavor':
|
||||
case "Not":
|
||||
case "Flavor":
|
||||
needs = false
|
||||
break
|
||||
}
|
||||
|
||||
if (needs) {
|
||||
return '(' + this.toString() + ')'
|
||||
return "(" + this.toString() + ")"
|
||||
} else {
|
||||
return this.toString()
|
||||
}
|
||||
@@ -490,36 +490,36 @@ export class VersionRange {
|
||||
|
||||
toString(): string {
|
||||
switch (this.atom.type) {
|
||||
case 'Anchor':
|
||||
case "Anchor":
|
||||
return `${this.atom.operator}${this.atom.version}`
|
||||
case 'And':
|
||||
case "And":
|
||||
return `${this.atom.left.toStringParens(this.atom.type)} && ${this.atom.right.toStringParens(this.atom.type)}`
|
||||
case 'Or':
|
||||
case "Or":
|
||||
return `${this.atom.left.toStringParens(this.atom.type)} || ${this.atom.right.toStringParens(this.atom.type)}`
|
||||
case 'Not':
|
||||
case "Not":
|
||||
return `!${this.atom.value.toStringParens(this.atom.type)}`
|
||||
case 'Flavor':
|
||||
case "Flavor":
|
||||
return this.atom.flavor == null ? `#` : `#${this.atom.flavor}`
|
||||
case 'Any':
|
||||
return '*'
|
||||
case 'None':
|
||||
return '!'
|
||||
case "Any":
|
||||
return "*"
|
||||
case "None":
|
||||
return "!"
|
||||
}
|
||||
}
|
||||
|
||||
private static parseAtom(atom: P.VersionRangeAtom): VersionRange {
|
||||
switch (atom.type) {
|
||||
case 'Not':
|
||||
case "Not":
|
||||
return new VersionRange({
|
||||
type: 'Not',
|
||||
type: "Not",
|
||||
value: VersionRange.parseAtom(atom.value),
|
||||
})
|
||||
case 'Parens':
|
||||
case "Parens":
|
||||
return VersionRange.parseRange(atom.expr)
|
||||
case 'Anchor':
|
||||
case "Anchor":
|
||||
return new VersionRange({
|
||||
type: 'Anchor',
|
||||
operator: atom.operator || '^',
|
||||
type: "Anchor",
|
||||
operator: atom.operator || "^",
|
||||
version: new ExtendedVersion(
|
||||
atom.version.flavor,
|
||||
new Version(
|
||||
@@ -532,7 +532,7 @@ export class VersionRange {
|
||||
),
|
||||
),
|
||||
})
|
||||
case 'Flavor':
|
||||
case "Flavor":
|
||||
return VersionRange.flavor(atom.flavor)
|
||||
default:
|
||||
return new VersionRange(atom)
|
||||
@@ -543,17 +543,17 @@ export class VersionRange {
|
||||
let result = VersionRange.parseAtom(range[0])
|
||||
for (const next of range[1]) {
|
||||
switch (next[1]?.[0]) {
|
||||
case '||':
|
||||
case "||":
|
||||
result = new VersionRange({
|
||||
type: 'Or',
|
||||
type: "Or",
|
||||
left: result,
|
||||
right: VersionRange.parseAtom(next[2]),
|
||||
})
|
||||
break
|
||||
case '&&':
|
||||
case "&&":
|
||||
default:
|
||||
result = new VersionRange({
|
||||
type: 'And',
|
||||
type: "And",
|
||||
left: result,
|
||||
right: VersionRange.parseAtom(next[2]),
|
||||
})
|
||||
@@ -565,49 +565,49 @@ export class VersionRange {
|
||||
|
||||
static parse(range: string): VersionRange {
|
||||
return VersionRange.parseRange(
|
||||
P.parse(range, { startRule: 'VersionRange' }),
|
||||
P.parse(range, { startRule: "VersionRange" }),
|
||||
)
|
||||
}
|
||||
|
||||
static anchor(operator: P.CmpOp, version: ExtendedVersion) {
|
||||
return new VersionRange({ type: 'Anchor', operator, version })
|
||||
return new VersionRange({ type: "Anchor", operator, version })
|
||||
}
|
||||
|
||||
static flavor(flavor: string | null) {
|
||||
return new VersionRange({ type: 'Flavor', flavor })
|
||||
return new VersionRange({ type: "Flavor", flavor })
|
||||
}
|
||||
|
||||
static parseEmver(range: string): VersionRange {
|
||||
return VersionRange.parseRange(
|
||||
P.parse(range, { startRule: 'EmverVersionRange' }),
|
||||
P.parse(range, { startRule: "EmverVersionRange" }),
|
||||
)
|
||||
}
|
||||
|
||||
and(right: VersionRange) {
|
||||
return new VersionRange({ type: 'And', left: this, right })
|
||||
return new VersionRange({ type: "And", left: this, right })
|
||||
}
|
||||
|
||||
or(right: VersionRange) {
|
||||
return new VersionRange({ type: 'Or', left: this, right })
|
||||
return new VersionRange({ type: "Or", left: this, right })
|
||||
}
|
||||
|
||||
not() {
|
||||
return new VersionRange({ type: 'Not', value: this })
|
||||
return new VersionRange({ type: "Not", value: this })
|
||||
}
|
||||
|
||||
static and(...xs: Array<VersionRange>) {
|
||||
let y = VersionRange.any()
|
||||
for (let x of xs) {
|
||||
if (x.atom.type == 'Any') {
|
||||
if (x.atom.type == "Any") {
|
||||
continue
|
||||
}
|
||||
if (x.atom.type == 'None') {
|
||||
if (x.atom.type == "None") {
|
||||
return x
|
||||
}
|
||||
if (y.atom.type == 'Any') {
|
||||
if (y.atom.type == "Any") {
|
||||
y = x
|
||||
} else {
|
||||
y = new VersionRange({ type: 'And', left: y, right: x })
|
||||
y = new VersionRange({ type: "And", left: y, right: x })
|
||||
}
|
||||
}
|
||||
return y
|
||||
@@ -616,27 +616,27 @@ export class VersionRange {
|
||||
static or(...xs: Array<VersionRange>) {
|
||||
let y = VersionRange.none()
|
||||
for (let x of xs) {
|
||||
if (x.atom.type == 'None') {
|
||||
if (x.atom.type == "None") {
|
||||
continue
|
||||
}
|
||||
if (x.atom.type == 'Any') {
|
||||
if (x.atom.type == "Any") {
|
||||
return x
|
||||
}
|
||||
if (y.atom.type == 'None') {
|
||||
if (y.atom.type == "None") {
|
||||
y = x
|
||||
} else {
|
||||
y = new VersionRange({ type: 'Or', left: y, right: x })
|
||||
y = new VersionRange({ type: "Or", left: y, right: x })
|
||||
}
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
static any() {
|
||||
return new VersionRange({ type: 'Any' })
|
||||
return new VersionRange({ type: "Any" })
|
||||
}
|
||||
|
||||
static none() {
|
||||
return new VersionRange({ type: 'None' })
|
||||
return new VersionRange({ type: "None" })
|
||||
}
|
||||
|
||||
satisfiedBy(version: Version | ExtendedVersion) {
|
||||
@@ -645,23 +645,23 @@ export class VersionRange {
|
||||
|
||||
tables(): VersionRangeTables {
|
||||
switch (this.atom.type) {
|
||||
case 'Anchor':
|
||||
case "Anchor":
|
||||
switch (this.atom.operator) {
|
||||
case '=':
|
||||
case "=":
|
||||
// `=1.2.3` is equivalent to `>=1.2.3 && <=1.2.4 && #flavor`
|
||||
return VersionRangeTable.and(
|
||||
VersionRangeTable.cmp(this.atom.version, -1, false, true),
|
||||
VersionRangeTable.cmp(this.atom.version, 1, true, false),
|
||||
)
|
||||
case '>':
|
||||
case ">":
|
||||
return VersionRangeTable.cmp(this.atom.version, 1, false, true)
|
||||
case '<':
|
||||
case "<":
|
||||
return VersionRangeTable.cmp(this.atom.version, -1, true, false)
|
||||
case '>=':
|
||||
case ">=":
|
||||
return VersionRangeTable.cmp(this.atom.version, -1, false, true)
|
||||
case '<=':
|
||||
case "<=":
|
||||
return VersionRangeTable.cmp(this.atom.version, 1, true, false)
|
||||
case '!=':
|
||||
case "!=":
|
||||
// `!=1.2.3` is equivalent to `!(>=1.2.3 && <=1.2.3 && #flavor)`
|
||||
// **not** equivalent to `(<1.2.3 || >1.2.3) && #flavor`
|
||||
return VersionRangeTable.not(
|
||||
@@ -670,7 +670,7 @@ export class VersionRange {
|
||||
VersionRangeTable.cmp(this.atom.version, 1, true, false),
|
||||
),
|
||||
)
|
||||
case '^':
|
||||
case "^":
|
||||
// `^1.2.3` is equivalent to `>=1.2.3 && <2.0.0 && #flavor`
|
||||
return VersionRangeTable.and(
|
||||
VersionRangeTable.cmp(this.atom.version, -1, false, true),
|
||||
@@ -681,7 +681,7 @@ export class VersionRange {
|
||||
false,
|
||||
),
|
||||
)
|
||||
case '~':
|
||||
case "~":
|
||||
// `~1.2.3` is equivalent to `>=1.2.3 && <1.3.0 && #flavor`
|
||||
return VersionRangeTable.and(
|
||||
VersionRangeTable.cmp(this.atom.version, -1, false, true),
|
||||
@@ -693,23 +693,23 @@ export class VersionRange {
|
||||
),
|
||||
)
|
||||
}
|
||||
case 'Flavor':
|
||||
case "Flavor":
|
||||
return VersionRangeTable.eqFlavor(this.atom.flavor)
|
||||
case 'Not':
|
||||
case "Not":
|
||||
return VersionRangeTable.not(this.atom.value.tables())
|
||||
case 'And':
|
||||
case "And":
|
||||
return VersionRangeTable.and(
|
||||
this.atom.left.tables(),
|
||||
this.atom.right.tables(),
|
||||
)
|
||||
case 'Or':
|
||||
case "Or":
|
||||
return VersionRangeTable.or(
|
||||
this.atom.left.tables(),
|
||||
this.atom.right.tables(),
|
||||
)
|
||||
case 'Any':
|
||||
case "Any":
|
||||
return true
|
||||
case 'None':
|
||||
case "None":
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -734,23 +734,23 @@ export class Version {
|
||||
) {}
|
||||
|
||||
toString(): string {
|
||||
return `${this.number.join('.')}${this.prerelease.length > 0 ? `-${this.prerelease.join('.')}` : ''}`
|
||||
return `${this.number.join(".")}${this.prerelease.length > 0 ? `-${this.prerelease.join(".")}` : ""}`
|
||||
}
|
||||
|
||||
compare(other: Version): 'greater' | 'equal' | 'less' {
|
||||
compare(other: Version): "greater" | "equal" | "less" {
|
||||
const numLen = Math.max(this.number.length, other.number.length)
|
||||
for (let i = 0; i < numLen; i++) {
|
||||
if ((this.number[i] || 0) > (other.number[i] || 0)) {
|
||||
return 'greater'
|
||||
return "greater"
|
||||
} else if ((this.number[i] || 0) < (other.number[i] || 0)) {
|
||||
return 'less'
|
||||
return "less"
|
||||
}
|
||||
}
|
||||
|
||||
if (this.prerelease.length === 0 && other.prerelease.length !== 0) {
|
||||
return 'greater'
|
||||
return "greater"
|
||||
} else if (this.prerelease.length !== 0 && other.prerelease.length === 0) {
|
||||
return 'less'
|
||||
return "less"
|
||||
}
|
||||
|
||||
const prereleaseLen = Math.max(
|
||||
@@ -760,42 +760,42 @@ export class Version {
|
||||
for (let i = 0; i < prereleaseLen; i++) {
|
||||
if (typeof this.prerelease[i] === typeof other.prerelease[i]) {
|
||||
if (this.prerelease[i] > other.prerelease[i]) {
|
||||
return 'greater'
|
||||
return "greater"
|
||||
} else if (this.prerelease[i] < other.prerelease[i]) {
|
||||
return 'less'
|
||||
return "less"
|
||||
}
|
||||
} else {
|
||||
switch (`${typeof this.prerelease[1]}:${typeof other.prerelease[i]}`) {
|
||||
case 'number:string':
|
||||
return 'less'
|
||||
case 'string:number':
|
||||
return 'greater'
|
||||
case 'number:undefined':
|
||||
case 'string:undefined':
|
||||
return 'greater'
|
||||
case 'undefined:number':
|
||||
case 'undefined:string':
|
||||
return 'less'
|
||||
case "number:string":
|
||||
return "less"
|
||||
case "string:number":
|
||||
return "greater"
|
||||
case "number:undefined":
|
||||
case "string:undefined":
|
||||
return "greater"
|
||||
case "undefined:number":
|
||||
case "undefined:string":
|
||||
return "less"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'equal'
|
||||
return "equal"
|
||||
}
|
||||
|
||||
compareForSort(other: Version): -1 | 0 | 1 {
|
||||
switch (this.compare(other)) {
|
||||
case 'greater':
|
||||
case "greater":
|
||||
return 1
|
||||
case 'equal':
|
||||
case "equal":
|
||||
return 0
|
||||
case 'less':
|
||||
case "less":
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
static parse(version: string): Version {
|
||||
const parsed = P.parse(version, { startRule: 'Version' })
|
||||
const parsed = P.parse(version, { startRule: "Version" })
|
||||
return new Version(parsed.number, parsed.prerelease)
|
||||
}
|
||||
|
||||
@@ -815,25 +815,25 @@ export class ExtendedVersion {
|
||||
) {}
|
||||
|
||||
toString(): string {
|
||||
return `${this.flavor ? `#${this.flavor}:` : ''}${this.upstream.toString()}:${this.downstream.toString()}`
|
||||
return `${this.flavor ? `#${this.flavor}:` : ""}${this.upstream.toString()}:${this.downstream.toString()}`
|
||||
}
|
||||
|
||||
compare(other: ExtendedVersion): 'greater' | 'equal' | 'less' | null {
|
||||
compare(other: ExtendedVersion): "greater" | "equal" | "less" | null {
|
||||
if (this.flavor !== other.flavor) {
|
||||
return null
|
||||
}
|
||||
const upstreamCmp = this.upstream.compare(other.upstream)
|
||||
if (upstreamCmp !== 'equal') {
|
||||
if (upstreamCmp !== "equal") {
|
||||
return upstreamCmp
|
||||
}
|
||||
return this.downstream.compare(other.downstream)
|
||||
}
|
||||
|
||||
compareLexicographic(other: ExtendedVersion): 'greater' | 'equal' | 'less' {
|
||||
if ((this.flavor || '') > (other.flavor || '')) {
|
||||
return 'greater'
|
||||
} else if ((this.flavor || '') > (other.flavor || '')) {
|
||||
return 'less'
|
||||
compareLexicographic(other: ExtendedVersion): "greater" | "equal" | "less" {
|
||||
if ((this.flavor || "") > (other.flavor || "")) {
|
||||
return "greater"
|
||||
} else if ((this.flavor || "") > (other.flavor || "")) {
|
||||
return "less"
|
||||
} else {
|
||||
return this.compare(other)!
|
||||
}
|
||||
@@ -841,37 +841,37 @@ export class ExtendedVersion {
|
||||
|
||||
compareForSort(other: ExtendedVersion): 1 | 0 | -1 {
|
||||
switch (this.compareLexicographic(other)) {
|
||||
case 'greater':
|
||||
case "greater":
|
||||
return 1
|
||||
case 'equal':
|
||||
case "equal":
|
||||
return 0
|
||||
case 'less':
|
||||
case "less":
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
greaterThan(other: ExtendedVersion): boolean {
|
||||
return this.compare(other) === 'greater'
|
||||
return this.compare(other) === "greater"
|
||||
}
|
||||
|
||||
greaterThanOrEqual(other: ExtendedVersion): boolean {
|
||||
return ['greater', 'equal'].includes(this.compare(other) as string)
|
||||
return ["greater", "equal"].includes(this.compare(other) as string)
|
||||
}
|
||||
|
||||
equals(other: ExtendedVersion): boolean {
|
||||
return this.compare(other) === 'equal'
|
||||
return this.compare(other) === "equal"
|
||||
}
|
||||
|
||||
lessThan(other: ExtendedVersion): boolean {
|
||||
return this.compare(other) === 'less'
|
||||
return this.compare(other) === "less"
|
||||
}
|
||||
|
||||
lessThanOrEqual(other: ExtendedVersion): boolean {
|
||||
return ['less', 'equal'].includes(this.compare(other) as string)
|
||||
return ["less", "equal"].includes(this.compare(other) as string)
|
||||
}
|
||||
|
||||
static parse(extendedVersion: string): ExtendedVersion {
|
||||
const parsed = P.parse(extendedVersion, { startRule: 'ExtendedVersion' })
|
||||
const parsed = P.parse(extendedVersion, { startRule: "ExtendedVersion" })
|
||||
return new ExtendedVersion(
|
||||
parsed.flavor || null,
|
||||
new Version(parsed.upstream.number, parsed.upstream.prerelease),
|
||||
@@ -881,7 +881,7 @@ export class ExtendedVersion {
|
||||
|
||||
static parseEmver(extendedVersion: string): ExtendedVersion {
|
||||
try {
|
||||
const parsed = P.parse(extendedVersion, { startRule: 'Emver' })
|
||||
const parsed = P.parse(extendedVersion, { startRule: "Emver" })
|
||||
return new ExtendedVersion(
|
||||
parsed.flavor || null,
|
||||
new Version(parsed.upstream.number, parsed.upstream.prerelease),
|
||||
@@ -956,22 +956,22 @@ export class ExtendedVersion {
|
||||
*/
|
||||
satisfies(versionRange: VersionRange): boolean {
|
||||
switch (versionRange.atom.type) {
|
||||
case 'Anchor':
|
||||
case "Anchor":
|
||||
const otherVersion = versionRange.atom.version
|
||||
switch (versionRange.atom.operator) {
|
||||
case '=':
|
||||
case "=":
|
||||
return this.equals(otherVersion)
|
||||
case '>':
|
||||
case ">":
|
||||
return this.greaterThan(otherVersion)
|
||||
case '<':
|
||||
case "<":
|
||||
return this.lessThan(otherVersion)
|
||||
case '>=':
|
||||
case ">=":
|
||||
return this.greaterThanOrEqual(otherVersion)
|
||||
case '<=':
|
||||
case "<=":
|
||||
return this.lessThanOrEqual(otherVersion)
|
||||
case '!=':
|
||||
case "!=":
|
||||
return !this.equals(otherVersion)
|
||||
case '^':
|
||||
case "^":
|
||||
const nextMajor = versionRange.atom.version.incrementMajor()
|
||||
if (
|
||||
this.greaterThanOrEqual(otherVersion) &&
|
||||
@@ -981,7 +981,7 @@ export class ExtendedVersion {
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case '~':
|
||||
case "~":
|
||||
const nextMinor = versionRange.atom.version.incrementMinor()
|
||||
if (
|
||||
this.greaterThanOrEqual(otherVersion) &&
|
||||
@@ -992,23 +992,23 @@ export class ExtendedVersion {
|
||||
return false
|
||||
}
|
||||
}
|
||||
case 'Flavor':
|
||||
case "Flavor":
|
||||
return versionRange.atom.flavor == this.flavor
|
||||
case 'And':
|
||||
case "And":
|
||||
return (
|
||||
this.satisfies(versionRange.atom.left) &&
|
||||
this.satisfies(versionRange.atom.right)
|
||||
)
|
||||
case 'Or':
|
||||
case "Or":
|
||||
return (
|
||||
this.satisfies(versionRange.atom.left) ||
|
||||
this.satisfies(versionRange.atom.right)
|
||||
)
|
||||
case 'Not':
|
||||
case "Not":
|
||||
return !this.satisfies(versionRange.atom.value)
|
||||
case 'Any':
|
||||
case "Any":
|
||||
return true
|
||||
case 'None':
|
||||
case "None":
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1020,34 +1020,34 @@ export const testTypeVersion = <T extends string>(t: T & ValidateVersion<T>) =>
|
||||
t
|
||||
|
||||
function tests() {
|
||||
testTypeVersion('1.2.3')
|
||||
testTypeVersion('1')
|
||||
testTypeVersion('12.34.56')
|
||||
testTypeVersion('1.2-3')
|
||||
testTypeVersion('1-3')
|
||||
testTypeVersion('1-alpha')
|
||||
testTypeVersion("1.2.3")
|
||||
testTypeVersion("1")
|
||||
testTypeVersion("12.34.56")
|
||||
testTypeVersion("1.2-3")
|
||||
testTypeVersion("1-3")
|
||||
testTypeVersion("1-alpha")
|
||||
// @ts-expect-error
|
||||
testTypeVersion('-3')
|
||||
testTypeVersion("-3")
|
||||
// @ts-expect-error
|
||||
testTypeVersion('1.2.3:1')
|
||||
testTypeVersion("1.2.3:1")
|
||||
// @ts-expect-error
|
||||
testTypeVersion('#cat:1:1')
|
||||
testTypeVersion("#cat:1:1")
|
||||
|
||||
testTypeExVer('1.2.3:1.2.3')
|
||||
testTypeExVer('1.2.3.4.5.6.7.8.9.0:1')
|
||||
testTypeExVer('100:1')
|
||||
testTypeExVer('#cat:1:1')
|
||||
testTypeExVer('1.2.3.4.5.6.7.8.9.11.22.33:1')
|
||||
testTypeExVer('1-0:1')
|
||||
testTypeExVer('1-0:1')
|
||||
testTypeExVer("1.2.3:1.2.3")
|
||||
testTypeExVer("1.2.3.4.5.6.7.8.9.0:1")
|
||||
testTypeExVer("100:1")
|
||||
testTypeExVer("#cat:1:1")
|
||||
testTypeExVer("1.2.3.4.5.6.7.8.9.11.22.33:1")
|
||||
testTypeExVer("1-0:1")
|
||||
testTypeExVer("1-0:1")
|
||||
// @ts-expect-error
|
||||
testTypeExVer('1.2-3')
|
||||
testTypeExVer("1.2-3")
|
||||
// @ts-expect-error
|
||||
testTypeExVer('1-3')
|
||||
testTypeExVer("1-3")
|
||||
// @ts-expect-error
|
||||
testTypeExVer('1.2.3.4.5.6.7.8.9.0.10:1' as string)
|
||||
testTypeExVer("1.2.3.4.5.6.7.8.9.0.10:1" as string)
|
||||
// @ts-expect-error
|
||||
testTypeExVer('1.-2:1')
|
||||
testTypeExVer("1.-2:1")
|
||||
// @ts-expect-error
|
||||
testTypeExVer('1..2.3:3')
|
||||
testTypeExVer("1..2.3:3")
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
export { S9pk } from './s9pk'
|
||||
export { VersionRange, ExtendedVersion, Version } from './exver'
|
||||
export { S9pk } from "./s9pk"
|
||||
export { VersionRange, ExtendedVersion, Version } from "./exver"
|
||||
|
||||
export * as inputSpec from './actions/input'
|
||||
export * as ISB from './actions/input/builder'
|
||||
export * as IST from './actions/input/inputSpecTypes'
|
||||
export * as types from './types'
|
||||
export * as T from './types'
|
||||
export * as yaml from 'yaml'
|
||||
export * as inits from './inits'
|
||||
export * as matches from 'ts-matches'
|
||||
export * as inputSpec from "./actions/input"
|
||||
export * as ISB from "./actions/input/builder"
|
||||
export * as IST from "./actions/input/inputSpecTypes"
|
||||
export * as types from "./types"
|
||||
export * as T from "./types"
|
||||
export * as yaml from "yaml"
|
||||
export * as inits from "./inits"
|
||||
export * as matches from "ts-matches"
|
||||
|
||||
export * as utils from './util'
|
||||
export * as utils from "./util"
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './setupInit'
|
||||
export * from './setupUninit'
|
||||
export * from "./setupInit"
|
||||
export * from "./setupUninit"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { VersionRange } from '../../../base/lib/exver'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { once } from '../util'
|
||||
import { VersionRange } from "../../../base/lib/exver"
|
||||
import * as T from "../../../base/lib/types"
|
||||
import { once } from "../util"
|
||||
|
||||
export type InitKind = 'install' | 'update' | 'restore' | null
|
||||
export type InitKind = "install" | "update" | "restore" | null
|
||||
|
||||
export type InitFn<Kind extends InitKind = InitKind> = (
|
||||
effects: T.Effects,
|
||||
@@ -31,7 +31,7 @@ export function setupInit(...inits: InitScriptOrFn[]): T.ExpectedExports.init {
|
||||
complete.then(() => fn()).catch(console.error),
|
||||
)
|
||||
try {
|
||||
if ('init' in init) await init.init(e, opts.kind)
|
||||
if ("init" in init) await init.init(e, opts.kind)
|
||||
else await init(e, opts.kind)
|
||||
} finally {
|
||||
res()
|
||||
@@ -43,7 +43,7 @@ export function setupInit(...inits: InitScriptOrFn[]): T.ExpectedExports.init {
|
||||
}
|
||||
|
||||
export function setupOnInit(onInit: InitScriptOrFn): InitScript {
|
||||
return 'init' in onInit
|
||||
return "init" in onInit
|
||||
? onInit
|
||||
: {
|
||||
init: async (effects, kind) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExtendedVersion, VersionRange } from '../../../base/lib/exver'
|
||||
import * as T from '../../../base/lib/types'
|
||||
import { ExtendedVersion, VersionRange } from "../../../base/lib/exver"
|
||||
import * as T from "../../../base/lib/types"
|
||||
|
||||
export type UninitFn = (
|
||||
effects: T.Effects,
|
||||
@@ -34,14 +34,14 @@ export function setupUninit(
|
||||
): T.ExpectedExports.uninit {
|
||||
return async (opts) => {
|
||||
for (const uninit of uninits) {
|
||||
if ('uninit' in uninit) await uninit.uninit(opts.effects, opts.target)
|
||||
if ("uninit" in uninit) await uninit.uninit(opts.effects, opts.target)
|
||||
else await uninit(opts.effects, opts.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setupOnUninit(onUninit: UninitScriptOrFn): UninitScript {
|
||||
return 'uninit' in onUninit
|
||||
return "uninit" in onUninit
|
||||
? onUninit
|
||||
: {
|
||||
uninit: async (effects, target) => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { object, string } from 'ts-matches'
|
||||
import { Effects } from '../Effects'
|
||||
import { Origin } from './Origin'
|
||||
import { AddSslOptions, BindParams } from '../osBindings'
|
||||
import { Security } from '../osBindings'
|
||||
import { BindOptions } from '../osBindings'
|
||||
import { AlpnInfo } from '../osBindings'
|
||||
import { object, string } from "ts-matches"
|
||||
import { Effects } from "../Effects"
|
||||
import { Origin } from "./Origin"
|
||||
import { AddSslOptions, BindParams } from "../osBindings"
|
||||
import { Security } from "../osBindings"
|
||||
import { BindOptions } from "../osBindings"
|
||||
import { AlpnInfo } from "../osBindings"
|
||||
|
||||
export { AddSslOptions, Security, BindOptions }
|
||||
|
||||
@@ -12,8 +12,8 @@ export const knownProtocols = {
|
||||
http: {
|
||||
secure: null,
|
||||
defaultPort: 80,
|
||||
withSsl: 'https',
|
||||
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
||||
withSsl: "https",
|
||||
alpn: { specified: ["http/1.1"] } as AlpnInfo,
|
||||
},
|
||||
https: {
|
||||
secure: { ssl: true },
|
||||
@@ -22,8 +22,8 @@ export const knownProtocols = {
|
||||
ws: {
|
||||
secure: null,
|
||||
defaultPort: 80,
|
||||
withSsl: 'wss',
|
||||
alpn: { specified: ['http/1.1'] } as AlpnInfo,
|
||||
withSsl: "wss",
|
||||
alpn: { specified: ["http/1.1"] } as AlpnInfo,
|
||||
},
|
||||
wss: {
|
||||
secure: { ssl: true },
|
||||
@@ -140,8 +140,8 @@ export class MultiHost {
|
||||
addXForwardedHeaders: false,
|
||||
preferredExternalPort: knownProtocols[sslProto].defaultPort,
|
||||
scheme: sslProto,
|
||||
alpn: 'alpn' in protoInfo ? protoInfo.alpn : null,
|
||||
...('addSsl' in options ? options.addSsl : null),
|
||||
alpn: "alpn" in protoInfo ? protoInfo.alpn : null,
|
||||
...("addSsl" in options ? options.addSsl : null),
|
||||
}
|
||||
: options.addSsl
|
||||
? {
|
||||
@@ -149,7 +149,7 @@ export class MultiHost {
|
||||
preferredExternalPort: 443,
|
||||
scheme: sslProto,
|
||||
alpn: null,
|
||||
...('addSsl' in options ? options.addSsl : null),
|
||||
...("addSsl" in options ? options.addSsl : null),
|
||||
}
|
||||
: null
|
||||
|
||||
@@ -169,8 +169,8 @@ export class MultiHost {
|
||||
private getSslProto(options: BindOptionsByKnownProtocol) {
|
||||
const proto = options.protocol
|
||||
const protoInfo = knownProtocols[proto]
|
||||
if (inObject('noAddSsl', options) && options.noAddSsl) return null
|
||||
if ('withSsl' in protoInfo && protoInfo.withSsl) return protoInfo.withSsl
|
||||
if (inObject("noAddSsl", options) && options.noAddSsl) return null
|
||||
if ("withSsl" in protoInfo && protoInfo.withSsl) return protoInfo.withSsl
|
||||
if (protoInfo.secure?.ssl) return proto
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AddressInfo } from '../types'
|
||||
import { AddressReceipt } from './AddressReceipt'
|
||||
import { MultiHost, Scheme } from './Host'
|
||||
import { ServiceInterfaceBuilder } from './ServiceInterfaceBuilder'
|
||||
import { AddressInfo } from "../types"
|
||||
import { AddressReceipt } from "./AddressReceipt"
|
||||
import { MultiHost, Scheme } from "./Host"
|
||||
import { ServiceInterfaceBuilder } from "./ServiceInterfaceBuilder"
|
||||
|
||||
export class Origin {
|
||||
constructor(
|
||||
@@ -21,9 +21,9 @@ export class Origin {
|
||||
.map(
|
||||
([key, val]) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`,
|
||||
)
|
||||
.join('&')
|
||||
.join("&")
|
||||
|
||||
const qp = qpEntries.length ? `?${qpEntries}` : ''
|
||||
const qp = qpEntries.length ? `?${qpEntries}` : ""
|
||||
|
||||
return {
|
||||
hostId: this.host.options.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ServiceInterfaceType } from '../types'
|
||||
import { Effects } from '../Effects'
|
||||
import { Scheme } from './Host'
|
||||
import { ServiceInterfaceType } from "../types"
|
||||
import { Effects } from "../Effects"
|
||||
import { Scheme } from "./Host"
|
||||
|
||||
/**
|
||||
* A helper class for creating a Network Interface
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as T from '../types'
|
||||
import { once } from '../util'
|
||||
import { AddressReceipt } from './AddressReceipt'
|
||||
import * as T from "../types"
|
||||
import { once } from "../util"
|
||||
import { AddressReceipt } from "./AddressReceipt"
|
||||
|
||||
declare const UpdateServiceInterfacesProof: unique symbol
|
||||
export type UpdateServiceInterfacesReceipt = {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AnyVerifyingKey } from './AnyVerifyingKey'
|
||||
import type { AnyVerifyingKey } from "./AnyVerifyingKey"
|
||||
|
||||
export type AcceptSigners =
|
||||
| { signer: AnyVerifyingKey }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user