procler 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- procler/__init__.py +3 -0
- procler/__main__.py +6 -0
- procler/api/__init__.py +5 -0
- procler/api/app.py +261 -0
- procler/api/deps.py +21 -0
- procler/api/routes/__init__.py +5 -0
- procler/api/routes/config.py +290 -0
- procler/api/routes/groups.py +62 -0
- procler/api/routes/logs.py +43 -0
- procler/api/routes/processes.py +185 -0
- procler/api/routes/recipes.py +69 -0
- procler/api/routes/snippets.py +134 -0
- procler/api/routes/ws.py +459 -0
- procler/cli.py +1478 -0
- procler/config/__init__.py +65 -0
- procler/config/changelog.py +148 -0
- procler/config/loader.py +256 -0
- procler/config/schema.py +315 -0
- procler/core/__init__.py +54 -0
- procler/core/context_base.py +117 -0
- procler/core/context_docker.py +384 -0
- procler/core/context_local.py +287 -0
- procler/core/daemon_detector.py +325 -0
- procler/core/events.py +74 -0
- procler/core/groups.py +419 -0
- procler/core/health.py +280 -0
- procler/core/log_tailer.py +262 -0
- procler/core/process_manager.py +1277 -0
- procler/core/recipes.py +330 -0
- procler/core/snippets.py +231 -0
- procler/core/variable_substitution.py +65 -0
- procler/db.py +96 -0
- procler/logging.py +41 -0
- procler/models.py +130 -0
- procler/py.typed +0 -0
- procler/settings.py +29 -0
- procler/static/assets/AboutView-BwZnsfpW.js +4 -0
- procler/static/assets/AboutView-UHbxWXcS.css +1 -0
- procler/static/assets/Code-HTS-H1S6.js +74 -0
- procler/static/assets/ConfigView-CGJcmp9G.css +1 -0
- procler/static/assets/ConfigView-aVtbRDf8.js +1 -0
- procler/static/assets/DashboardView-C5jw9Nsd.css +1 -0
- procler/static/assets/DashboardView-Dab7Cu9v.js +1 -0
- procler/static/assets/DataTable-z39TOAa4.js +746 -0
- procler/static/assets/DescriptionsItem-B2E8YbqJ.js +74 -0
- procler/static/assets/Divider-Dk-6aD2Y.js +42 -0
- procler/static/assets/Empty-MuygEHZM.js +24 -0
- procler/static/assets/Grid-CZ9QVKAT.js +1 -0
- procler/static/assets/GroupsView-BALG7i1X.js +1 -0
- procler/static/assets/GroupsView-gXAI1CVC.css +1 -0
- procler/static/assets/Input-e0xaxoWE.js +259 -0
- procler/static/assets/PhArrowsClockwise.vue-DqDg31az.js +1 -0
- procler/static/assets/PhCheckCircle.vue-Fwj9sh9m.js +1 -0
- procler/static/assets/PhEye.vue-JcPHciC2.js +1 -0
- procler/static/assets/PhPlay.vue-CZm7Gy3u.js +1 -0
- procler/static/assets/PhPlus.vue-yTWqKlSh.js +1 -0
- procler/static/assets/PhStop.vue-DxsqwIki.js +1 -0
- procler/static/assets/PhTrash.vue-DcqQbN1_.js +125 -0
- procler/static/assets/PhXCircle.vue-BXWmrabV.js +1 -0
- procler/static/assets/ProcessDetailView-DDbtIWq9.css +1 -0
- procler/static/assets/ProcessDetailView-DPtdNV-q.js +1 -0
- procler/static/assets/ProcessesView-B3a6Umur.js +1 -0
- procler/static/assets/ProcessesView-goLmghbJ.css +1 -0
- procler/static/assets/RecipesView-D2VxdneD.js +166 -0
- procler/static/assets/RecipesView-DXnFDCK4.css +1 -0
- procler/static/assets/Select-BBR17AHq.js +317 -0
- procler/static/assets/SnippetsView-B3a9q3AI.css +1 -0
- procler/static/assets/SnippetsView-DBCB2yGq.js +1 -0
- procler/static/assets/Spin-BXTjvFUk.js +90 -0
- procler/static/assets/Tag-Bh_qV63A.js +71 -0
- procler/static/assets/changelog-KkTT4H9-.js +1 -0
- procler/static/assets/groups-Zu-_v8ey.js +1 -0
- procler/static/assets/index-BsN-YMXq.css +1 -0
- procler/static/assets/index-BzW1XhyH.js +1282 -0
- procler/static/assets/procler-DOrSB1Vj.js +1 -0
- procler/static/assets/recipes-1w5SseGb.js +1 -0
- procler/static/index.html +17 -0
- procler/static/procler.png +0 -0
- procler-0.2.0.dist-info/METADATA +545 -0
- procler-0.2.0.dist-info/RECORD +83 -0
- procler-0.2.0.dist-info/WHEEL +4 -0
- procler-0.2.0.dist-info/entry_points.txt +2 -0
- procler-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Configuration management for procler."""
|
|
2
|
+
|
|
3
|
+
from .changelog import ChangelogAction, append_changelog, read_changelog
|
|
4
|
+
from .loader import (
|
|
5
|
+
find_config_dir,
|
|
6
|
+
generate_template_config,
|
|
7
|
+
get_changelog_path,
|
|
8
|
+
get_config,
|
|
9
|
+
get_config_file_path,
|
|
10
|
+
get_state_db_path,
|
|
11
|
+
load_config,
|
|
12
|
+
reload_config,
|
|
13
|
+
)
|
|
14
|
+
from .schema import (
|
|
15
|
+
ContextType,
|
|
16
|
+
DependencyCondition,
|
|
17
|
+
DependencyDef,
|
|
18
|
+
GroupDef,
|
|
19
|
+
HealthCheckDef,
|
|
20
|
+
OnErrorAction,
|
|
21
|
+
ProcessDef,
|
|
22
|
+
ProclerConfig,
|
|
23
|
+
RecipeDef,
|
|
24
|
+
RecipeStep,
|
|
25
|
+
RecipeStepExec,
|
|
26
|
+
RecipeStepGroupStart,
|
|
27
|
+
RecipeStepGroupStop,
|
|
28
|
+
RecipeStepRestart,
|
|
29
|
+
RecipeStepStart,
|
|
30
|
+
RecipeStepStop,
|
|
31
|
+
RecipeStepWait,
|
|
32
|
+
SnippetDef,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"find_config_dir",
|
|
37
|
+
"load_config",
|
|
38
|
+
"get_config",
|
|
39
|
+
"reload_config",
|
|
40
|
+
"generate_template_config",
|
|
41
|
+
"get_config_file_path",
|
|
42
|
+
"get_changelog_path",
|
|
43
|
+
"get_state_db_path",
|
|
44
|
+
"ProclerConfig",
|
|
45
|
+
"ProcessDef",
|
|
46
|
+
"GroupDef",
|
|
47
|
+
"RecipeDef",
|
|
48
|
+
"RecipeStep",
|
|
49
|
+
"SnippetDef",
|
|
50
|
+
"OnErrorAction",
|
|
51
|
+
"ContextType",
|
|
52
|
+
"RecipeStepStart",
|
|
53
|
+
"RecipeStepStop",
|
|
54
|
+
"RecipeStepRestart",
|
|
55
|
+
"RecipeStepGroupStart",
|
|
56
|
+
"RecipeStepGroupStop",
|
|
57
|
+
"RecipeStepWait",
|
|
58
|
+
"RecipeStepExec",
|
|
59
|
+
"HealthCheckDef",
|
|
60
|
+
"DependencyCondition",
|
|
61
|
+
"DependencyDef",
|
|
62
|
+
"append_changelog",
|
|
63
|
+
"read_changelog",
|
|
64
|
+
"ChangelogAction",
|
|
65
|
+
]
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Append-only changelog for tracking config changes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .loader import get_changelog_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChangelogAction(str, Enum):
|
|
15
|
+
"""Types of changelog actions."""
|
|
16
|
+
|
|
17
|
+
CREATE = "CREATE"
|
|
18
|
+
UPDATE = "UPDATE"
|
|
19
|
+
DELETE = "DELETE"
|
|
20
|
+
EXECUTE = "EXECUTE" # For recipe executions
|
|
21
|
+
START = "START" # Process started
|
|
22
|
+
STOP = "STOP" # Process stopped
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def append_changelog(
|
|
26
|
+
action: ChangelogAction,
|
|
27
|
+
entity_type: str,
|
|
28
|
+
entity_name: str,
|
|
29
|
+
details: dict[str, Any] | None = None,
|
|
30
|
+
changelog_path: Path | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Append an entry to the changelog.
|
|
34
|
+
|
|
35
|
+
Format: [ISO8601] ACTION type:name {json_details}
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
action: The action performed (CREATE, UPDATE, DELETE, EXECUTE, START, STOP)
|
|
39
|
+
entity_type: Type of entity (process, group, recipe, snippet)
|
|
40
|
+
entity_name: Name of the entity
|
|
41
|
+
details: Optional dict of additional details
|
|
42
|
+
changelog_path: Override path for testing
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
if changelog_path is None:
|
|
46
|
+
changelog_path = get_changelog_path()
|
|
47
|
+
except (ValueError, FileNotFoundError):
|
|
48
|
+
# No config directory found - skip logging silently
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
# Ensure directory exists
|
|
52
|
+
changelog_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
|
|
54
|
+
# Create header if file doesn't exist
|
|
55
|
+
if not changelog_path.exists():
|
|
56
|
+
header = (
|
|
57
|
+
"# Procler Changelog - AUTO-GENERATED\n"
|
|
58
|
+
"# DO NOT EDIT MANUALLY\n"
|
|
59
|
+
"# Format: [ISO8601] ACTION type:name {json_details}\n"
|
|
60
|
+
"#\n"
|
|
61
|
+
)
|
|
62
|
+
changelog_path.write_text(header)
|
|
63
|
+
|
|
64
|
+
# Build log entry
|
|
65
|
+
timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
66
|
+
details_json = json.dumps(details or {}, separators=(",", ":"))
|
|
67
|
+
entry = f"[{timestamp}] {action.value} {entity_type}:{entity_name} {details_json}\n"
|
|
68
|
+
|
|
69
|
+
# Append to file
|
|
70
|
+
with open(changelog_path, "a") as f:
|
|
71
|
+
f.write(entry)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def read_changelog(changelog_path: Path | None = None) -> list[dict[str, Any]]:
|
|
75
|
+
"""
|
|
76
|
+
Read and parse the changelog.
|
|
77
|
+
|
|
78
|
+
Returns list of entries:
|
|
79
|
+
[
|
|
80
|
+
{
|
|
81
|
+
"timestamp": "2024-01-08T12:00:00Z",
|
|
82
|
+
"action": "CREATE",
|
|
83
|
+
"entity_type": "process",
|
|
84
|
+
"entity_name": "api",
|
|
85
|
+
"details": {...}
|
|
86
|
+
},
|
|
87
|
+
...
|
|
88
|
+
]
|
|
89
|
+
"""
|
|
90
|
+
if changelog_path is None:
|
|
91
|
+
changelog_path = get_changelog_path()
|
|
92
|
+
|
|
93
|
+
if not changelog_path.exists():
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
entries = []
|
|
97
|
+
for line in changelog_path.read_text().splitlines():
|
|
98
|
+
line = line.strip()
|
|
99
|
+
# Skip comments and empty lines
|
|
100
|
+
if not line or line.startswith("#"):
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
# Parse: [TIMESTAMP] ACTION type:name {json}
|
|
105
|
+
# Find the timestamp
|
|
106
|
+
ts_end = line.index("]")
|
|
107
|
+
timestamp = line[1:ts_end]
|
|
108
|
+
|
|
109
|
+
# Find the action
|
|
110
|
+
rest = line[ts_end + 2 :] # Skip "] "
|
|
111
|
+
parts = rest.split(" ", 2)
|
|
112
|
+
if len(parts) < 2:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
action = parts[0]
|
|
116
|
+
entity = parts[1]
|
|
117
|
+
details_str = parts[2] if len(parts) > 2 else "{}"
|
|
118
|
+
|
|
119
|
+
# Parse entity type:name
|
|
120
|
+
entity_type, _, entity_name = entity.partition(":")
|
|
121
|
+
|
|
122
|
+
# Parse details JSON
|
|
123
|
+
details = json.loads(details_str)
|
|
124
|
+
|
|
125
|
+
entries.append(
|
|
126
|
+
{
|
|
127
|
+
"timestamp": timestamp,
|
|
128
|
+
"action": action,
|
|
129
|
+
"entity_type": entity_type,
|
|
130
|
+
"entity_name": entity_name,
|
|
131
|
+
"details": details,
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
except (ValueError, json.JSONDecodeError):
|
|
135
|
+
# Skip malformed lines
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
return entries
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_entity_history(
|
|
142
|
+
entity_type: str,
|
|
143
|
+
entity_name: str,
|
|
144
|
+
changelog_path: Path | None = None,
|
|
145
|
+
) -> list[dict[str, Any]]:
|
|
146
|
+
"""Get changelog entries for a specific entity."""
|
|
147
|
+
entries = read_changelog(changelog_path)
|
|
148
|
+
return [e for e in entries if e["entity_type"] == entity_type and e["entity_name"] == entity_name]
|
procler/config/loader.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Config file discovery and loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from .schema import ProclerConfig
|
|
12
|
+
|
|
13
|
+
# Singleton config cache
|
|
14
|
+
_config_cache: ProclerConfig | None = None
|
|
15
|
+
_config_dir_cache: Path | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def find_git_root() -> Path | None:
|
|
19
|
+
"""Find the root of the git repository, if any."""
|
|
20
|
+
try:
|
|
21
|
+
result = subprocess.run(
|
|
22
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
23
|
+
capture_output=True,
|
|
24
|
+
text=True,
|
|
25
|
+
timeout=5,
|
|
26
|
+
)
|
|
27
|
+
if result.returncode == 0:
|
|
28
|
+
return Path(result.stdout.strip())
|
|
29
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
30
|
+
pass
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_env_file(path: Path) -> dict[str, str]:
|
|
35
|
+
"""Parse a simple .env file (KEY=value format)."""
|
|
36
|
+
env = {}
|
|
37
|
+
if path.exists():
|
|
38
|
+
for line in path.read_text().splitlines():
|
|
39
|
+
line = line.strip()
|
|
40
|
+
if line and not line.startswith("#") and "=" in line:
|
|
41
|
+
key, _, value = line.partition("=")
|
|
42
|
+
# Strip quotes if present
|
|
43
|
+
value = value.strip().strip('"').strip("'")
|
|
44
|
+
env[key.strip()] = value
|
|
45
|
+
return env
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_config_dir(start_dir: Path | None = None) -> Path:
|
|
49
|
+
"""
|
|
50
|
+
Find the procler config directory using this discovery chain:
|
|
51
|
+
|
|
52
|
+
1. PROCLER_CONFIG_DIR environment variable
|
|
53
|
+
2. .procler.env file in current dir or parents (sets PROCLER_CONFIG_DIR)
|
|
54
|
+
3. .procler/ directory in current dir
|
|
55
|
+
4. .procler/ directory in git root
|
|
56
|
+
5. ~/.procler/ (global fallback)
|
|
57
|
+
"""
|
|
58
|
+
global _config_dir_cache
|
|
59
|
+
if _config_dir_cache is not None:
|
|
60
|
+
return _config_dir_cache
|
|
61
|
+
|
|
62
|
+
start = start_dir or Path.cwd()
|
|
63
|
+
|
|
64
|
+
# 1. Explicit environment variable
|
|
65
|
+
if env_dir := os.environ.get("PROCLER_CONFIG_DIR"):
|
|
66
|
+
_config_dir_cache = Path(env_dir).expanduser().resolve()
|
|
67
|
+
return _config_dir_cache
|
|
68
|
+
|
|
69
|
+
# 2. Search for .procler.env in current dir and parents
|
|
70
|
+
for parent in [start] + list(start.parents):
|
|
71
|
+
env_file = parent / ".procler.env"
|
|
72
|
+
if env_file.exists():
|
|
73
|
+
env = parse_env_file(env_file)
|
|
74
|
+
if config_dir := env.get("PROCLER_CONFIG_DIR"):
|
|
75
|
+
# Resolve relative to the .procler.env location
|
|
76
|
+
resolved = (parent / config_dir).resolve()
|
|
77
|
+
_config_dir_cache = resolved
|
|
78
|
+
return _config_dir_cache
|
|
79
|
+
|
|
80
|
+
# 3. .procler/ in current directory
|
|
81
|
+
local_config = start / ".procler"
|
|
82
|
+
if local_config.exists() and local_config.is_dir():
|
|
83
|
+
_config_dir_cache = local_config
|
|
84
|
+
return _config_dir_cache
|
|
85
|
+
|
|
86
|
+
# 4. .procler/ in git root
|
|
87
|
+
git_root = find_git_root()
|
|
88
|
+
if git_root:
|
|
89
|
+
git_config = git_root / ".procler"
|
|
90
|
+
if git_config.exists() and git_config.is_dir():
|
|
91
|
+
_config_dir_cache = git_config
|
|
92
|
+
return _config_dir_cache
|
|
93
|
+
|
|
94
|
+
# 5. Global fallback
|
|
95
|
+
_config_dir_cache = Path.home() / ".procler"
|
|
96
|
+
return _config_dir_cache
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def get_config_file_path() -> Path:
|
|
100
|
+
"""Get the path to the config.yaml file."""
|
|
101
|
+
return find_config_dir() / "config.yaml"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_changelog_path() -> Path:
|
|
105
|
+
"""Get the path to the changelog.log file."""
|
|
106
|
+
return find_config_dir() / "changelog.log"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_state_db_path() -> Path:
|
|
110
|
+
"""Get the path to the state.db file."""
|
|
111
|
+
return find_config_dir() / "state.db"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_config(config_path: Path | None = None) -> ProclerConfig:
|
|
115
|
+
"""
|
|
116
|
+
Load configuration from YAML file.
|
|
117
|
+
|
|
118
|
+
Returns an empty config if no config file exists.
|
|
119
|
+
"""
|
|
120
|
+
global _config_cache
|
|
121
|
+
|
|
122
|
+
if config_path is None:
|
|
123
|
+
config_path = get_config_file_path()
|
|
124
|
+
|
|
125
|
+
if not config_path.exists():
|
|
126
|
+
# Return empty config - processes/groups/recipes can be empty
|
|
127
|
+
return ProclerConfig()
|
|
128
|
+
|
|
129
|
+
with open(config_path) as f:
|
|
130
|
+
data = yaml.safe_load(f) or {}
|
|
131
|
+
|
|
132
|
+
config = ProclerConfig.model_validate(data)
|
|
133
|
+
|
|
134
|
+
# Warn about suspicious patterns in vars (security check)
|
|
135
|
+
if config.vars:
|
|
136
|
+
from ..core.variable_substitution import warn_suspicious_vars
|
|
137
|
+
|
|
138
|
+
warn_suspicious_vars(config.vars)
|
|
139
|
+
|
|
140
|
+
_config_cache = config
|
|
141
|
+
return config
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_config() -> ProclerConfig:
|
|
145
|
+
"""Get the cached config, loading if necessary."""
|
|
146
|
+
global _config_cache
|
|
147
|
+
if _config_cache is None:
|
|
148
|
+
_config_cache = load_config()
|
|
149
|
+
return _config_cache
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def reload_config() -> ProclerConfig:
|
|
153
|
+
"""Force reload of config from disk."""
|
|
154
|
+
global _config_cache
|
|
155
|
+
_config_cache = None
|
|
156
|
+
return load_config()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def save_config(config: ProclerConfig, config_path: Path | None = None) -> None:
|
|
160
|
+
"""Save configuration to YAML file."""
|
|
161
|
+
if config_path is None:
|
|
162
|
+
config_path = get_config_file_path()
|
|
163
|
+
|
|
164
|
+
# Ensure directory exists
|
|
165
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
# Convert to dict, excluding defaults for cleaner output
|
|
168
|
+
data = config.model_dump(exclude_defaults=True, exclude_none=True)
|
|
169
|
+
|
|
170
|
+
# Always include version
|
|
171
|
+
data["version"] = config.version
|
|
172
|
+
|
|
173
|
+
with open(config_path, "w") as f:
|
|
174
|
+
yaml.dump(data, f, default_flow_style=False, sort_keys=False)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def reset_config_cache() -> None:
|
|
178
|
+
"""Reset the config cache (useful for testing)."""
|
|
179
|
+
global _config_cache, _config_dir_cache
|
|
180
|
+
_config_cache = None
|
|
181
|
+
_config_dir_cache = None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def generate_template_config() -> str:
|
|
185
|
+
"""Generate a template config.yaml with examples."""
|
|
186
|
+
return """\
|
|
187
|
+
# Procler Configuration - LLM-First Process Manager
|
|
188
|
+
# https://github.com/gabu-quest/procler
|
|
189
|
+
#
|
|
190
|
+
# This file is VERSION CONTROLLED - commit it to your repo!
|
|
191
|
+
# Each project can have its own .procler/ directory.
|
|
192
|
+
#
|
|
193
|
+
# Files in .procler/:
|
|
194
|
+
# config.yaml - This file (commit to git)
|
|
195
|
+
# changelog.log - Audit trail of operations (commit to git)
|
|
196
|
+
# state.db - Runtime state (auto-gitignored)
|
|
197
|
+
#
|
|
198
|
+
# Discovery order: $PROCLER_CONFIG_DIR > .procler.env > .procler/ > git root > ~/.procler/
|
|
199
|
+
#
|
|
200
|
+
# CLI: `procler config explain` shows what this config does in plain language
|
|
201
|
+
# API: GET /api/config/explain returns the same as JSON
|
|
202
|
+
version: 1
|
|
203
|
+
|
|
204
|
+
# Process definitions - things that run continuously
|
|
205
|
+
processes:
|
|
206
|
+
# Example local process
|
|
207
|
+
# api:
|
|
208
|
+
# command: uvicorn main:app --reload
|
|
209
|
+
# context: local
|
|
210
|
+
# cwd: /path/to/project
|
|
211
|
+
# tags: [backend, api]
|
|
212
|
+
# description: "FastAPI development server"
|
|
213
|
+
#
|
|
214
|
+
# Example docker process
|
|
215
|
+
# worker:
|
|
216
|
+
# command: celery -A app worker
|
|
217
|
+
# context: docker
|
|
218
|
+
# container: my-container
|
|
219
|
+
# description: "Background task worker"
|
|
220
|
+
|
|
221
|
+
# Process groups - ordered start/stop
|
|
222
|
+
# Use: `procler group start backend` / `procler group stop backend`
|
|
223
|
+
groups:
|
|
224
|
+
# Example group
|
|
225
|
+
# backend:
|
|
226
|
+
# description: "Full backend stack"
|
|
227
|
+
# processes: [redis, api, worker] # Start in this order
|
|
228
|
+
# # stop_order defaults to reverse of processes
|
|
229
|
+
# # stop_order: [worker, api, redis] # Custom stop order
|
|
230
|
+
|
|
231
|
+
# Recipes - multi-step operations (like makefiles for processes)
|
|
232
|
+
# Use: `procler recipe run deploy --dry-run` to preview
|
|
233
|
+
recipes:
|
|
234
|
+
# Example recipe
|
|
235
|
+
# deploy:
|
|
236
|
+
# description: "Graceful deployment with migration"
|
|
237
|
+
# on_error: stop # or "continue"
|
|
238
|
+
# steps:
|
|
239
|
+
# - stop: worker
|
|
240
|
+
# - stop: api
|
|
241
|
+
# - wait: 2s
|
|
242
|
+
# - exec: "alembic upgrade head"
|
|
243
|
+
# context: docker
|
|
244
|
+
# container: my-container
|
|
245
|
+
# - start: api
|
|
246
|
+
# - start: worker
|
|
247
|
+
|
|
248
|
+
# Snippets - reusable one-off commands
|
|
249
|
+
# Use: `procler snippet run rebuild`
|
|
250
|
+
snippets:
|
|
251
|
+
# Example snippet
|
|
252
|
+
# rebuild:
|
|
253
|
+
# command: docker compose build
|
|
254
|
+
# description: "Rebuild all containers"
|
|
255
|
+
# tags: [docker, build]
|
|
256
|
+
"""
|