Externalize auth secrets to .env and rename default config to triggershell.yml
sessionSecret was previously baked directly into the scaffolded config file;
`triggershell init` now generates a .env with TRIGGERSHELL_SESSION_SECRET
instead and references it via ${VAR} interpolation, keeping the actual
secret out of the (often committed) config file. `triggershell dev/start/
validate` load that .env automatically without overriding real env vars.
Also renames the default config filename from triggershell.config.yaml to
triggershell.yml throughout the CLI, app, docs, and examples.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+6
-1
@@ -13,8 +13,13 @@ venv/
|
|||||||
# Runtime data (created by `triggershell start/dev` in whatever directory the config lives in)
|
# Runtime data (created by `triggershell start/dev` in whatever directory the config lives in)
|
||||||
.triggershell/
|
.triggershell/
|
||||||
|
|
||||||
|
# Secrets loaded by `triggershell` via ${VAR} interpolation - never commit these
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# Local config files a developer might create while testing against this repo
|
# Local config files a developer might create while testing against this repo
|
||||||
/triggershell.config.yaml
|
/triggershell.yml
|
||||||
|
|
||||||
# Editors / OS
|
# Editors / OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ API for automation.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install triggershell # or: pip install -e . from a checkout
|
pip install triggershell # or: pip install -e . from a checkout
|
||||||
triggershell init # scaffold triggershell.config.yaml in the current directory
|
triggershell init # scaffold triggershell.yml (+ .env for secrets) in the current directory
|
||||||
triggershell users add admin # create a login (skip if you set auth.enabled: false)
|
triggershell users add admin # create a login (skip if you set auth.enabled: false)
|
||||||
triggershell dev # start in dev mode and open the browser
|
triggershell dev # start in dev mode and open the browser
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit `triggershell.config.yaml` to add your own scripts (see [Configuration](#configuration) below),
|
Edit `triggershell.yml` to add your own scripts (see [Configuration](#configuration) below),
|
||||||
then run `triggershell start` for a production build.
|
then run `triggershell start` for a production build.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@@ -41,8 +41,10 @@ then run `triggershell start` for a production build.
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
TriggerShell is driven entirely by one YAML file (default: `./triggershell.config.yaml`, override
|
TriggerShell is driven entirely by one YAML file (default: `./triggershell.yml`, override with
|
||||||
with `-c/--config` or `TRIGGERSHELL_CONFIG_PATH`). Minimal example:
|
`-c/--config` or `TRIGGERSHELL_CONFIG_PATH`). Secrets referenced via `${VAR}` (like
|
||||||
|
`auth.sessionSecret`) are meant to live in a `.env` file next to the config, not in the config
|
||||||
|
itself — `triggershell init` generates both. Minimal example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
server:
|
server:
|
||||||
@@ -84,8 +86,9 @@ scripts:
|
|||||||
argName: --dry-run
|
argName: --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
A full, richly-commented example lives at [`examples/triggershell.config.yaml`](examples/triggershell.config.yaml),
|
A full, richly-commented example lives at [`examples/triggershell.yml`](examples/triggershell.yml)
|
||||||
and the complete field-by-field reference is in [`docs/CONFIG_REFERENCE.md`](docs/CONFIG_REFERENCE.md).
|
(with a matching [`.env.example`](examples/.env.example)), and the complete field-by-field
|
||||||
|
reference is in [`docs/CONFIG_REFERENCE.md`](docs/CONFIG_REFERENCE.md).
|
||||||
|
|
||||||
Each variable's `type` (`string` / `number` / `boolean` / `enum` / `multiselect`) picks a sensible
|
Each variable's `type` (`string` / `number` / `boolean` / `enum` / `multiselect`) picks a sensible
|
||||||
default UI control; set `control` explicitly to override it (e.g. `slider` for a `number`, `radio`
|
default UI control; set `control` explicitly to override it (e.g. `slider` for a `number`, `radio`
|
||||||
@@ -96,7 +99,7 @@ the script — always as a discrete argv element or env var, never interpolated
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `triggershell init [PATH]` | Scaffold a new config file (`--port`, `--auth/--no-auth`, `--force`) |
|
| `triggershell init [PATH]` | Scaffold a new config file + `.env` (`--port`, `--auth/--no-auth`, `--force`) |
|
||||||
| `triggershell validate [-c CONFIG]` | Validate a config file (fast Python pre-flight + full Node/Zod schema) |
|
| `triggershell validate [-c CONFIG]` | Validate a config file (fast Python pre-flight + full Node/Zod schema) |
|
||||||
| `triggershell dev [-c CONFIG] [--port] [--host] [--no-browser]` | Run in development mode (hot reload) |
|
| `triggershell dev [-c CONFIG] [--port] [--host] [--no-browser]` | Run in development mode (hot reload) |
|
||||||
| `triggershell start [-c CONFIG] [--port] [--host] [--no-browser] [--skip-build]` | Build (if stale) and run in production mode |
|
| `triggershell start [-c CONFIG] [--port] [--host] [--no-browser] [--skip-build]` | Build (if stale) and run in production mode |
|
||||||
@@ -173,6 +176,9 @@ See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for how the pieces fit togeth
|
|||||||
- The server binds to `127.0.0.1` by default — set `server.host` explicitly to expose it further.
|
- The server binds to `127.0.0.1` by default — set `server.host` explicitly to expose it further.
|
||||||
- Route Handlers check auth themselves (`requireAuth()`); `proxy.ts` is only a fast, optimistic
|
- Route Handlers check auth themselves (`requireAuth()`); `proxy.ts` is only a fast, optimistic
|
||||||
redirect layer, not the security boundary.
|
redirect layer, not the security boundary.
|
||||||
|
- `auth.sessionSecret` should live in `.env`, not the config file — `triggershell init` sets this
|
||||||
|
up for you. Only `passwordHash`/`tokenHash` values (one-way hashes, not the secrets themselves)
|
||||||
|
belong in the config.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function resolveConfigPath(configPathArg?: string): string {
|
|||||||
const candidate =
|
const candidate =
|
||||||
configPathArg ??
|
configPathArg ??
|
||||||
process.env.TRIGGERSHELL_CONFIG_PATH ??
|
process.env.TRIGGERSHELL_CONFIG_PATH ??
|
||||||
"triggershell.config.yaml";
|
"triggershell.yml";
|
||||||
return path.resolve(/*turbopackIgnore: true*/ candidate);
|
return path.resolve(/*turbopackIgnore: true*/ candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
# Configuration Reference
|
# Configuration Reference
|
||||||
|
|
||||||
The config file is YAML, resolved from (in order): `-c/--config`, `TRIGGERSHELL_CONFIG_PATH`, or
|
The config file is YAML, resolved from (in order): `-c/--config`, `TRIGGERSHELL_CONFIG_PATH`, or
|
||||||
`./triggershell.config.yaml`. Values support `${VAR}` / `${VAR:-default}` interpolation against
|
`./triggershell.yml`. Values support `${VAR}` / `${VAR:-default}` interpolation, evaluated before
|
||||||
the CLI's environment, evaluated before YAML parsing. `database.path` and `logs.dir` are resolved
|
YAML parsing. `database.path` and `logs.dir` are resolved relative to the config file's own
|
||||||
relative to the config file's own directory, not the current working directory.
|
directory, not the current working directory.
|
||||||
|
|
||||||
|
Before interpolation, the CLI loads a `.env` file from the same directory as the config file (if
|
||||||
|
present) into its environment - without overriding any variable already set in the shell - so
|
||||||
|
secrets referenced via `${VAR}` don't have to be committed alongside the config. `triggershell
|
||||||
|
init` scaffolds both files together.
|
||||||
|
|
||||||
The canonical schema is the Zod schema at `app/src/lib/config/schema.ts` — this document mirrors
|
The canonical schema is the Zod schema at `app/src/lib/config/schema.ts` — this document mirrors
|
||||||
it. `triggershell validate` runs the Python pre-flight checks below, then that full schema.
|
it. `triggershell validate` runs the Python pre-flight checks below, then that full schema.
|
||||||
@@ -21,7 +26,7 @@ it. `triggershell validate` runs the Python pre-flight checks below, then that f
|
|||||||
| Field | Type | Default | Notes |
|
| Field | Type | Default | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `enabled` | boolean | `true` | `false` disables login entirely |
|
| `enabled` | boolean | `true` | `false` disables login entirely |
|
||||||
| `sessionSecret` | string | — | Required, >= 32 chars, if `enabled` |
|
| `sessionSecret` | string | — | Required, >= 32 chars, if `enabled`. Reference it via `${TRIGGERSHELL_SESSION_SECRET}` and set the real value in `.env`, not here |
|
||||||
| `sessionTtlHours` | number | `12` | Session cookie lifetime |
|
| `sessionTtlHours` | number | `12` | Session cookie lifetime |
|
||||||
| `users` | array | `[]` | `{username, passwordHash}` — hash via `triggershell users add` |
|
| `users` | array | `[]` | `{username, passwordHash}` — hash via `triggershell users add` |
|
||||||
| `tokens` | array | `[]` | `{name, tokenHash}` — hash via `triggershell users add-token` |
|
| `tokens` | array | `[]` | `{name, tokenHash}` — hash via `triggershell users add-token` |
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Copy this file to `.env` (same directory as triggershell.yml) and set a real value.
|
||||||
|
# TriggerShell loads .env automatically from the config file's directory and makes its
|
||||||
|
# variables available to ${VAR} interpolation in the config - this is how secrets like
|
||||||
|
# sessionSecret should be kept out of the config file (and out of version control).
|
||||||
|
TRIGGERSHELL_SESSION_SECRET=dev-only-insecure-session-secret-change-me-before-deploying
|
||||||
@@ -8,7 +8,8 @@ server:
|
|||||||
auth:
|
auth:
|
||||||
# Set to false for trusted/local-only use - no login required.
|
# Set to false for trusted/local-only use - no login required.
|
||||||
enabled: true
|
enabled: true
|
||||||
# Must be >= 32 characters. Generate one with `triggershell init` or `openssl rand -hex 32`.
|
# Must be >= 32 characters. Loaded from .env (see .env.example) - the fallback after `:-` only
|
||||||
|
# exists so this example runs with zero setup; don't rely on it for anything real.
|
||||||
sessionSecret: "${TRIGGERSHELL_SESSION_SECRET:-dev-only-insecure-session-secret-change-me-before-deploying}"
|
sessionSecret: "${TRIGGERSHELL_SESSION_SECRET:-dev-only-insecure-session-secret-change-me-before-deploying}"
|
||||||
sessionTtlHours: 12
|
sessionTtlHours: 12
|
||||||
users:
|
users:
|
||||||
+7
-1
@@ -10,7 +10,13 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = [{ name = "TriggerShell contributors" }]
|
authors = [{ name = "TriggerShell contributors" }]
|
||||||
dependencies = ["typer>=0.12", "rich>=13.7", "pyyaml>=6.0", "argon2-cffi>=23.1"]
|
dependencies = [
|
||||||
|
"typer>=0.12",
|
||||||
|
"rich>=13.7",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"argon2-cffi>=23.1",
|
||||||
|
"python-dotenv>=1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
triggershell = "triggershell.cli:app"
|
triggershell = "triggershell.cli:app"
|
||||||
|
|||||||
+44
-3
@@ -7,6 +7,7 @@ from triggershell.bootstrap import (
|
|||||||
BootstrapError,
|
BootstrapError,
|
||||||
check_node,
|
check_node,
|
||||||
ensure_dependencies_installed,
|
ensure_dependencies_installed,
|
||||||
|
load_dotenv_for_config,
|
||||||
needs_build,
|
needs_build,
|
||||||
record_build_stamp,
|
record_build_stamp,
|
||||||
)
|
)
|
||||||
@@ -73,7 +74,7 @@ def test_ensure_dependencies_installed_runs_when_stale(tmp_path: Path) -> None:
|
|||||||
def test_needs_build_true_when_no_stamp(tmp_path: Path) -> None:
|
def test_needs_build_true_when_no_stamp(tmp_path: Path) -> None:
|
||||||
app_dir = tmp_path / "app"
|
app_dir = tmp_path / "app"
|
||||||
(app_dir / "src").mkdir(parents=True)
|
(app_dir / "src").mkdir(parents=True)
|
||||||
config_path = tmp_path / "triggershell.config.yaml"
|
config_path = tmp_path / "triggershell.yml"
|
||||||
config_path.write_text("scripts: []")
|
config_path.write_text("scripts: []")
|
||||||
|
|
||||||
assert needs_build(app_dir, config_path) is True
|
assert needs_build(app_dir, config_path) is True
|
||||||
@@ -82,7 +83,7 @@ def test_needs_build_true_when_no_stamp(tmp_path: Path) -> None:
|
|||||||
def test_needs_build_false_after_stamp_recorded(tmp_path: Path) -> None:
|
def test_needs_build_false_after_stamp_recorded(tmp_path: Path) -> None:
|
||||||
app_dir = tmp_path / "app"
|
app_dir = tmp_path / "app"
|
||||||
(app_dir / "src").mkdir(parents=True)
|
(app_dir / "src").mkdir(parents=True)
|
||||||
config_path = tmp_path / "triggershell.config.yaml"
|
config_path = tmp_path / "triggershell.yml"
|
||||||
config_path.write_text("scripts: []")
|
config_path.write_text("scripts: []")
|
||||||
|
|
||||||
record_build_stamp(app_dir, config_path)
|
record_build_stamp(app_dir, config_path)
|
||||||
@@ -95,7 +96,7 @@ def test_needs_build_true_after_config_touched(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
app_dir = tmp_path / "app"
|
app_dir = tmp_path / "app"
|
||||||
(app_dir / "src").mkdir(parents=True)
|
(app_dir / "src").mkdir(parents=True)
|
||||||
config_path = tmp_path / "triggershell.config.yaml"
|
config_path = tmp_path / "triggershell.yml"
|
||||||
config_path.write_text("scripts: []")
|
config_path.write_text("scripts: []")
|
||||||
|
|
||||||
record_build_stamp(app_dir, config_path)
|
record_build_stamp(app_dir, config_path)
|
||||||
@@ -107,3 +108,43 @@ def test_needs_build_true_after_config_touched(tmp_path: Path) -> None:
|
|||||||
os.utime(config_path, (future, future))
|
os.utime(config_path, (future, future))
|
||||||
|
|
||||||
assert needs_build(app_dir, config_path) is True
|
assert needs_build(app_dir, config_path) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_dotenv_for_config_noop_when_missing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
import os
|
||||||
|
|
||||||
|
monkeypatch.delenv("TRIGGERSHELL_SESSION_SECRET", raising=False)
|
||||||
|
config_path = tmp_path / "triggershell.yml"
|
||||||
|
config_path.write_text("scripts: []")
|
||||||
|
|
||||||
|
load_dotenv_for_config(config_path)
|
||||||
|
|
||||||
|
assert "TRIGGERSHELL_SESSION_SECRET" not in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_dotenv_for_config_loads_values(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
import os
|
||||||
|
|
||||||
|
monkeypatch.delenv("TRIGGERSHELL_SESSION_SECRET", raising=False)
|
||||||
|
config_path = tmp_path / "triggershell.yml"
|
||||||
|
config_path.write_text("scripts: []")
|
||||||
|
(tmp_path / ".env").write_text("TRIGGERSHELL_SESSION_SECRET=from-dotenv-file\n")
|
||||||
|
|
||||||
|
load_dotenv_for_config(config_path)
|
||||||
|
|
||||||
|
assert os.environ["TRIGGERSHELL_SESSION_SECRET"] == "from-dotenv-file"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_dotenv_for_config_does_not_override_existing_env(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setenv("TRIGGERSHELL_SESSION_SECRET", "from-shell")
|
||||||
|
config_path = tmp_path / "triggershell.yml"
|
||||||
|
config_path.write_text("scripts: []")
|
||||||
|
(tmp_path / ".env").write_text("TRIGGERSHELL_SESSION_SECRET=from-dotenv-file\n")
|
||||||
|
|
||||||
|
load_dotenv_for_config(config_path)
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
assert os.environ["TRIGGERSHELL_SESSION_SECRET"] == "from-shell"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from triggershell.config import ConfigError, preflight_check
|
|||||||
|
|
||||||
|
|
||||||
def write(tmp_path: Path, content: str) -> Path:
|
def write(tmp_path: Path, content: str) -> Path:
|
||||||
path = tmp_path / "triggershell.config.yaml"
|
path = tmp_path / "triggershell.yml"
|
||||||
path.write_text(content)
|
path.write_text(content)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dotenv import dotenv_values
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
@@ -17,6 +19,20 @@ class BootstrapError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def load_dotenv_for_config(config_path: Path) -> None:
|
||||||
|
"""Loads a `.env` file next to the config file into this process's environment, so secrets
|
||||||
|
referenced as `${VAR}` in the config (e.g. `auth.sessionSecret`) don't have to be committed to
|
||||||
|
it. Merged into `os.environ` before the Node side spawns and interpolates the config, and
|
||||||
|
never overrides a variable the shell already exported."""
|
||||||
|
env_path = config_path.parent / ".env"
|
||||||
|
if not env_path.is_file():
|
||||||
|
return
|
||||||
|
|
||||||
|
for key, value in dotenv_values(env_path).items():
|
||||||
|
if value is not None and key not in os.environ:
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
def find_app_dir() -> Path:
|
def find_app_dir() -> Path:
|
||||||
"""Locates the bundled Next.js app: `_app` next to this package in an installed wheel,
|
"""Locates the bundled Next.js app: `_app` next to this package in an installed wheel,
|
||||||
or `../app` when running from the repo checkout (editable install / development)."""
|
or `../app` when running from the repo checkout (editable install / development)."""
|
||||||
|
|||||||
+19
-10
@@ -19,6 +19,7 @@ from triggershell.bootstrap import (
|
|||||||
ensure_dependencies_installed,
|
ensure_dependencies_installed,
|
||||||
ensure_pnpm,
|
ensure_pnpm,
|
||||||
find_app_dir,
|
find_app_dir,
|
||||||
|
load_dotenv_for_config,
|
||||||
needs_build,
|
needs_build,
|
||||||
record_build_stamp,
|
record_build_stamp,
|
||||||
)
|
)
|
||||||
@@ -35,7 +36,7 @@ app = typer.Typer(
|
|||||||
users_app = typer.Typer(help="Manage auth users and API tokens defined in your config file.")
|
users_app = typer.Typer(help="Manage auth users and API tokens defined in your config file.")
|
||||||
app.add_typer(users_app, name="users")
|
app.add_typer(users_app, name="users")
|
||||||
|
|
||||||
DEFAULT_CONFIG_NAME = "triggershell.config.yaml"
|
DEFAULT_CONFIG_NAME = "triggershell.yml"
|
||||||
|
|
||||||
|
|
||||||
def _resolve_config_path(config: Optional[Path]) -> Path:
|
def _resolve_config_path(config: Optional[Path]) -> Path:
|
||||||
@@ -86,7 +87,7 @@ def init(
|
|||||||
auth: bool = typer.Option(True, help="Enable built-in login for the web app."),
|
auth: bool = typer.Option(True, help="Enable built-in login for the web app."),
|
||||||
force: bool = typer.Option(False, help="Overwrite an existing config file."),
|
force: bool = typer.Option(False, help="Overwrite an existing config file."),
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Scaffold a new `triggershell.config.yaml`."""
|
"""Scaffold a new `triggershell.yml`."""
|
||||||
target_dir = (path or Path.cwd()).resolve()
|
target_dir = (path or Path.cwd()).resolve()
|
||||||
target_dir.mkdir(parents=True, exist_ok=True)
|
target_dir.mkdir(parents=True, exist_ok=True)
|
||||||
config_path = target_dir / DEFAULT_CONFIG_NAME
|
config_path = target_dir / DEFAULT_CONFIG_NAME
|
||||||
@@ -97,19 +98,25 @@ def init(
|
|||||||
|
|
||||||
template_path = Path(__file__).parent / "templates" / DEFAULT_CONFIG_NAME
|
template_path = Path(__file__).parent / "templates" / DEFAULT_CONFIG_NAME
|
||||||
template = template_path.read_text(encoding="utf-8")
|
template = template_path.read_text(encoding="utf-8")
|
||||||
session_secret = secrets.token_hex(32)
|
|
||||||
|
|
||||||
rendered = (
|
rendered = template.replace("__PORT__", str(port)).replace("__AUTH_ENABLED__", "true" if auth else "false")
|
||||||
template.replace("__PORT__", str(port))
|
|
||||||
.replace("__AUTH_ENABLED__", "true" if auth else "false")
|
|
||||||
.replace("__SESSION_SECRET__", session_secret)
|
|
||||||
)
|
|
||||||
config_path.write_text(rendered, encoding="utf-8")
|
config_path.write_text(rendered, encoding="utf-8")
|
||||||
|
|
||||||
console.print(f"[green]Created[/green] {config_path}")
|
console.print(f"[green]Created[/green] {config_path}")
|
||||||
|
|
||||||
|
env_path = target_dir / ".env"
|
||||||
if auth:
|
if auth:
|
||||||
console.print("Auth is enabled but no users are configured yet. Add one with:")
|
if env_path.exists():
|
||||||
|
console.print(
|
||||||
|
f"[yellow]{env_path} already exists[/yellow] - make sure it sets TRIGGERSHELL_SESSION_SECRET "
|
||||||
|
"(>= 32 chars)."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
session_secret = secrets.token_hex(32)
|
||||||
|
env_path.write_text(f"TRIGGERSHELL_SESSION_SECRET={session_secret}\n", encoding="utf-8")
|
||||||
|
console.print(f"[green]Created[/green] {env_path} [dim](keep this out of version control)[/dim]")
|
||||||
|
console.print("\nAuth is enabled but no users are configured yet. Add one with:")
|
||||||
console.print(f" [bold]triggershell users add <username> --config {config_path}[/bold]")
|
console.print(f" [bold]triggershell users add <username> --config {config_path}[/bold]")
|
||||||
|
|
||||||
console.print("\nStart the app with:")
|
console.print("\nStart the app with:")
|
||||||
console.print(f" [bold]triggershell dev --config {config_path}[/bold]")
|
console.print(f" [bold]triggershell dev --config {config_path}[/bold]")
|
||||||
|
|
||||||
@@ -120,6 +127,7 @@ def validate(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Validate a config file: a fast Python pre-flight check, then the full Zod schema in Node."""
|
"""Validate a config file: a fast Python pre-flight check, then the full Zod schema in Node."""
|
||||||
config_path = _resolve_config_path(config)
|
config_path = _resolve_config_path(config)
|
||||||
|
load_dotenv_for_config(config_path)
|
||||||
_require_config(config_path)
|
_require_config(config_path)
|
||||||
console.print("[green]OK[/green] (pre-flight checks passed)")
|
console.print("[green]OK[/green] (pre-flight checks passed)")
|
||||||
|
|
||||||
@@ -164,6 +172,7 @@ def _run_server(
|
|||||||
skip_build: bool = False,
|
skip_build: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
config_path = _resolve_config_path(config)
|
config_path = _resolve_config_path(config)
|
||||||
|
load_dotenv_for_config(config_path)
|
||||||
config_data = _require_config(config_path)
|
config_data = _require_config(config_path)
|
||||||
|
|
||||||
effective_host = host or (config_data.get("server") or {}).get("host") or "127.0.0.1"
|
effective_host = host or (config_data.get("server") or {}).get("host") or "127.0.0.1"
|
||||||
|
|||||||
+3
-2
@@ -7,8 +7,9 @@ server:
|
|||||||
|
|
||||||
auth:
|
auth:
|
||||||
enabled: __AUTH_ENABLED__
|
enabled: __AUTH_ENABLED__
|
||||||
# Must be >= 32 characters if auth is enabled. Generated by `triggershell init`.
|
# Must be >= 32 characters if auth is enabled. Kept out of this file - `triggershell init`
|
||||||
sessionSecret: "__SESSION_SECRET__"
|
# generated one into .env (TRIGGERSHELL_SESSION_SECRET) alongside this config.
|
||||||
|
sessionSecret: "${TRIGGERSHELL_SESSION_SECRET}"
|
||||||
sessionTtlHours: 12
|
sessionTtlHours: 12
|
||||||
users: []
|
users: []
|
||||||
# Add a user with: triggershell users add <username>
|
# Add a user with: triggershell users add <username>
|
||||||
Reference in New Issue
Block a user