forktex-cloud 1.0.0__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/PKG-INFO +1 -1
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/pyproject.toml +1 -1
- forktex_cloud-2.0.0/src/forktex_cloud/__init__.py +148 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/bridge/local_compose.py +88 -52
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/client/client.py +13 -2
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/client/generated/__init__.py +15 -2
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/secrets/factory.py +6 -9
- forktex_cloud-1.0.0/src/forktex_cloud/__init__.py +0 -76
- forktex_cloud-1.0.0/src/forktex_cloud/paths.py +0 -274
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/LICENSE +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/README.md +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/bridge/__init__.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/bridge/log_formatter.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/bridge/loki.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/bridge/persistence_defaults.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/client/__init__.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/config.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/manifest/__init__.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/manifest/errors.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/manifest/loader.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/manifest/merge.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/manifest/schema.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/scaffold/__init__.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/scaffold/templates.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/secrets/__init__.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/secrets/base.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/secrets/fernet.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/secrets/resolver.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/templates/observability/loki.yml +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/templates/observability/promtail.yml +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/vpn/__init__.py +0 -0
- {forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/vpn/local.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "forktex-cloud"
|
|
3
|
-
version = "
|
|
3
|
+
version = "2.0.0"
|
|
4
4
|
description = "Typed Python SDK for the ForkTex Cloud platform — provision, deploy, and manage VPS-backed apps via a declarative manifest."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "ForkTex", email = "info@forktex.com"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""forktex_cloud — Standalone Python SDK for the ForkTex Cloud platform.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from forktex_cloud import Cloud
|
|
6
|
+
|
|
7
|
+
with Cloud("https://cloud.forktex.com", account_key="ftx-...") as cloud:
|
|
8
|
+
projects = cloud.list_projects()
|
|
9
|
+
servers = cloud.list_servers()
|
|
10
|
+
|
|
11
|
+
Or, when you already hold a ``CloudContext``::
|
|
12
|
+
|
|
13
|
+
from forktex_cloud import Cloud, CloudContext
|
|
14
|
+
|
|
15
|
+
ctx = CloudContext(controller="https://cloud.forktex.com", account_key="ftx-...")
|
|
16
|
+
with Cloud.from_context(ctx) as cloud:
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
Loading model — lazy
|
|
20
|
+
--------------------
|
|
21
|
+
|
|
22
|
+
The SDK is **filesystem-independent**: it deals in data (manifests, compose
|
|
23
|
+
dicts, cipher bytes), never in ``.forktex/`` paths — forktex-py owns the
|
|
24
|
+
on-disk layout. The HTTP client (``Cloud``), the auth/config types
|
|
25
|
+
(``CloudContext``), the manifest loader, and the 17 OpenAPI-codegen models are
|
|
26
|
+
loaded **lazily** via PEP 562 ``__getattr__`` on first access, keeping
|
|
27
|
+
``import forktex_cloud`` cheap. The public surface is unchanged:
|
|
28
|
+
``from forktex_cloud import Cloud`` still works.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
__version__ = "2.0.0"
|
|
36
|
+
|
|
37
|
+
# ── Lazy attribute map ──────────────────────────────────────────────────────
|
|
38
|
+
#
|
|
39
|
+
# Name → (submodule, attribute). Anything not listed raises AttributeError.
|
|
40
|
+
# Codegen models share one submodule, so first access pays once — subsequent
|
|
41
|
+
# model lookups hit the module-level cache populated by __getattr__.
|
|
42
|
+
_LAZY_ATTRS: dict[str, tuple[str, str]] = {
|
|
43
|
+
# Client (httpx-backed, heavy)
|
|
44
|
+
"Cloud": ("forktex_cloud.client.client", "Cloud"),
|
|
45
|
+
"CloudAPIError": ("forktex_cloud.client.client", "CloudAPIError"),
|
|
46
|
+
# Config
|
|
47
|
+
"CloudContext": ("forktex_cloud.config", "CloudContext"),
|
|
48
|
+
# Manifest
|
|
49
|
+
"Manifest": ("forktex_cloud.manifest.loader", "Manifest"),
|
|
50
|
+
"ManifestError": ("forktex_cloud.manifest.loader", "ManifestError"),
|
|
51
|
+
# Codegen contract + OpenAPI models (all from forktex_cloud.client.generated)
|
|
52
|
+
"SPEC_HASH": ("forktex_cloud.client.generated", "SPEC_HASH"),
|
|
53
|
+
"SPEC_VERSION": ("forktex_cloud.client.generated", "SPEC_VERSION"),
|
|
54
|
+
"ApiKeyCreated": ("forktex_cloud.client.generated", "ApiKeyCreated"),
|
|
55
|
+
"ApiKeyRead": ("forktex_cloud.client.generated", "ApiKeyRead"),
|
|
56
|
+
"AuditEventRead": ("forktex_cloud.client.generated", "AuditEventRead"),
|
|
57
|
+
"EnvironmentRead": ("forktex_cloud.client.generated", "EnvironmentRead"),
|
|
58
|
+
"HealthRead": ("forktex_cloud.client.generated", "HealthRead"),
|
|
59
|
+
"JobResponse": ("forktex_cloud.client.generated", "JobResponse"),
|
|
60
|
+
"MeResponse": ("forktex_cloud.client.generated", "MeResponse"),
|
|
61
|
+
"OrgRead": ("forktex_cloud.client.generated", "OrgRead"),
|
|
62
|
+
"ProjectRead": ("forktex_cloud.client.generated", "ProjectRead"),
|
|
63
|
+
"ServerRead": ("forktex_cloud.client.generated", "ServerRead"),
|
|
64
|
+
"StatusResponse": ("forktex_cloud.client.generated", "StatusResponse"),
|
|
65
|
+
"TokenResponse": ("forktex_cloud.client.generated", "TokenResponse"),
|
|
66
|
+
"UserRead": ("forktex_cloud.client.generated", "UserRead"),
|
|
67
|
+
"VaultGetResponse": ("forktex_cloud.client.generated", "VaultGetResponse"),
|
|
68
|
+
"WorkspaceRead": ("forktex_cloud.client.generated", "WorkspaceRead"),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def __getattr__(name: str) -> Any:
|
|
73
|
+
"""PEP 562 lazy attribute resolver — imports a submodule on first access.
|
|
74
|
+
|
|
75
|
+
Once resolved, the attribute is bound on the module so subsequent accesses
|
|
76
|
+
skip this function entirely (Python looks it up in ``globals()`` first).
|
|
77
|
+
"""
|
|
78
|
+
if name in _LAZY_ATTRS:
|
|
79
|
+
import importlib
|
|
80
|
+
|
|
81
|
+
module_path, attr_name = _LAZY_ATTRS[name]
|
|
82
|
+
value = getattr(importlib.import_module(module_path), attr_name)
|
|
83
|
+
globals()[name] = value
|
|
84
|
+
return value
|
|
85
|
+
raise AttributeError(f"module 'forktex_cloud' has no attribute {name!r}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def __dir__() -> list[str]:
|
|
89
|
+
"""Include lazy attributes in ``dir()`` so REPL completion still works."""
|
|
90
|
+
return sorted(set(__all__) | set(globals()))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# Static type-checker hint — declared as a TYPE_CHECKING block so it costs
|
|
94
|
+
# nothing at runtime but lets editors / mypy see the eventual symbol types.
|
|
95
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
96
|
+
from forktex_cloud.client.client import Cloud, CloudAPIError
|
|
97
|
+
from forktex_cloud.client.generated import (
|
|
98
|
+
SPEC_HASH,
|
|
99
|
+
SPEC_VERSION,
|
|
100
|
+
ApiKeyCreated,
|
|
101
|
+
ApiKeyRead,
|
|
102
|
+
AuditEventRead,
|
|
103
|
+
EnvironmentRead,
|
|
104
|
+
HealthRead,
|
|
105
|
+
JobResponse,
|
|
106
|
+
MeResponse,
|
|
107
|
+
OrgRead,
|
|
108
|
+
ProjectRead,
|
|
109
|
+
ServerRead,
|
|
110
|
+
StatusResponse,
|
|
111
|
+
TokenResponse,
|
|
112
|
+
UserRead,
|
|
113
|
+
VaultGetResponse,
|
|
114
|
+
WorkspaceRead,
|
|
115
|
+
)
|
|
116
|
+
from forktex_cloud.config import CloudContext
|
|
117
|
+
from forktex_cloud.manifest.loader import Manifest, ManifestError
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
# Codegen contract (wire-compatibility markers) — lazy
|
|
122
|
+
"SPEC_VERSION",
|
|
123
|
+
"SPEC_HASH",
|
|
124
|
+
# Client — lazy
|
|
125
|
+
"Cloud",
|
|
126
|
+
"CloudAPIError",
|
|
127
|
+
# Config — lazy
|
|
128
|
+
"CloudContext",
|
|
129
|
+
# Manifest — lazy
|
|
130
|
+
"Manifest",
|
|
131
|
+
"ManifestError",
|
|
132
|
+
# Models (from OpenAPI codegen — the single source of truth) — lazy
|
|
133
|
+
"ApiKeyCreated",
|
|
134
|
+
"ApiKeyRead",
|
|
135
|
+
"AuditEventRead",
|
|
136
|
+
"EnvironmentRead",
|
|
137
|
+
"HealthRead",
|
|
138
|
+
"JobResponse",
|
|
139
|
+
"MeResponse",
|
|
140
|
+
"OrgRead",
|
|
141
|
+
"ProjectRead",
|
|
142
|
+
"ServerRead",
|
|
143
|
+
"StatusResponse",
|
|
144
|
+
"TokenResponse",
|
|
145
|
+
"UserRead",
|
|
146
|
+
"VaultGetResponse",
|
|
147
|
+
"WorkspaceRead",
|
|
148
|
+
]
|
|
@@ -6,11 +6,9 @@ No proxy, no blue-green, no SSL -- just plain containers with direct port mappin
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import shutil
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Any
|
|
12
11
|
|
|
13
|
-
from forktex_cloud import paths
|
|
14
12
|
from forktex_cloud.bridge.persistence_defaults import detect_persistence_defaults
|
|
15
13
|
from forktex_cloud.manifest.loader import Manifest
|
|
16
14
|
from forktex_cloud.secrets.base import SecretsProvider
|
|
@@ -58,31 +56,37 @@ def _templates_dir() -> Path:
|
|
|
58
56
|
return Path(__file__).resolve().parent.parent / "templates"
|
|
59
57
|
|
|
60
58
|
|
|
61
|
-
def
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
def render_observability_configs() -> dict[str, str]:
|
|
60
|
+
"""Return the observability config files as ``{filename: content}``.
|
|
61
|
+
|
|
62
|
+
Pure: reads the bundled SDK templates and returns their text. The caller
|
|
63
|
+
(forktex-py) writes them next to the generated compose file. No ``.forktex/``
|
|
64
|
+
knowledge here — the SDK only produces data.
|
|
65
|
+
"""
|
|
66
|
+
out: dict[str, str] = {}
|
|
65
67
|
src_dir = _templates_dir() / "observability"
|
|
66
68
|
if src_dir.is_dir():
|
|
67
|
-
for src_file in src_dir.iterdir():
|
|
69
|
+
for src_file in sorted(src_dir.iterdir()):
|
|
68
70
|
if src_file.is_file():
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
+
out[src_file.name] = src_file.read_text(encoding="utf-8")
|
|
72
|
+
return out
|
|
71
73
|
|
|
72
74
|
|
|
73
75
|
def _add_observability_services(
|
|
74
76
|
services: dict[str, Any],
|
|
75
77
|
named_volumes: dict[str, Any],
|
|
76
|
-
project_root: Path,
|
|
77
78
|
) -> None:
|
|
78
|
-
"""Add Loki + Promtail services.
|
|
79
|
-
obs_dir = _write_observability_configs(project_root)
|
|
79
|
+
"""Add Loki + Promtail services.
|
|
80
80
|
|
|
81
|
+
The compose file and the ``observability/`` configs are written into the
|
|
82
|
+
same directory by the caller, so the bind mounts are siblings
|
|
83
|
+
(``./observability/...``).
|
|
84
|
+
"""
|
|
81
85
|
services["loki"] = {
|
|
82
86
|
"image": "grafana/loki:2.9.0",
|
|
83
87
|
"ports": ["3100:3100"],
|
|
84
88
|
"volumes": [
|
|
85
|
-
|
|
89
|
+
"./observability/loki.yml:/etc/loki/local-config.yaml:ro",
|
|
86
90
|
"loki-data:/loki",
|
|
87
91
|
],
|
|
88
92
|
"healthcheck": {
|
|
@@ -101,7 +105,7 @@ def _add_observability_services(
|
|
|
101
105
|
services["promtail"] = {
|
|
102
106
|
"image": "grafana/promtail:2.9.0",
|
|
103
107
|
"volumes": [
|
|
104
|
-
|
|
108
|
+
"./observability/promtail.yml:/etc/promtail/config.yml:ro",
|
|
105
109
|
"/var/run/docker.sock:/var/run/docker.sock:ro",
|
|
106
110
|
"/var/lib/docker/containers:/var/lib/docker/containers:ro",
|
|
107
111
|
],
|
|
@@ -118,8 +122,16 @@ def local_compose_from_manifest(
|
|
|
118
122
|
*,
|
|
119
123
|
secrets_provider: SecretsProvider | None = None,
|
|
120
124
|
observability: bool = True,
|
|
125
|
+
root_prefix: str = "../..",
|
|
121
126
|
) -> dict[str, Any]:
|
|
122
|
-
"""Build a docker-compose dict suitable for local development.
|
|
127
|
+
"""Build a docker-compose dict suitable for local development.
|
|
128
|
+
|
|
129
|
+
``root_prefix`` is the relative path from the compose file's directory up to
|
|
130
|
+
the project root. forktex-py writes the compose into ``.forktex/cache/``
|
|
131
|
+
(two levels under the root), so the default is ``../..``; build contexts and
|
|
132
|
+
source bind-mounts are emitted relative to it. (cache-sibling artefacts —
|
|
133
|
+
``./observability``, ``./data`` — are unaffected.)
|
|
134
|
+
"""
|
|
123
135
|
services: dict[str, Any] = {}
|
|
124
136
|
named_volumes: dict[str, Any] = {}
|
|
125
137
|
env_name = getattr(manifest, "env_name", None) or "default"
|
|
@@ -134,6 +146,35 @@ def local_compose_from_manifest(
|
|
|
134
146
|
if svc_def.get("type") == "persistence":
|
|
135
147
|
persistence_ids.append(svc_def["id"])
|
|
136
148
|
|
|
149
|
+
# Precompute which persistence services WILL get a healthcheck (declared or
|
|
150
|
+
# from image defaults), independent of generation order. The depends_on
|
|
151
|
+
# condition derivation below used to read the not-yet-generated persistence
|
|
152
|
+
# service dict, so services appearing before their DB in the manifest (api,
|
|
153
|
+
# client, ...) wrongly got ``service_started`` and raced the DB on startup.
|
|
154
|
+
persistence_has_healthcheck: dict[str, bool] = {}
|
|
155
|
+
for svc_def in local_services:
|
|
156
|
+
if svc_def.get("type") != "persistence":
|
|
157
|
+
continue
|
|
158
|
+
pid = svc_def["id"]
|
|
159
|
+
if svc_def.get("healthcheck"):
|
|
160
|
+
persistence_has_healthcheck[pid] = True
|
|
161
|
+
else:
|
|
162
|
+
defaults = detect_persistence_defaults(svc_def.get("image", ""))
|
|
163
|
+
persistence_has_healthcheck[pid] = bool(defaults and "healthcheck" in defaults)
|
|
164
|
+
|
|
165
|
+
# One-shot init services (`labels.forktex.oneshot=true`) — compute
|
|
166
|
+
# services get an auto-depends_on on each so they're at least ordered
|
|
167
|
+
# behind the initializer's start. We can't wait on `service_completed`
|
|
168
|
+
# for a true oneshot the way k8s init-containers do, but the
|
|
169
|
+
# ``service_started`` ordering closes the most common race (e.g.
|
|
170
|
+
# gitea-init writes the token file → api can read it on its first
|
|
171
|
+
# gitea call). The initializer itself self-polls its dependencies.
|
|
172
|
+
oneshot_ids: list[str] = [
|
|
173
|
+
svc_def["id"]
|
|
174
|
+
for svc_def in local_services
|
|
175
|
+
if (svc_def.get("labels") or {}).get("forktex.oneshot") == "true"
|
|
176
|
+
]
|
|
177
|
+
|
|
137
178
|
for svc_def in local_services:
|
|
138
179
|
sid = svc_def["id"]
|
|
139
180
|
image = svc_def.get("image", "")
|
|
@@ -150,22 +191,37 @@ def local_compose_from_manifest(
|
|
|
150
191
|
if build_cfg and isinstance(build_cfg, dict):
|
|
151
192
|
build_entry: dict[str, str] = {}
|
|
152
193
|
ctx = build_cfg.get("context", f"./{sid}")
|
|
153
|
-
# Rewrite relative context to be relative to
|
|
154
|
-
|
|
194
|
+
# Rewrite a project-relative context to be relative to the compose
|
|
195
|
+
# file's directory (.forktex/cache/) via root_prefix.
|
|
196
|
+
build_entry["context"] = (
|
|
197
|
+
f"{root_prefix}/{ctx.removeprefix('./')}" if ctx.startswith("./") else ctx
|
|
198
|
+
)
|
|
155
199
|
if build_cfg.get("dockerfile"):
|
|
156
200
|
build_entry["dockerfile"] = build_cfg["dockerfile"]
|
|
157
201
|
svc["build"] = build_entry
|
|
158
202
|
elif svc_type == "compute":
|
|
159
203
|
dockerfile = project_root / sid / "Dockerfile"
|
|
160
204
|
if dockerfile.is_file():
|
|
161
|
-
svc["build"] = {"context": f"
|
|
205
|
+
svc["build"] = {"context": f"{root_prefix}/{sid}"}
|
|
162
206
|
|
|
163
207
|
if sid in host_ports:
|
|
164
208
|
host_port = host_ports[sid]
|
|
165
209
|
svc["ports"] = [f"{host_port}:{port}"]
|
|
166
210
|
|
|
167
|
-
|
|
211
|
+
# One-shot init services (e.g. `gitea-init`) carry the
|
|
212
|
+
# `forktex.oneshot=true` label so the generator skips the implicit
|
|
213
|
+
# restart-always; they should exit cleanly after their seed step
|
|
214
|
+
# and stay exited. The label lives on the existing ServiceDef
|
|
215
|
+
# `labels` field — no schema/codegen change required.
|
|
216
|
+
is_oneshot = (svc_def.get("labels") or {}).get("forktex.oneshot") == "true"
|
|
217
|
+
if svc_type in ("persistence", "observability") and not is_oneshot:
|
|
168
218
|
svc["restart"] = "always"
|
|
219
|
+
if svc_def.get("labels"):
|
|
220
|
+
svc["labels"] = dict(svc_def["labels"])
|
|
221
|
+
# docker-compose `pid:` passthrough — `service:<id>` shares another
|
|
222
|
+
# service's PID namespace so e.g. a sidecar can signal its primary.
|
|
223
|
+
if svc_def.get("pid"):
|
|
224
|
+
svc["pid"] = svc_def["pid"]
|
|
169
225
|
|
|
170
226
|
if svc_type == "persistence":
|
|
171
227
|
defaults = detect_persistence_defaults(image)
|
|
@@ -196,7 +252,7 @@ def local_compose_from_manifest(
|
|
|
196
252
|
src = v.split(":")[0]
|
|
197
253
|
rest = v[len(src) :]
|
|
198
254
|
if src.startswith("./"):
|
|
199
|
-
rewritten.append(f"
|
|
255
|
+
rewritten.append(f"{root_prefix}/{src[2:]}{rest}")
|
|
200
256
|
elif src.startswith("/") or not src.startswith("."):
|
|
201
257
|
rewritten.append(v)
|
|
202
258
|
if not src.startswith("/"):
|
|
@@ -222,16 +278,19 @@ def local_compose_from_manifest(
|
|
|
222
278
|
if cmd:
|
|
223
279
|
svc["command"] = cmd
|
|
224
280
|
|
|
225
|
-
if svc_type == "compute"
|
|
281
|
+
if svc_type == "compute":
|
|
226
282
|
depends: dict[str, Any] = {}
|
|
227
283
|
for pid in persistence_ids:
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
284
|
+
healthy = persistence_has_healthcheck.get(pid)
|
|
285
|
+
cond = "service_healthy" if healthy else "service_started"
|
|
286
|
+
depends[pid] = {"condition": cond}
|
|
287
|
+
for iid in oneshot_ids:
|
|
288
|
+
# Initializers run side-by-side; we only need start-ordering,
|
|
289
|
+
# not completion. The initializer's contract is "write its
|
|
290
|
+
# output before the first compute request needs it."
|
|
291
|
+
depends[iid] = {"condition": "service_started"}
|
|
292
|
+
if depends:
|
|
293
|
+
svc["depends_on"] = depends
|
|
235
294
|
|
|
236
295
|
explicit_deps = svc_def.get("depends_on")
|
|
237
296
|
if explicit_deps:
|
|
@@ -241,7 +300,7 @@ def local_compose_from_manifest(
|
|
|
241
300
|
services[sid] = svc
|
|
242
301
|
|
|
243
302
|
if observability:
|
|
244
|
-
_add_observability_services(services, named_volumes
|
|
303
|
+
_add_observability_services(services, named_volumes)
|
|
245
304
|
|
|
246
305
|
compose: dict[str, Any] = {"services": services}
|
|
247
306
|
|
|
@@ -251,26 +310,3 @@ def local_compose_from_manifest(
|
|
|
251
310
|
compose["networks"] = {"forktex": {}}
|
|
252
311
|
|
|
253
312
|
return compose
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def write_local_compose(
|
|
257
|
-
manifest: Manifest,
|
|
258
|
-
project_root: Path,
|
|
259
|
-
*,
|
|
260
|
-
secrets_provider: SecretsProvider | None = None,
|
|
261
|
-
observability: bool = True,
|
|
262
|
-
) -> Path:
|
|
263
|
-
"""Generate the local docker-compose file for the project and return its path."""
|
|
264
|
-
import yaml
|
|
265
|
-
|
|
266
|
-
compose = local_compose_from_manifest(
|
|
267
|
-
manifest,
|
|
268
|
-
project_root,
|
|
269
|
-
secrets_provider=secrets_provider,
|
|
270
|
-
observability=observability,
|
|
271
|
-
)
|
|
272
|
-
paths.ensure_project_dirs(project_root)
|
|
273
|
-
out_path = paths.compose_path(project_root, "local")
|
|
274
|
-
with open(out_path, "w") as f:
|
|
275
|
-
yaml.dump(compose, f, default_flow_style=False, sort_keys=False)
|
|
276
|
-
return out_path
|
|
@@ -895,12 +895,23 @@ class Cloud:
|
|
|
895
895
|
svc_type = svc.get("type", "compute")
|
|
896
896
|
service_prefix = f"services/{svc_id}"
|
|
897
897
|
|
|
898
|
-
# Volume-mounted local files and directories
|
|
898
|
+
# Volume-mounted local files and directories.
|
|
899
|
+
#
|
|
900
|
+
# Only RELATIVE local paths (./foo, ../bar) are treated as
|
|
901
|
+
# tarball-worthy assets — they're project-local source dirs the
|
|
902
|
+
# deploy needs to ship.
|
|
903
|
+
#
|
|
904
|
+
# Absolute paths in `volumes:` (e.g. /media/megatyres/db) are
|
|
905
|
+
# by convention **server-side host paths** on the deployment
|
|
906
|
+
# target, NOT local asset directories. Tarballing them is
|
|
907
|
+
# always wrong (they may not exist locally; if they do, the
|
|
908
|
+
# permissions are whatever the local OS happens to assign,
|
|
909
|
+
# which has nothing to do with the deploy). Skip them.
|
|
899
910
|
for vol in svc.get("volumes", []):
|
|
900
911
|
if isinstance(vol, str) and ":" in vol:
|
|
901
912
|
parts = vol.split(":")
|
|
902
913
|
local_part = parts[0]
|
|
903
|
-
if local_part.startswith("./") or local_part.startswith("
|
|
914
|
+
if local_part.startswith("./") or local_part.startswith("../"):
|
|
904
915
|
local_path = (project_dir / local_part).resolve()
|
|
905
916
|
if local_path.is_dir() or local_path.is_file():
|
|
906
917
|
source_basename = Path(local_part).name
|
|
@@ -4,13 +4,13 @@ DO NOT EDIT — regenerate with: make codegen
|
|
|
4
4
|
|
|
5
5
|
Source: GET /api/openapi.json
|
|
6
6
|
API version: 0.6.0
|
|
7
|
-
Spec hash:
|
|
7
|
+
Spec hash: 8d93387d1a2e1ae4
|
|
8
8
|
"""
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
SPEC_VERSION = "0.6.0"
|
|
13
|
-
SPEC_HASH = "
|
|
13
|
+
SPEC_HASH = "8d93387d1a2e1ae4"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
from enum import StrEnum
|
|
@@ -656,6 +656,14 @@ class ManifestVersionRead(BaseModel):
|
|
|
656
656
|
version: Annotated[int, Field(title="Version")]
|
|
657
657
|
|
|
658
658
|
|
|
659
|
+
class MemberRead(BaseModel):
|
|
660
|
+
createdAt: Annotated[AwareDatetime, Field(title="Createdat")]
|
|
661
|
+
id: Annotated[UUID, Field(title="Id")]
|
|
662
|
+
orgId: Annotated[UUID, Field(title="Orgid")]
|
|
663
|
+
role: Annotated[str, Field(title="Role")]
|
|
664
|
+
userId: Annotated[UUID, Field(title="Userid")]
|
|
665
|
+
|
|
666
|
+
|
|
659
667
|
class MetadataDef(BaseModel):
|
|
660
668
|
model_config = ConfigDict(
|
|
661
669
|
extra="forbid",
|
|
@@ -741,6 +749,7 @@ class OpsResponse(BaseModel):
|
|
|
741
749
|
class OrgBrief(BaseModel):
|
|
742
750
|
id: Annotated[UUID, Field(title="Id")]
|
|
743
751
|
name: Annotated[str, Field(title="Name")]
|
|
752
|
+
role: Annotated[str | None, Field(title="Role")] = None
|
|
744
753
|
slug: Annotated[str, Field(title="Slug")]
|
|
745
754
|
|
|
746
755
|
|
|
@@ -1190,6 +1199,10 @@ class ServiceDef(BaseModel):
|
|
|
1190
1199
|
"""
|
|
1191
1200
|
Log format this service writes to stdout. 'json' enables structured field extraction in Loki (level, request_id). 'auto' (default when omitted) infers from the image name.
|
|
1192
1201
|
"""
|
|
1202
|
+
pid: Annotated[str | None, Field(title="Pid")] = None
|
|
1203
|
+
"""
|
|
1204
|
+
Docker-compose ``pid:`` passthrough. Common values: ``service:<other-service-id>`` to share another service's PID namespace (useful for sidecars that need to signal their primary, e.g. ``kill -HUP``), or ``host`` for full host PID namespace. When omitted, the container runs in its own PID namespace (compose default).
|
|
1205
|
+
"""
|
|
1193
1206
|
port: Annotated[int | None, Field(title="Port")] = None
|
|
1194
1207
|
resources: Annotated[dict[str, Any] | None, Field(title="Resources")] = None
|
|
1195
1208
|
routePrefix: Annotated[str | None, Field(title="Routeprefix")] = None
|
|
@@ -5,30 +5,27 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
|
|
8
|
-
from forktex_cloud import paths
|
|
9
8
|
from forktex_cloud.secrets.base import SecretsProvider
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def get_secrets_provider(
|
|
13
12
|
*,
|
|
14
|
-
|
|
13
|
+
vault_root: Path,
|
|
15
14
|
provider_name: str | None = None,
|
|
16
15
|
master_key: str | None = None,
|
|
17
16
|
) -> SecretsProvider:
|
|
18
17
|
"""Create and return the configured SecretsProvider.
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
``vault_root`` is supplied by the caller — forktex-py owns the ``.forktex/``
|
|
20
|
+
layout; the SDK never computes filesystem paths. Resolution order for the
|
|
21
|
+
provider name: explicit ``provider_name`` → ``FORKTEX_SECRETS_PROVIDER`` →
|
|
22
|
+
``"fernet"``.
|
|
24
23
|
"""
|
|
25
24
|
name = provider_name or os.environ.get("FORKTEX_SECRETS_PROVIDER", "fernet")
|
|
26
25
|
|
|
27
26
|
if name == "fernet":
|
|
28
27
|
from forktex_cloud.secrets.fernet import FernetVault
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
vault_root = paths.project_dir(root) / "vault"
|
|
32
|
-
return FernetVault(vault_root, master_key=master_key)
|
|
29
|
+
return FernetVault(Path(vault_root), master_key=master_key)
|
|
33
30
|
|
|
34
31
|
raise ValueError(f"Unknown secrets provider: {name}")
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""forktex_cloud — Standalone Python SDK for the ForkTex Cloud platform.
|
|
2
|
-
|
|
3
|
-
Usage::
|
|
4
|
-
|
|
5
|
-
from forktex_cloud import Cloud
|
|
6
|
-
|
|
7
|
-
with Cloud("https://cloud.forktex.com", account_key="ftx-...") as cloud:
|
|
8
|
-
projects = cloud.list_projects()
|
|
9
|
-
servers = cloud.list_servers()
|
|
10
|
-
|
|
11
|
-
Or, when you already hold a ``CloudContext``::
|
|
12
|
-
|
|
13
|
-
from forktex_cloud import Cloud, CloudContext
|
|
14
|
-
|
|
15
|
-
ctx = CloudContext(controller="https://cloud.forktex.com", account_key="ftx-...")
|
|
16
|
-
with Cloud.from_context(ctx) as cloud:
|
|
17
|
-
...
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
__version__ = "1.0.0"
|
|
21
|
-
|
|
22
|
-
from forktex_cloud import paths
|
|
23
|
-
from forktex_cloud.client.client import Cloud, CloudAPIError
|
|
24
|
-
from forktex_cloud.client.generated import (
|
|
25
|
-
SPEC_HASH,
|
|
26
|
-
SPEC_VERSION,
|
|
27
|
-
ApiKeyCreated,
|
|
28
|
-
ApiKeyRead,
|
|
29
|
-
AuditEventRead,
|
|
30
|
-
EnvironmentRead,
|
|
31
|
-
HealthRead,
|
|
32
|
-
JobResponse,
|
|
33
|
-
MeResponse,
|
|
34
|
-
OrgRead,
|
|
35
|
-
ProjectRead,
|
|
36
|
-
ServerRead,
|
|
37
|
-
StatusResponse,
|
|
38
|
-
TokenResponse,
|
|
39
|
-
UserRead,
|
|
40
|
-
VaultGetResponse,
|
|
41
|
-
WorkspaceRead,
|
|
42
|
-
)
|
|
43
|
-
from forktex_cloud.config import CloudContext
|
|
44
|
-
from forktex_cloud.manifest.loader import Manifest, ManifestError
|
|
45
|
-
|
|
46
|
-
__all__ = [
|
|
47
|
-
# Filesystem layout spec (V1)
|
|
48
|
-
"paths",
|
|
49
|
-
# Codegen contract (wire-compatibility markers)
|
|
50
|
-
"SPEC_VERSION",
|
|
51
|
-
"SPEC_HASH",
|
|
52
|
-
# Client
|
|
53
|
-
"Cloud",
|
|
54
|
-
"CloudAPIError",
|
|
55
|
-
# Config
|
|
56
|
-
"CloudContext",
|
|
57
|
-
# Manifest
|
|
58
|
-
"Manifest",
|
|
59
|
-
"ManifestError",
|
|
60
|
-
# Models (from OpenAPI codegen — the single source of truth)
|
|
61
|
-
"ApiKeyCreated",
|
|
62
|
-
"ApiKeyRead",
|
|
63
|
-
"AuditEventRead",
|
|
64
|
-
"EnvironmentRead",
|
|
65
|
-
"HealthRead",
|
|
66
|
-
"JobResponse",
|
|
67
|
-
"MeResponse",
|
|
68
|
-
"OrgRead",
|
|
69
|
-
"ProjectRead",
|
|
70
|
-
"ServerRead",
|
|
71
|
-
"StatusResponse",
|
|
72
|
-
"TokenResponse",
|
|
73
|
-
"UserRead",
|
|
74
|
-
"VaultGetResponse",
|
|
75
|
-
"WorkspaceRead",
|
|
76
|
-
]
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
"""Canonical filesystem layout for the forktex ecosystem (V1).
|
|
2
|
-
|
|
3
|
-
Every subsystem that reads or writes under ``.forktex/`` (project scope) or
|
|
4
|
-
``~/.forktex/`` (user/OS scope) MUST go through this module. Hardcoding path
|
|
5
|
-
literals elsewhere is enforced against by ``tests/test_paths_contract.py``.
|
|
6
|
-
|
|
7
|
-
Cross-platform rules:
|
|
8
|
-
- All returned values are ``pathlib.Path`` (never ``str``).
|
|
9
|
-
- Project scope directory is always lowercase ``.forktex``.
|
|
10
|
-
- User scope directory is ``~/.forktex`` on POSIX and ``%APPDATA%/forktex`` on Windows.
|
|
11
|
-
- Secrets-bearing directories are created with mode ``0o700`` on POSIX; Windows
|
|
12
|
-
relies on per-user ACLs on ``%APPDATA%``.
|
|
13
|
-
|
|
14
|
-
See ``cloud/docs/forktex-directory-spec.md`` for the full V1 spec.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
|
-
import os
|
|
20
|
-
import sys
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
#: Bump when the on-disk layout changes incompatibly. Written to
|
|
24
|
-
#: ``.forktex/.version`` by :func:`ensure_project_dirs` on init.
|
|
25
|
-
SCHEMA_VERSION = 1
|
|
26
|
-
|
|
27
|
-
#: Directory name at project scope.
|
|
28
|
-
PROJECT_DIRNAME = ".forktex"
|
|
29
|
-
|
|
30
|
-
#: Directory name at user/OS scope (stripped of leading dot on Windows).
|
|
31
|
-
_USER_DIRNAME_POSIX = ".forktex"
|
|
32
|
-
_USER_DIRNAME_WINDOWS = "forktex"
|
|
33
|
-
|
|
34
|
-
#: Marker comment used to idempotently detect the canonical ``.gitignore`` block.
|
|
35
|
-
_GITIGNORE_MARKER = "# forktex — generated + secrets (keep .forktex/.version committed)"
|
|
36
|
-
_GITIGNORE_BLOCK = f"""
|
|
37
|
-
{_GITIGNORE_MARKER}
|
|
38
|
-
.forktex/**
|
|
39
|
-
!.forktex/.version
|
|
40
|
-
"""
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# ── Project-scope paths ──────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def project_dir(root: Path) -> Path:
|
|
47
|
-
return root / PROJECT_DIRNAME
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def version_file(root: Path) -> Path:
|
|
51
|
-
return project_dir(root) / ".version"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def compose_path(root: Path, env: str) -> Path:
|
|
55
|
-
return project_dir(root) / f"docker-compose.{env}.yml"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def observability_dir(root: Path) -> Path:
|
|
59
|
-
return project_dir(root) / "observability"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def vault_dir(root: Path, env: str) -> Path:
|
|
63
|
-
return project_dir(root) / "vault" / env
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def vault_secrets_file(root: Path, env: str) -> Path:
|
|
67
|
-
return vault_dir(root, env) / "secrets.enc"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def state_dir(root: Path) -> Path:
|
|
71
|
-
return project_dir(root) / "state"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def servers_json(root: Path) -> Path:
|
|
75
|
-
return state_dir(root) / "servers.json"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def server_keys_dir(root: Path) -> Path:
|
|
79
|
-
return state_dir(root) / "keys"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def generated_dir(root: Path) -> Path:
|
|
83
|
-
return project_dir(root) / "generated"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def data_dir(root: Path, service_id: str) -> Path:
|
|
87
|
-
return project_dir(root) / "data" / service_id
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def custom_ssl_dir(root: Path) -> Path:
|
|
91
|
-
return project_dir(root) / "ssl" / "custom"
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def fsd_evidence_dir(root: Path) -> Path:
|
|
95
|
-
return project_dir(root) / "fsd" / "evidence"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def architecture_dir(root: Path) -> Path:
|
|
99
|
-
return project_dir(root) / "architecture"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def agents_history_dir(root: Path) -> Path:
|
|
103
|
-
return project_dir(root) / "agents" / "history"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def agents_history_file(root: Path, agent_id: str) -> Path:
|
|
107
|
-
return agents_history_dir(root) / f"{agent_id}.jsonl"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
def agents_types_file(root: Path) -> Path:
|
|
111
|
-
return project_dir(root) / "agents" / "types.json"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def scraper_truths_dir(root: Path) -> Path:
|
|
115
|
-
return project_dir(root) / "scraper" / "truths"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def scraper_truths_file(root: Path, domain: str) -> Path:
|
|
119
|
-
return scraper_truths_dir(root) / f"{domain}.json"
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def scraper_output_dir(root: Path) -> Path:
|
|
123
|
-
return project_dir(root) / "scraper" / "output"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def project_config_file(root: Path) -> Path:
|
|
127
|
-
return project_dir(root) / "config.json"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def project_cloud_file(root: Path) -> Path:
|
|
131
|
-
return project_dir(root) / "cloud" / "config.json"
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
def project_intelligence_file(root: Path) -> Path:
|
|
135
|
-
return project_dir(root) / "intelligence.json"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def project_network_file(root: Path) -> Path:
|
|
139
|
-
return project_dir(root) / "network.json"
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
# ── User/OS-scope paths ──────────────────────────────────────────────────────
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
def global_dir() -> Path:
|
|
146
|
-
"""Return the global forktex directory, cross-platform.
|
|
147
|
-
|
|
148
|
-
POSIX: ``~/.forktex/``. Windows: ``%APPDATA%/forktex/`` (roaming user profile).
|
|
149
|
-
"""
|
|
150
|
-
if sys.platform == "win32":
|
|
151
|
-
appdata = os.environ.get("APPDATA")
|
|
152
|
-
base = Path(appdata) if appdata else Path.home()
|
|
153
|
-
return base / _USER_DIRNAME_WINDOWS
|
|
154
|
-
return Path.home() / _USER_DIRNAME_POSIX
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def global_cloud_file() -> Path:
|
|
158
|
-
return global_dir() / "cloud.json"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def global_intelligence_file() -> Path:
|
|
162
|
-
return global_dir() / "intelligence.json"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def global_network_file() -> Path:
|
|
166
|
-
return global_dir() / "network.json"
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def global_config_file() -> Path:
|
|
170
|
-
return global_dir() / "config.toml"
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
# ── Lifecycle helpers ────────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def ensure_project_dirs(root: Path) -> None:
|
|
177
|
-
"""Create ``.forktex/`` under *root*, write the schema version marker, and
|
|
178
|
-
ensure the project ``.gitignore`` has the canonical forktex block.
|
|
179
|
-
|
|
180
|
-
Safe to call repeatedly. Does not touch existing files.
|
|
181
|
-
"""
|
|
182
|
-
pdir = project_dir(root)
|
|
183
|
-
pdir.mkdir(parents=True, exist_ok=True)
|
|
184
|
-
write_schema_version(root)
|
|
185
|
-
_ensure_gitignore_block(root)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
def ensure_global_dir() -> None:
|
|
189
|
-
"""Create the user/OS-scope forktex dir with secure permissions on POSIX."""
|
|
190
|
-
gdir = global_dir()
|
|
191
|
-
gdir.mkdir(parents=True, exist_ok=True)
|
|
192
|
-
if sys.platform != "win32":
|
|
193
|
-
try:
|
|
194
|
-
gdir.chmod(0o700)
|
|
195
|
-
except OSError:
|
|
196
|
-
# Non-fatal: existing dir with stricter perms is fine.
|
|
197
|
-
pass
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def read_schema_version(root: Path) -> int | None:
|
|
201
|
-
"""Return the on-disk ``.forktex/.version`` as int, or ``None`` if missing."""
|
|
202
|
-
vf = version_file(root)
|
|
203
|
-
if not vf.is_file():
|
|
204
|
-
return None
|
|
205
|
-
try:
|
|
206
|
-
return int(vf.read_text().strip())
|
|
207
|
-
except (ValueError, OSError):
|
|
208
|
-
return None
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def write_schema_version(root: Path) -> None:
|
|
212
|
-
"""Write ``SCHEMA_VERSION`` to ``.forktex/.version`` if not already correct."""
|
|
213
|
-
vf = version_file(root)
|
|
214
|
-
if read_schema_version(root) == SCHEMA_VERSION:
|
|
215
|
-
return
|
|
216
|
-
vf.parent.mkdir(parents=True, exist_ok=True)
|
|
217
|
-
vf.write_text(f"{SCHEMA_VERSION}\n")
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
def _ensure_gitignore_block(root: Path) -> None:
|
|
221
|
-
gi = root / ".gitignore"
|
|
222
|
-
if gi.is_file():
|
|
223
|
-
existing = gi.read_text()
|
|
224
|
-
if _GITIGNORE_MARKER in existing:
|
|
225
|
-
return
|
|
226
|
-
# Append, ensuring a trailing newline separator.
|
|
227
|
-
if not existing.endswith("\n"):
|
|
228
|
-
existing += "\n"
|
|
229
|
-
gi.write_text(existing + _GITIGNORE_BLOCK)
|
|
230
|
-
else:
|
|
231
|
-
gi.write_text(_GITIGNORE_BLOCK.lstrip("\n"))
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
__all__ = [
|
|
235
|
-
# Constants
|
|
236
|
-
"SCHEMA_VERSION",
|
|
237
|
-
"PROJECT_DIRNAME",
|
|
238
|
-
# Project-scope
|
|
239
|
-
"project_dir",
|
|
240
|
-
"version_file",
|
|
241
|
-
"compose_path",
|
|
242
|
-
"observability_dir",
|
|
243
|
-
"vault_dir",
|
|
244
|
-
"vault_secrets_file",
|
|
245
|
-
"state_dir",
|
|
246
|
-
"servers_json",
|
|
247
|
-
"server_keys_dir",
|
|
248
|
-
"generated_dir",
|
|
249
|
-
"data_dir",
|
|
250
|
-
"custom_ssl_dir",
|
|
251
|
-
"fsd_evidence_dir",
|
|
252
|
-
"architecture_dir",
|
|
253
|
-
"agents_history_dir",
|
|
254
|
-
"agents_history_file",
|
|
255
|
-
"agents_types_file",
|
|
256
|
-
"scraper_truths_dir",
|
|
257
|
-
"scraper_truths_file",
|
|
258
|
-
"scraper_output_dir",
|
|
259
|
-
"project_config_file",
|
|
260
|
-
"project_cloud_file",
|
|
261
|
-
"project_intelligence_file",
|
|
262
|
-
"project_network_file",
|
|
263
|
-
# User/OS-scope
|
|
264
|
-
"global_dir",
|
|
265
|
-
"global_cloud_file",
|
|
266
|
-
"global_intelligence_file",
|
|
267
|
-
"global_network_file",
|
|
268
|
-
"global_config_file",
|
|
269
|
-
# Lifecycle
|
|
270
|
-
"ensure_project_dirs",
|
|
271
|
-
"ensure_global_dir",
|
|
272
|
-
"read_schema_version",
|
|
273
|
-
"write_schema_version",
|
|
274
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/bridge/persistence_defaults.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/templates/observability/loki.yml
RENAMED
|
File without changes
|
{forktex_cloud-1.0.0 → forktex_cloud-2.0.0}/src/forktex_cloud/templates/observability/promtail.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|