fadenstack 0.4.2__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.
- faden_cli/__init__.py +16 -0
- faden_cli/__main__.py +9 -0
- faden_cli/bundle/shared/.gitignore +25 -0
- faden_cli/bundle/shared/LICENSE +187 -0
- faden_cli/bundle/shared/NOTICE +15 -0
- faden_cli/bundle/shared/OFFICIAL.md +9 -0
- faden_cli/bundle/shared/README.md +16 -0
- faden_cli/bundle/shared/agent-binaries/.gitignore +5 -0
- faden_cli/bundle/shared/alloy/config.alloy +40 -0
- faden_cli/bundle/shared/clickhouse/config.d/system-logs.xml +26 -0
- faden_cli/bundle/shared/docker-compose.gpu.yaml +34 -0
- faden_cli/bundle/shared/docker-compose.yaml +998 -0
- faden_cli/bundle/shared/grafana/dashboards/templates/vllm-runtime.json +2099 -0
- faden_cli/bundle/shared/grafana/provisioning/dashboards/dashboard.yml +10 -0
- faden_cli/bundle/shared/grafana/provisioning/dashboards/json/faden-overview.json +211 -0
- faden_cli/bundle/shared/grafana/provisioning/datasources/ds.yaml +9 -0
- faden_cli/bundle/shared/grafana/provisioning/datasources/prometheus.yaml +15 -0
- faden_cli/bundle/shared/initdb/01-init.sql +32 -0
- faden_cli/bundle/shared/loki/local-config.yaml +32 -0
- faden_cli/bundle/shared/nginx/locations.conf +245 -0
- faden_cli/bundle/shared/nginx/maintenance.html +239 -0
- faden_cli/bundle/shared/nginx/nginx-tls.conf +79 -0
- faden_cli/bundle/shared/nginx/nginx.conf +44 -0
- faden_cli/bundle/shared/nginx/rate-limits.conf +42 -0
- faden_cli/bundle/shared/nginx/security-headers.conf +22 -0
- faden_cli/bundle/shared/nginx/select-config.sh +37 -0
- faden_cli/bundle/shared/prometheus/prometheus.yml +40 -0
- faden_cli/bundle/shared/rabbitmq/definitions.json.example +37 -0
- faden_cli/bundle/shared/tls/README.md +30 -0
- faden_cli/cli.py +158 -0
- faden_cli/commands/__init__.py +1 -0
- faden_cli/commands/admin.py +161 -0
- faden_cli/commands/backup.py +159 -0
- faden_cli/commands/config.py +146 -0
- faden_cli/commands/deploy.py +949 -0
- faden_cli/commands/dev/__init__.py +1 -0
- faden_cli/commands/dev/dev_doctor.py +46 -0
- faden_cli/commands/dev/dev_down.py +241 -0
- faden_cli/commands/dev/dev_group.py +24 -0
- faden_cli/commands/dev/dev_init.py +710 -0
- faden_cli/commands/dev/dev_pull.py +297 -0
- faden_cli/commands/dev/dev_status.py +151 -0
- faden_cli/commands/dev/dev_up.py +1392 -0
- faden_cli/commands/doctor.py +225 -0
- faden_cli/commands/down.py +75 -0
- faden_cli/commands/logs_cmd.py +41 -0
- faden_cli/commands/maintenance.py +74 -0
- faden_cli/commands/module.py +113 -0
- faden_cli/commands/observe.py +95 -0
- faden_cli/commands/ports.py +91 -0
- faden_cli/commands/rebuild.py +158 -0
- faden_cli/commands/restore.py +139 -0
- faden_cli/commands/runtime_images.py +174 -0
- faden_cli/commands/status.py +71 -0
- faden_cli/commands/tls.py +722 -0
- faden_cli/commands/tune.py +37 -0
- faden_cli/commands/up.py +64 -0
- faden_cli/commands/upgrade.py +470 -0
- faden_cli/commands/version.py +78 -0
- faden_cli/core/__init__.py +1 -0
- faden_cli/core/api_client.py +77 -0
- faden_cli/core/backup.py +717 -0
- faden_cli/core/bootstrap.py +291 -0
- faden_cli/core/bundle.py +110 -0
- faden_cli/core/clickhouse.py +61 -0
- faden_cli/core/compose.py +484 -0
- faden_cli/core/console.py +106 -0
- faden_cli/core/detect.py +580 -0
- faden_cli/core/dev_modules.py +149 -0
- faden_cli/core/docker_env.py +147 -0
- faden_cli/core/edge.py +94 -0
- faden_cli/core/env_gen.py +598 -0
- faden_cli/core/git.py +219 -0
- faden_cli/core/install.py +288 -0
- faden_cli/core/installation.py +186 -0
- faden_cli/core/local_node.py +719 -0
- faden_cli/core/maintenance.py +82 -0
- faden_cli/core/private_files.py +133 -0
- faden_cli/core/registry.py +574 -0
- faden_cli/core/release_feed.py +342 -0
- faden_cli/core/rmq.py +135 -0
- faden_cli/core/runtime_images.py +309 -0
- faden_cli/core/schedule.py +278 -0
- faden_cli/core/settings.py +159 -0
- faden_cli/core/sysinfo.py +303 -0
- faden_cli/core/tls.py +790 -0
- faden_cli/core/tune.py +154 -0
- faden_cli/core/update_check.py +254 -0
- faden_cli/core/workspace.py +155 -0
- faden_cli/templates/env.j2 +33 -0
- faden_cli/tui/__init__.py +1 -0
- faden_cli/tui/widgets/__init__.py +1 -0
- faden_cli/tui/wizard/__init__.py +1 -0
- fadenstack-0.4.2.dist-info/METADATA +304 -0
- fadenstack-0.4.2.dist-info/RECORD +99 -0
- fadenstack-0.4.2.dist-info/WHEEL +4 -0
- fadenstack-0.4.2.dist-info/entry_points.txt +3 -0
- fadenstack-0.4.2.dist-info/licenses/LICENSE +187 -0
- fadenstack-0.4.2.dist-info/licenses/NOTICE +15 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""``faden dev pull`` — pull the latest changes from a branch.
|
|
2
|
+
|
|
3
|
+
Fetches and fast-forwards every repository checked out in the dev
|
|
4
|
+
workspace to the latest commit of the target branch (default:
|
|
5
|
+
``main``). This is the counterpart of ``dev init --overwrite`` for an already
|
|
6
|
+
bootstrapped workspace: no cloning and no dependency installation — it
|
|
7
|
+
just brings the checkouts up to date.
|
|
8
|
+
|
|
9
|
+
Repositories discovered in the workspace (same convention as
|
|
10
|
+
``dev status``):
|
|
11
|
+
|
|
12
|
+
* every Fadenstack service in the monorepo layout
|
|
13
|
+
``<workspace>/fadenstack/<name>``, or
|
|
14
|
+
* flat ``<workspace>/<name>`` checkouts, when that layout is in use;
|
|
15
|
+
* the workspace root itself, if it is a git checkout (running the CLI
|
|
16
|
+
from inside a checkout).
|
|
17
|
+
|
|
18
|
+
Subdirectories sharing a git repository collapse to the single repo
|
|
19
|
+
root, and only repositories that live *inside* the workspace are ever
|
|
20
|
+
touched — unrelated checkouts (e.g. an ``nvm`` install under a
|
|
21
|
+
home-directory workspace) and ancestor checkouts the workspace merely
|
|
22
|
+
sits in are both excluded.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import click
|
|
32
|
+
from rich.table import Table
|
|
33
|
+
|
|
34
|
+
from faden_cli.core.console import console, error, info, success, warning
|
|
35
|
+
from faden_cli.core.registry import REPO_DIR_MAP
|
|
36
|
+
from faden_cli.core.settings import load_config
|
|
37
|
+
from faden_cli.core.workspace import find_service_dir
|
|
38
|
+
|
|
39
|
+
from .dev_group import dev_group
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _git(repo_dir: Path, *args: str, timeout: int = 300) -> subprocess.CompletedProcess | None:
|
|
43
|
+
"""Run a git command in *repo_dir*; returns None if git is missing or timed out."""
|
|
44
|
+
git = shutil.which("git")
|
|
45
|
+
if not git:
|
|
46
|
+
return None
|
|
47
|
+
try:
|
|
48
|
+
return subprocess.run( # noqa: S603
|
|
49
|
+
[git, *args],
|
|
50
|
+
cwd=str(repo_dir),
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
)
|
|
55
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _git_toplevel(path: Path, within: Path | None = None) -> Path | None:
|
|
60
|
+
"""Return the git repo root containing *path*, or None if it is not in one.
|
|
61
|
+
|
|
62
|
+
When *within* is given, the repo root must lie inside *within*
|
|
63
|
+
(inclusive) — ``rev-parse --show-toplevel`` walks up ancestor
|
|
64
|
+
directories, so a candidate inside a checkout merely *containing*
|
|
65
|
+
the workspace would otherwise resolve to that outer repo.
|
|
66
|
+
"""
|
|
67
|
+
if not path.is_dir():
|
|
68
|
+
return None
|
|
69
|
+
proc = _git(path, "rev-parse", "--show-toplevel", timeout=15)
|
|
70
|
+
if proc is None or proc.returncode != 0:
|
|
71
|
+
return None
|
|
72
|
+
toplevel = Path(proc.stdout.strip())
|
|
73
|
+
if within is not None:
|
|
74
|
+
try:
|
|
75
|
+
toplevel.resolve().relative_to(within.resolve())
|
|
76
|
+
except ValueError:
|
|
77
|
+
return None
|
|
78
|
+
return toplevel
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _find_repo_dirs(workspace: Path) -> list[tuple[str, Path]]:
|
|
82
|
+
"""Return ``(display_name, repo_root)`` for each repo in the workspace.
|
|
83
|
+
|
|
84
|
+
Discovery follows the ``dev status`` convention — registry-driven via
|
|
85
|
+
:func:`find_service_dir` (monorepo layout ``<ws>/fadenstack/<name>``
|
|
86
|
+
or flat ``<ws>/<name>``) — plus the workspace root itself when it is a
|
|
87
|
+
checkout (running the CLI from inside a repo). Candidates that live
|
|
88
|
+
inside the same git repository (e.g. monorepo subdirs) collapse to the
|
|
89
|
+
single repo root, so each checkout is pulled exactly once. Unrelated
|
|
90
|
+
repos that happen to sit in the workspace (e.g. ``.nvm``) are never
|
|
91
|
+
touched.
|
|
92
|
+
"""
|
|
93
|
+
found: list[tuple[str, Path]] = []
|
|
94
|
+
seen: set[str] = set()
|
|
95
|
+
|
|
96
|
+
def _add(candidate: Path) -> None:
|
|
97
|
+
toplevel = _git_toplevel(candidate, within=workspace)
|
|
98
|
+
if toplevel is None:
|
|
99
|
+
return
|
|
100
|
+
key = str(toplevel.resolve())
|
|
101
|
+
if key in seen:
|
|
102
|
+
return
|
|
103
|
+
seen.add(key)
|
|
104
|
+
found.append((toplevel.name or str(toplevel), toplevel))
|
|
105
|
+
|
|
106
|
+
if workspace.is_dir():
|
|
107
|
+
_add(workspace)
|
|
108
|
+
for local_name in REPO_DIR_MAP.values():
|
|
109
|
+
_add(find_service_dir(workspace, local_name))
|
|
110
|
+
|
|
111
|
+
return found
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _pull_repo(
|
|
115
|
+
repo_dir: Path, branch: str, force: bool = False, hard: bool = False
|
|
116
|
+
) -> tuple[str, str]:
|
|
117
|
+
"""Pull *branch* into *repo_dir*.
|
|
118
|
+
|
|
119
|
+
Returns a ``(status, detail)`` tuple where status is one of
|
|
120
|
+
``up-to-date``, ``updated``, ``forced``, ``failed``, ``skipped`` and
|
|
121
|
+
detail is a human-readable explanation (current branch, error
|
|
122
|
+
message, …).
|
|
123
|
+
|
|
124
|
+
With *hard*, the checkout is forcibly aligned to
|
|
125
|
+
``origin/<branch>`` via three steps, in order:
|
|
126
|
+
|
|
127
|
+
1. ``git clean -fd`` — remove untracked files; build artifacts that
|
|
128
|
+
are git-ignored (``node_modules/``, ``.venv/``, ``.env``) are
|
|
129
|
+
preserved, everything else that is not tracked is deleted.
|
|
130
|
+
2. ``git checkout -f <branch>`` — switch to the target branch,
|
|
131
|
+
discarding any changes that would be overwritten by the switch.
|
|
132
|
+
3. ``git reset --hard origin/<branch>`` — discard the remaining
|
|
133
|
+
tracked modifications and move the working tree exactly to the
|
|
134
|
+
remote branch tip.
|
|
135
|
+
|
|
136
|
+
**No backup is taken** — local edits are irrecoverable.
|
|
137
|
+
"""
|
|
138
|
+
proc = _git(repo_dir, "fetch", "origin", "--prune")
|
|
139
|
+
if proc is None:
|
|
140
|
+
return "failed", "git not found on PATH (or fetch timed out)"
|
|
141
|
+
if proc.returncode != 0:
|
|
142
|
+
return "failed", (proc.stderr or proc.stdout).strip() or f"git fetch exited with code {proc.returncode}"
|
|
143
|
+
|
|
144
|
+
# Does the branch even exist on the remote?
|
|
145
|
+
check = _git(repo_dir, "rev-parse", "--verify", "--quiet", f"origin/{branch}")
|
|
146
|
+
if check is not None and check.returncode != 0:
|
|
147
|
+
return "skipped", f"branch '{branch}' not found on remote"
|
|
148
|
+
|
|
149
|
+
if hard:
|
|
150
|
+
cl = _git(repo_dir, "clean", "-fd", timeout=600)
|
|
151
|
+
if cl is None or cl.returncode != 0:
|
|
152
|
+
detail = (cl.stderr if cl else "git timed out").strip() or "git clean -fd failed"
|
|
153
|
+
return "failed", detail
|
|
154
|
+
co = _git(repo_dir, "checkout", "-f", branch)
|
|
155
|
+
if co is None or co.returncode != 0:
|
|
156
|
+
detail = (co.stderr if co else "git timed out").strip() or "git checkout -f failed"
|
|
157
|
+
return "failed", detail
|
|
158
|
+
rs = _git(repo_dir, "reset", "--hard", f"origin/{branch}")
|
|
159
|
+
if rs is None or rs.returncode != 0:
|
|
160
|
+
detail = (rs.stderr if rs else "git reset timed out").strip() or "git reset --hard failed"
|
|
161
|
+
return "failed", detail
|
|
162
|
+
return "forced", f"reset to origin/{branch} (untracked files cleaned, local changes discarded)"
|
|
163
|
+
|
|
164
|
+
cur_proc = _git(repo_dir, "rev-parse", "--abbrev-ref", "HEAD")
|
|
165
|
+
current = cur_proc.stdout.strip() if cur_proc and cur_proc.returncode == 0 else "unknown"
|
|
166
|
+
|
|
167
|
+
if current != branch:
|
|
168
|
+
if force:
|
|
169
|
+
co = _git(repo_dir, "checkout", branch)
|
|
170
|
+
if co is None or co.returncode != 0:
|
|
171
|
+
detail = (co.stderr if co else "git timed out").strip() or "git checkout failed"
|
|
172
|
+
return "failed", detail
|
|
173
|
+
else:
|
|
174
|
+
return "skipped", f"checked out on '{current}' (use --force to switch to '{branch}')"
|
|
175
|
+
|
|
176
|
+
pull = _git(repo_dir, "pull", "--ff-only", "origin", branch)
|
|
177
|
+
if pull is None:
|
|
178
|
+
return "failed", "git pull timed out"
|
|
179
|
+
if pull.returncode != 0:
|
|
180
|
+
detail = (pull.stderr or pull.stdout).strip()
|
|
181
|
+
if "not possible" in detail or "diverged" in detail.lower() or "conflict" in detail.lower():
|
|
182
|
+
return "failed", "local changes conflict with origin — commit or stash them, then re-run"
|
|
183
|
+
return "failed", detail or f"git pull exited with code {pull.returncode}"
|
|
184
|
+
|
|
185
|
+
if "Already up to date" in pull.stdout:
|
|
186
|
+
return "up-to-date", f"on '{branch}'"
|
|
187
|
+
return "updated", f"on '{branch}'"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dev_group.command("pull")
|
|
191
|
+
@click.option(
|
|
192
|
+
"--branch",
|
|
193
|
+
"-b",
|
|
194
|
+
default="main",
|
|
195
|
+
show_default=True,
|
|
196
|
+
help="Branch to pull from the remote.",
|
|
197
|
+
)
|
|
198
|
+
@click.option(
|
|
199
|
+
"--force",
|
|
200
|
+
"-f",
|
|
201
|
+
is_flag=True,
|
|
202
|
+
help="Switch repos to the target branch even if another branch is checked out.",
|
|
203
|
+
)
|
|
204
|
+
@click.option(
|
|
205
|
+
"--hard",
|
|
206
|
+
"-H",
|
|
207
|
+
is_flag=True,
|
|
208
|
+
help=(
|
|
209
|
+
"DESTRUCTIVE: clean untracked files, discard all local changes and "
|
|
210
|
+
"reset every checkout to the remote branch. Un-mergeable pull "
|
|
211
|
+
"errors (e.g. local changes or untracked files colliding with "
|
|
212
|
+
"incoming files) are resolved this way. No backup is taken."
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
def dev_pull(*, branch: str, force: bool, hard: bool) -> None:
|
|
216
|
+
"""Pull the latest changes from the default branch.
|
|
217
|
+
|
|
218
|
+
Fetches and fast-forwards every repository checked out in the dev
|
|
219
|
+
workspace to the latest commit of the target branch (default
|
|
220
|
+
``main``). Working trees that cannot be fast-forwarded are
|
|
221
|
+
reported and left untouched; ``--hard`` cleans untracked files,
|
|
222
|
+
discards all local changes and resets every checkout to the remote
|
|
223
|
+
branch (destructive).
|
|
224
|
+
|
|
225
|
+
\b
|
|
226
|
+
Examples:
|
|
227
|
+
faden dev pull
|
|
228
|
+
faden dev pull --branch feature-x
|
|
229
|
+
faden dev pull -b main --force
|
|
230
|
+
faden dev pull --hard # destructive
|
|
231
|
+
"""
|
|
232
|
+
cfg = load_config()
|
|
233
|
+
workspace = (
|
|
234
|
+
Path(cfg.dev.workspace_dir).expanduser()
|
|
235
|
+
if cfg.dev and cfg.dev.workspace_dir
|
|
236
|
+
else Path.cwd()
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
console.print(f"\n[bold cyan]Pulling [bold]{branch}[/bold] in {workspace}[/bold cyan]\n")
|
|
240
|
+
if not workspace.is_dir():
|
|
241
|
+
error(f"Dev workspace does not exist: {workspace}")
|
|
242
|
+
raise SystemExit(1)
|
|
243
|
+
|
|
244
|
+
repos = _find_repo_dirs(workspace)
|
|
245
|
+
if not repos:
|
|
246
|
+
error(
|
|
247
|
+
f"No git repositories found in {workspace}. "
|
|
248
|
+
"Run [bold]faden dev init[/bold] to bootstrap the workspace first."
|
|
249
|
+
)
|
|
250
|
+
raise SystemExit(1)
|
|
251
|
+
|
|
252
|
+
table = Table(title="Pull results", show_header=True, header_style="bold cyan")
|
|
253
|
+
table.add_column("Repository", style="bold")
|
|
254
|
+
table.add_column("Result")
|
|
255
|
+
table.add_column("Detail")
|
|
256
|
+
|
|
257
|
+
style_by_status = {
|
|
258
|
+
"up-to-date": "[green]up to date[/green]",
|
|
259
|
+
"updated": "[green]updated[/green]",
|
|
260
|
+
"forced": "[green]forced[/green]",
|
|
261
|
+
"failed": "[red]failed[/red]",
|
|
262
|
+
"skipped": "[yellow]skipped[/yellow]",
|
|
263
|
+
}
|
|
264
|
+
counts: dict[str, int] = {}
|
|
265
|
+
|
|
266
|
+
for name, repo_dir in repos:
|
|
267
|
+
status, detail = _pull_repo(repo_dir, branch, force, hard)
|
|
268
|
+
counts[status] = counts.get(status, 0) + 1
|
|
269
|
+
table.add_row(name, style_by_status.get(status, status), detail or "—")
|
|
270
|
+
|
|
271
|
+
console.print(table)
|
|
272
|
+
|
|
273
|
+
failed = counts.get("failed", 0)
|
|
274
|
+
if counts.get("updated", 0):
|
|
275
|
+
success(f"{counts['updated']} repository(ies) updated to [bold]{branch}[/bold].")
|
|
276
|
+
if counts.get("forced", 0):
|
|
277
|
+
success(
|
|
278
|
+
f"{counts['forced']} repository(ies) reset to [bold]{branch}[/bold]"
|
|
279
|
+
" (local changes discarded)."
|
|
280
|
+
)
|
|
281
|
+
if counts.get("up-to-date", 0):
|
|
282
|
+
info(f"{counts['up-to-date']} repository(ies) already up to date.")
|
|
283
|
+
if counts.get("skipped", 0):
|
|
284
|
+
warning(
|
|
285
|
+
f"{counts['skipped']} repository(ies) skipped "
|
|
286
|
+
"(branch missing on remote or checked out on another branch)."
|
|
287
|
+
)
|
|
288
|
+
if failed:
|
|
289
|
+
error(f"{failed} repository(ies) failed — resolve the reported issues and re-run.")
|
|
290
|
+
if not hard:
|
|
291
|
+
warning(
|
|
292
|
+
"If you want to discard ALL local changes and untracked files "
|
|
293
|
+
"and take the remote state as-is, re-run with [bold]--hard[/bold]."
|
|
294
|
+
)
|
|
295
|
+
raise SystemExit(1)
|
|
296
|
+
|
|
297
|
+
info("Next: [bold]faden dev up[/bold] to run the services against the new code.")
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""``faden dev status`` — show developer workspace status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from faden_cli.core.compose import ComposeContext, ps as compose_ps
|
|
12
|
+
from faden_cli.core.console import console, info
|
|
13
|
+
from faden_cli.core.git import current_branch
|
|
14
|
+
from faden_cli.core.registry import DEV_PROCESSES
|
|
15
|
+
from faden_cli.core.settings import REPO_DIR_MAP, load_config
|
|
16
|
+
from faden_cli.core.workspace import find_service_dir, resolve_shared_compose
|
|
17
|
+
|
|
18
|
+
from .dev_group import dev_group
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _check_process(pattern: str) -> bool:
|
|
22
|
+
"""Check if a process matching the pattern is running."""
|
|
23
|
+
import platform
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
if platform.system() == "Windows":
|
|
27
|
+
result = subprocess.run(
|
|
28
|
+
["powershell", "-Command",
|
|
29
|
+
f"Get-Process | Where-Object {{ $_.CommandLine -like '*{pattern}*' }} | "
|
|
30
|
+
f"Select-Object -First 1 | ForEach-Object {{ $_.Id }}"],
|
|
31
|
+
capture_output=True,
|
|
32
|
+
text=True,
|
|
33
|
+
)
|
|
34
|
+
return bool(result.stdout.strip())
|
|
35
|
+
else:
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["pgrep", "-f", pattern],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
)
|
|
40
|
+
return result.returncode == 0
|
|
41
|
+
except Exception:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dev_group.command("status")
|
|
46
|
+
def dev_status() -> None:
|
|
47
|
+
"""Show the status of the developer workspace.
|
|
48
|
+
|
|
49
|
+
Displays:
|
|
50
|
+
• Repository branches and git status
|
|
51
|
+
• Shared infrastructure containers
|
|
52
|
+
• Running dev processes (backend, worker, frontend)
|
|
53
|
+
"""
|
|
54
|
+
from faden_cli.core.workspace import resolve_workspace
|
|
55
|
+
|
|
56
|
+
cfg = load_config()
|
|
57
|
+
workspace = resolve_workspace()
|
|
58
|
+
|
|
59
|
+
console.print("[bold magenta]fadenstack Developer Status[/bold magenta]\n")
|
|
60
|
+
|
|
61
|
+
# ── Repositories ──────────────────────────────────────────────
|
|
62
|
+
repo_table = Table(title="Repositories", show_header=True, header_style="bold cyan")
|
|
63
|
+
repo_table.add_column("Repository", style="bold")
|
|
64
|
+
repo_table.add_column("Branch")
|
|
65
|
+
repo_table.add_column("Status")
|
|
66
|
+
|
|
67
|
+
for _gh_name, local_name in sorted(REPO_DIR_MAP.items()):
|
|
68
|
+
repo_path = find_service_dir(workspace, local_name)
|
|
69
|
+
if repo_path.exists():
|
|
70
|
+
branch = current_branch(repo_path) or "—"
|
|
71
|
+
# Quick dirty check
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
["git", "status", "--porcelain"],
|
|
75
|
+
cwd=str(repo_path),
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
)
|
|
79
|
+
if result.stdout.strip():
|
|
80
|
+
status = f"[yellow]{len(result.stdout.strip().splitlines())} changed[/yellow]"
|
|
81
|
+
else:
|
|
82
|
+
status = "[green]clean[/green]"
|
|
83
|
+
except Exception:
|
|
84
|
+
status = "[dim]unknown[/dim]"
|
|
85
|
+
|
|
86
|
+
repo_table.add_row(local_name, branch, status)
|
|
87
|
+
else:
|
|
88
|
+
repo_table.add_row(local_name, "—", "[dim]not cloned[/dim]")
|
|
89
|
+
|
|
90
|
+
console.print(repo_table)
|
|
91
|
+
|
|
92
|
+
# ── Shared infrastructure ─────────────────────────────────────
|
|
93
|
+
console.print()
|
|
94
|
+
compose_file = resolve_shared_compose(workspace)
|
|
95
|
+
if compose_file:
|
|
96
|
+
env_file = compose_file.parent / ".env"
|
|
97
|
+
if env_file.exists():
|
|
98
|
+
from faden_cli.commands.dev.dev_up import heal_dev_env
|
|
99
|
+
|
|
100
|
+
heal_dev_env(env_file)
|
|
101
|
+
ctx = ComposeContext(
|
|
102
|
+
compose_files=[str(compose_file)],
|
|
103
|
+
env_file=str(env_file) if env_file.exists() else None,
|
|
104
|
+
project_dir=str(compose_file.parent),
|
|
105
|
+
)
|
|
106
|
+
services = compose_ps(ctx)
|
|
107
|
+
|
|
108
|
+
if services:
|
|
109
|
+
svc_table = Table(title="Shared Infrastructure", show_header=True, header_style="bold cyan")
|
|
110
|
+
svc_table.add_column("Container", style="bold")
|
|
111
|
+
svc_table.add_column("State")
|
|
112
|
+
svc_table.add_column("Health")
|
|
113
|
+
svc_table.add_column("Ports")
|
|
114
|
+
|
|
115
|
+
state_styles = {
|
|
116
|
+
"running": "[green]running[/green]",
|
|
117
|
+
"exited": "[red]exited[/red]",
|
|
118
|
+
"restarting": "[yellow]restarting[/yellow]",
|
|
119
|
+
}
|
|
120
|
+
health_styles = {
|
|
121
|
+
"healthy": "[green]healthy[/green]",
|
|
122
|
+
"unhealthy": "[red]unhealthy[/red]",
|
|
123
|
+
"starting": "[yellow]starting[/yellow]",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for svc in sorted(services, key=lambda s: s.service):
|
|
127
|
+
svc_table.add_row(
|
|
128
|
+
svc.service,
|
|
129
|
+
state_styles.get(svc.state, svc.state or "—"),
|
|
130
|
+
health_styles.get(svc.health, svc.health or "—"),
|
|
131
|
+
svc.ports or "—",
|
|
132
|
+
)
|
|
133
|
+
console.print(svc_table)
|
|
134
|
+
else:
|
|
135
|
+
info("No shared infrastructure containers running.")
|
|
136
|
+
else:
|
|
137
|
+
info("Shared compose file not found.")
|
|
138
|
+
|
|
139
|
+
# ── Dev processes ─────────────────────────────────────────────
|
|
140
|
+
console.print()
|
|
141
|
+
proc_table = Table(title="Dev Processes", show_header=True, header_style="bold cyan")
|
|
142
|
+
proc_table.add_column("Process", style="bold")
|
|
143
|
+
proc_table.add_column("Status")
|
|
144
|
+
proc_table.add_column("URL")
|
|
145
|
+
|
|
146
|
+
for proc in DEV_PROCESSES:
|
|
147
|
+
running = _check_process(proc.pattern)
|
|
148
|
+
status = "[green]running[/green]" if running else "[dim]stopped[/dim]"
|
|
149
|
+
proc_table.add_row(proc.name, status, proc.url)
|
|
150
|
+
|
|
151
|
+
console.print(proc_table)
|