dev: enable default ticket tools and workdir helpers

This commit is contained in:
Keisuke Hirata 2026-07-18 17:32:46 +09:00
parent 60410f29a1
commit 5e91f98ae0
No known key found for this signature in database
18 changed files with 713 additions and 11 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
/target
/result
/.direnv
/.yoi/dev
.worktree
*.local*
.env

View File

@ -0,0 +1,31 @@
---
title: 'Enable Ticket tools in default profile'
state: 'closed'
created_at: '2026-07-18T03:04:36Z'
updated_at: '2026-07-18T08:32:27Z'
assignee: null
queued_by: 'yoi ticket'
queued_at: '2026-07-18T03:05:10Z'
---
## 背景
通常の `builtin:default` Worker では Ticket feature が無効で、`TicketList` / `TicketShow` などの typed Ticket tools が露出しない。実装作業では Ticket の前提・受け入れ条件・thread を読めないと正しく作業できないため、default profile でも Ticket tools を有効化する。
Orchestrator 固有の relation / orchestration plan tools は引き続き `ticket_orchestration` で分離し、default profile では有効化しない。
## 要件
- `resources/profiles/default.dcdl` の Ticket feature を有効化する。
- builtin default profile artifact の Ticket feature も同じ既定にする。
- setup wizard が生成する `user:default` profile でも Ticket feature を有効化する。
- `ticket_orchestration` は default では無効のままにする。
- 裸の `WorkerManifestConfig::builtin_defaults()` の feature default は profile 既定とは別なので変更しない。
## 受け入れ条件
- `builtin:default` profile resolution で `feature.ticket.enabled == true` になる。
- `builtin:default` profile resolution で `feature.ticket.access == lifecycle` になる。
- `builtin:default` profile resolution で `feature.ticket_orchestration.enabled == false` のままになる。
- setup model が生成する default profile に `[feature.ticket] enabled = true` が含まれる。
- 関連する profile/setup focused tests が通る。

View File

@ -0,0 +1 @@
Default profile now enables Ticket lifecycle tools by default while keeping ticket orchestration disabled. Builtin default profile and setup-generated user default profile were updated, with focused tests covering the new expectations.

View File

@ -0,0 +1,85 @@
<!-- event: create author: "yoi ticket" at: 2026-07-18T03:04:36Z -->
## 作成
LocalTicketBackend によって作成されました。
---
<!-- event: intake_summary author: hare at: 2026-07-18T03:05:10Z -->
## Intake summary
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T03:05:10Z from: planning to: ready reason: cli_state field: state -->
## State changed
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T03:05:10Z from: ready to: queued reason: queued field: state -->
## State changed
Ticket を `yoi ticket` が queued にしました。
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T03:05:10Z from: queued to: inprogress reason: cli_state field: state -->
## State changed
State changed to `inprogress`.
---
<!-- event: implementation_report author: hare at: 2026-07-18T03:07:25Z -->
## Implementation report
`builtin:default` profile と setup wizard 生成の `user:default` profile で Ticket feature を有効化した。
変更点:
- `resources/profiles/default.dcdl``feature.ticket.enabled``true` に変更。
- builtin default profile artifact の `feature.ticket.enabled``true` に変更。
- setup wizard が生成する default profile の `[feature.ticket] enabled``true` に変更。
- builtin default profile resolution / setup profile generation のテスト期待値を追加。
- `ticket_orchestration` は default では無効のまま維持。
- `WorkerManifestConfig::builtin_defaults()` の裸の feature default は変更していない。
検証:
- `cargo test -p manifest builtin_default_resolves_without_external_evaluator --quiet`
- `cargo test -p manifest builtin_role_profiles_preserve_role_tool_policy --quiet`
- `cargo test -p tui write_default_profile_config_creates_registry_and_profile --quiet`
- `cargo test -p worker installs_ticket_tools_when_default_root_is_usable --quiet`
- `cargo fmt --check`
- `git diff --check`
---
<!-- event: state_changed author: hare at: 2026-07-18T08:32:27Z from: inprogress to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-07-18T08:32:27Z status: closed -->
## 完了
Default profile now enables Ticket lifecycle tools by default while keeping ticket orchestration disabled. Builtin default profile and setup-generated user default profile were updated, with focused tests covering the new expectations.
---

View File

@ -0,0 +1,33 @@
---
title: 'Add development process switch script'
state: 'closed'
created_at: '2026-07-18T03:17:55Z'
updated_at: '2026-07-18T08:32:27Z'
assignee: null
queued_by: 'yoi ticket'
queued_at: '2026-07-18T03:18:16Z'
---
## 背景
開発中の backend / runtime / workspace frontend は、手動で `cargo run``deno` task を main worktree から起動している。作業ブランチを切った worktree に付け替える時、既存 process を安全に止めて同じ port で起動し直す手順が必要になる。
この worker 自体が同じ開発 process 群に依存している可能性があるため、実装時には stop/restart/start の実行は行わず、静的検証に留める。
## 要件
- `scripts/dev-workspace.sh` を追加する。
- 引数で `start` / `stop` / `restart` を選べる。
- `start` は runtime / backend / frontend をこの checkout から起動する。
- `stop` は runtime / backend / frontend を停止する。
- `restart` は frontend を触らず、runtime / backend だけ停止・起動する。
- frontend は `0.0.0.0` bind で起動する。
- pid/log は repository-local な runtime directory に保存し、生成物は git 管理対象外にする。
- port listener の付け替えに対応する。
## 受け入れ条件
- `bash -n scripts/dev-workspace.sh` が通る。
- `scripts/dev-workspace.sh --help` が安全に使い方を表示する。
- start/stop/restart は実装するが、この作業中には実行しない。
- runtime/backend/frontend の command と default bind/port が script 内で確認できる。

View File

@ -0,0 +1 @@
Added `scripts/dev-workspace.sh` with start/stop/restart/status. Start moves runtime/backend/frontend to this checkout, frontend binds `0.0.0.0`, restart intentionally leaves frontend untouched. Runtime files are kept under ignored `.yoi/dev/`.

View File

@ -0,0 +1,98 @@
<!-- event: create author: "yoi ticket" at: 2026-07-18T03:17:55Z -->
## 作成
LocalTicketBackend によって作成されました。
---
<!-- event: intake_summary author: hare at: 2026-07-18T03:18:16Z -->
## Intake summary
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T03:18:16Z from: planning to: ready reason: cli_state field: state -->
## State changed
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T03:18:16Z from: ready to: queued reason: queued field: state -->
## State changed
Ticket を `yoi ticket` が queued にしました。
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T03:18:16Z from: queued to: inprogress reason: cli_state field: state -->
## State changed
State changed to `inprogress`.
---
<!-- event: implementation_report author: hare at: 2026-07-18T03:20:37Z -->
## Implementation report
`script/dev-workspace.sh` ではなく repository root の `scripts/dev-workspace.sh` として開発 process 管理 script を追加した。
実装内容:
- `start` / `stop` / `restart` / `status` action を追加。
- `start` は runtime, backend, frontend をこの checkout から起動する。
- `stop` は runtime, backend, frontend を止める。
- `restart` は frontend を触らず、runtime/backend だけ止めて起動し直す。
- frontend は既定で `0.0.0.0:5173` bind。
- backend は既定で `127.0.0.1:8787`、runtime は `127.0.0.1:38800`
- pid/log は `.yoi/dev/{pids,logs}` に保存し、`.gitignore` に `/.yoi/dev` を追加。
- 既存 listener がいる場合、port listener PID を止めてからこの checkout の process を起動する。
- managed process は pidfile の pid/process group を使って止める。pidfile 管理外の既存 listener は、caller の process group を巻き込まないよう PID 単位で止める。
確認:
- `bash -n scripts/dev-workspace.sh`
- `scripts/dev-workspace.sh --help`
- `scripts/dev-workspace.sh status`
- `git diff --check`
実行しなかったこと:
- `scripts/dev-workspace.sh start`
- `scripts/dev-workspace.sh stop`
- `scripts/dev-workspace.sh restart`
理由:
- 現在の worker 自体が既存 backend/runtime/frontend process 群に依存している可能性があり、停止・再起動するとこのセッションが落ちるため。
現状確認:
- frontend listener pid は `/home/hare/Projects/yoi/web/workspace` 由来。
- backend/runtime listener pid は `/home/hare/Projects/yoi` 由来。
---
<!-- event: state_changed author: hare at: 2026-07-18T08:32:27Z from: inprogress to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-07-18T08:32:27Z status: closed -->
## 完了
Added `scripts/dev-workspace.sh` with start/stop/restart/status. Start moves runtime/backend/frontend to this checkout, frontend binds `0.0.0.0`, restart intentionally leaves frontend untouched. Runtime files are kept under ignored `.yoi/dev/`.
---

View File

@ -0,0 +1,28 @@
---
title: 'Limit workdir dirty source check to HEAD selector'
state: 'closed'
created_at: '2026-07-18T08:28:54Z'
updated_at: '2026-07-18T08:32:27Z'
assignee: null
queued_by: 'yoi ticket'
queued_at: '2026-07-18T08:29:31Z'
---
## 背景
Runtime workdir materializer は `selector``git rev-parse <selector>^{commit}` で commit に解決し、`git worktree add --detach` で detached worktree を作る。明示 branch / tag / commit selector の場合、source worktree の未コミット変更は生成される detached worktree には入らないため、dirty source を理由に拒否する必要はない。
一方で `HEAD` selector は現在の source worktree の状態を暗黙に参照する導線なので、既存の dirty source safety check を維持する。
## 要件
- workdir materialization の dirty source check は実質 selector が `HEAD` の時だけ行う。
- 明示 branch selector では source repository が dirty でも commit 解決と detached worktree 作成を許す。
- `HEAD` selector の dirty source rejection は維持する。
## 受け入れ条件
- `selector = HEAD` で source repository が dirty の場合、`working_directory_dirty_source_rejected` になる。
- `selector = <branch>` で source repository が dirty の場合、workdir materialization が成功する。
- 明示 branch selector で作った workdir は dirty source の未コミットファイルを含まない。
- focused worker-runtime tests が通る。

View File

@ -0,0 +1 @@
Workdir dirty source rejection is now scoped to the `HEAD` selector. Explicit branch selectors can materialize detached workdirs from their resolved commit even when the source worktree has uncommitted changes.

View File

@ -0,0 +1,82 @@
<!-- event: create author: "yoi ticket" at: 2026-07-18T08:28:54Z -->
## 作成
LocalTicketBackend によって作成されました。
---
<!-- event: intake_summary author: hare at: 2026-07-18T08:29:31Z -->
## Intake summary
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T08:29:31Z from: planning to: ready reason: cli_state field: state -->
## State changed
Marked ready by `yoi ticket state`.
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T08:29:31Z from: ready to: queued reason: queued field: state -->
## State changed
Ticket を `yoi ticket` が queued にしました。
---
<!-- event: state_changed author: "yoi ticket" at: 2026-07-18T08:29:31Z from: queued to: inprogress reason: cli_state field: state -->
## State changed
State changed to `inprogress`.
---
<!-- event: implementation_report author: hare at: 2026-07-18T08:32:27Z -->
## Implementation report
workdir materialization の dirty source check を `selector == "HEAD"` の場合だけ実行するようにした。
変更点:
- `LocalGitWorktreeMaterializer::materialize` で selector を先に確定。
- selector が `HEAD` の時だけ `git status --porcelain` による dirty source rejection を行う。
- 明示 branch selector では source worktree が dirty でも `git rev-parse <selector>^{commit}` で commit 解決し、detached worktree を作る。
- branch selector + dirty source の回帰テストを追加し、未コミットファイルが materialized worktree に入らないことを確認。
検証:
- `cargo test -p worker-runtime --features ws-server,fs-store branch_selector_allows_dirty_source_materialization --quiet`
- `cargo test -p worker-runtime --features ws-server,fs-store dirty_source_is_rejected_by_materialization --quiet`
- `cargo test -p worker-runtime --features ws-server,fs-store local_git_repo_materializes_detached_worktree_under_runtime_root --quiet`
- `cargo fmt --check`
- `git diff --check`
---
<!-- event: state_changed author: hare at: 2026-07-18T08:32:27Z from: inprogress to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-07-18T08:32:27Z status: closed -->
## 完了
Workdir dirty source rejection is now scoped to the `HEAD` selector. Explicit branch selectors can materialize detached workdirs from their resolved commit even when the source worktree has uncommitted changes.
---

View File

@ -981,7 +981,7 @@ fn builtin_default_profile_artifact() -> serde_json::Value {
"memory": { "enabled": true },
"web": { "enabled": true },
"workers": { "enabled": true },
"ticket": { "enabled": false, "access": "lifecycle" },
"ticket": { "enabled": true, "access": "lifecycle" },
"ticket_orchestration": { "enabled": false }
},
"memory": {
@ -1761,6 +1761,12 @@ worker_context_max_tokens = 68000
assert!(resolved.manifest.scope.allow.is_empty());
assert!(resolved.manifest.delegation_scope.allow.is_empty());
assert!(resolved.manifest.session.record_event_trace);
assert!(resolved.manifest.feature.ticket.enabled);
assert_eq!(
resolved.manifest.feature.ticket.access,
crate::TicketFeatureAccessConfig::Lifecycle
);
assert!(!resolved.manifest.feature.ticket_orchestration.enabled);
assert_eq!(
resolved.profile.as_ref().unwrap().name.as_deref(),
Some("default")

View File

@ -237,7 +237,7 @@ enabled = true
enabled = false
[feature.ticket]
enabled = false
enabled = true
access = "lifecycle"
[feature.ticket_orchestration]
@ -298,6 +298,8 @@ mod tests {
assert!(profile.contains("slug = \"default\""));
assert!(profile.contains("ref = \"codex-oauth/gpt-5.5\""));
assert!(profile.contains("scope = \"workspace_write\""));
assert!(profile.contains("[feature.ticket]\nenabled = true\naccess = \"lifecycle\""));
assert!(profile.contains("[feature.ticket_orchestration]\nenabled = false"));
}
#[test]

View File

@ -320,20 +320,21 @@ impl LocalGitWorktreeMaterializer {
)
})?;
let status = git_stdout(&source_root, ["status", "--porcelain"])?;
if !status.trim().is_empty() {
return Err(WorkingDirectoryDiagnostic::new(
"working_directory_dirty_source_rejected",
"working directory materialization rejects dirty source repository state",
));
}
let selector = request
.repository
.selector
.as_deref()
.unwrap_or("HEAD")
.to_string();
if selector == "HEAD" {
let status = git_stdout(&source_root, ["status", "--porcelain"])?;
if !status.trim().is_empty() {
return Err(WorkingDirectoryDiagnostic::new(
"working_directory_dirty_source_rejected",
"working directory materialization rejects dirty source repository state",
));
}
}
let commit_spec = format!("{selector}^{{commit}}");
let resolved_commit = git_stdout(&source_root, ["rev-parse", commit_spec.as_str()])?
.trim()
@ -851,6 +852,30 @@ mod tests {
assert!(error.message.contains("dirty source"));
}
#[test]
fn branch_selector_allows_dirty_source_materialization() {
let repo = create_clean_repo();
git(repo.path(), &["branch", "pinned"]);
fs::write(repo.path().join("dirty.txt"), "dirty\n").unwrap();
let runtime_root = tempfile::tempdir().unwrap();
let materializer = LocalGitWorktreeMaterializer::new(runtime_root.path());
let mut request = request(repo.path());
request.repository.selector = Some(RepositorySelector::from("pinned"));
let binding = materializer.materialize(&worker_ref(1), &request).unwrap();
assert_eq!(
binding
.working_directory
.evidence
.requested_selector
.as_deref(),
Some("pinned")
);
assert!(binding.root.join("README.md").exists());
assert!(!binding.root.join("dirty.txt").exists());
}
#[test]
fn unsupported_remote_and_non_git_provider_return_typed_diagnostics() {
let runtime_root = tempfile::tempdir().unwrap();

View File

@ -26,7 +26,7 @@ feature = {
memory = { enabled = true; };
web = { enabled = true; };
workers = { enabled = true; };
ticket = { enabled = false; access = "lifecycle"; };
ticket = { enabled = true; access = "lifecycle"; };
ticket_orchestration = { enabled = false; };
};

308
scripts/dev-workspace.sh Executable file
View File

@ -0,0 +1,308 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
RUNTIME_DIR="${YOI_DEV_RUNTIME_DIR:-$ROOT_DIR/.yoi/dev}"
PID_DIR="$RUNTIME_DIR/pids"
LOG_DIR="$RUNTIME_DIR/logs"
BACKEND_LISTEN="${YOI_DEV_BACKEND_LISTEN:-127.0.0.1:8787}"
RUNTIME_BIND="${YOI_DEV_RUNTIME_BIND:-127.0.0.1:38800}"
FRONTEND_HOST="${YOI_DEV_FRONTEND_HOST:-0.0.0.0}"
FRONTEND_PORT="${YOI_DEV_FRONTEND_PORT:-5173}"
RUNTIME_ENABLED="${YOI_DEV_RUNTIME_ENABLED:-1}"
usage() {
cat <<EOF
Usage: $(basename "$0") <start|stop|restart|status>
Manage the local Yoi development stack for this checkout:
runtime cargo run -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --bind $RUNTIME_BIND
backend cargo run -p yoi-workspace-server -- serve --workspace $ROOT_DIR --db $ROOT_DIR/.yoi/workspace.db --listen $BACKEND_LISTEN
frontend deno run -A npm:vite@7.2.7 dev --host $FRONTEND_HOST --port $FRONTEND_PORT (cwd: web/workspace)
Actions:
start stop existing listeners on the configured ports, then start runtime, backend, and frontend from this checkout
stop stop runtime, backend, and frontend
restart stop/start runtime and backend only; frontend is left untouched
status print pidfile and port-listener status without mutating processes
Environment overrides:
YOI_DEV_BACKEND_LISTEN=127.0.0.1:8787
YOI_DEV_RUNTIME_BIND=127.0.0.1:38800
YOI_DEV_RUNTIME_ENABLED=1 set to 0 to skip the standalone runtime process
YOI_DEV_FRONTEND_HOST=0.0.0.0
YOI_DEV_FRONTEND_PORT=5173
YOI_DEV_RUNTIME_DIR=$ROOT_DIR/.yoi/dev
EOF
}
log() {
printf '[dev-workspace] %s\n' "$*" >&2
}
ensure_dirs() {
mkdir -p "$PID_DIR" "$LOG_DIR"
}
pid_file() {
printf '%s/%s.pid' "$PID_DIR" "$1"
}
log_file() {
printf '%s/%s.log' "$LOG_DIR" "$1"
}
is_running() {
local pid="$1"
[[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null
}
service_pid() {
local file
file="$(pid_file "$1")"
[[ -f "$file" ]] || return 1
local pid
pid="$(cat "$file")"
is_running "$pid" || return 1
printf '%s' "$pid"
}
split_addr_port() {
local value="$1"
local port="${value##*:}"
local host="${value%:*}"
if [[ "$host" == "$value" || -z "$port" ]]; then
printf 'invalid address:port value: %s\n' "$value" >&2
return 1
fi
printf '%s\t%s\n' "$host" "$port"
}
port_for_addr() {
split_addr_port "$1" | awk '{ print $2 }'
}
listener_pids_for_port() {
local port="$1"
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null | sort -u
return 0
fi
if command -v ss >/dev/null 2>&1; then
ss -ltnp "sport = :$port" 2>/dev/null \
| sed -nE 's/.*pid=([0-9]+).*/\1/p' \
| sort -u
return 0
fi
}
stop_pid() {
local pid="$1"
local label="$2"
is_running "$pid" || return 0
local pgid
pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d '[:space:]' || true)"
if [[ "$pgid" == "$pid" ]]; then
log "stopping $label process group -$pid"
kill -TERM "-$pid" 2>/dev/null || true
else
log "stopping $label pid $pid"
kill -TERM "$pid" 2>/dev/null || true
fi
for _ in {1..50}; do
is_running "$pid" || return 0
sleep 0.1
done
log "forcing $label pid $pid"
if [[ "$pgid" == "$pid" ]]; then
kill -KILL "-$pid" 2>/dev/null || true
else
kill -KILL "$pid" 2>/dev/null || true
fi
}
stop_managed_service() {
local name="$1"
local file
file="$(pid_file "$name")"
if [[ ! -f "$file" ]]; then
return 0
fi
local pid
pid="$(cat "$file")"
if is_running "$pid"; then
stop_pid "$pid" "$name"
fi
rm -f "$file"
}
stop_port_listeners() {
local label="$1"
local port="$2"
local pids=()
mapfile -t pids < <(listener_pids_for_port "$port" || true)
if [[ "${#pids[@]}" -eq 0 ]]; then
return 0
fi
for pid in "${pids[@]}"; do
[[ -n "$pid" ]] || continue
log "stopping existing $label listener on port $port pid $pid"
# Unmanaged dev processes may share a process group with the caller's terminal
# or pod; stop only the listener PID here. Managed processes started by this
# script are stopped by process group via pidfiles above.
kill -TERM "$pid" 2>/dev/null || true
done
for _ in {1..50}; do
mapfile -t pids < <(listener_pids_for_port "$port" || true)
[[ "${#pids[@]}" -eq 0 ]] && return 0
sleep 0.1
done
for pid in "${pids[@]}"; do
[[ -n "$pid" ]] || continue
log "forcing existing $label listener on port $port pid $pid"
kill -KILL "$pid" 2>/dev/null || true
done
}
start_service() {
local name="$1"
local cwd="$2"
local port="$3"
shift 3
stop_managed_service "$name"
stop_port_listeners "$name" "$port"
local logfile pidfile
logfile="$(log_file "$name")"
pidfile="$(pid_file "$name")"
log "starting $name; log: $logfile"
(
cd "$cwd"
exec setsid "$@"
) >"$logfile" 2>&1 &
local pid="$!"
printf '%s\n' "$pid" >"$pidfile"
log "$name pid $pid"
}
start_runtime() {
if [[ "$RUNTIME_ENABLED" == "0" ]]; then
log "runtime disabled by YOI_DEV_RUNTIME_ENABLED=0"
return 0
fi
local port
port="$(port_for_addr "$RUNTIME_BIND")"
start_service runtime "$ROOT_DIR" "$port" \
cargo run -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --bind "$RUNTIME_BIND"
}
start_backend() {
local port
port="$(port_for_addr "$BACKEND_LISTEN")"
start_service backend "$ROOT_DIR" "$port" \
cargo run -p yoi-workspace-server -- serve --workspace "$ROOT_DIR" --db "$ROOT_DIR/.yoi/workspace.db" --listen "$BACKEND_LISTEN"
}
start_frontend() {
local frontend_dir="$ROOT_DIR/web/workspace"
start_service frontend "$frontend_dir" "$FRONTEND_PORT" \
deno run -A npm:vite@7.2.7 dev --host "$FRONTEND_HOST" --port "$FRONTEND_PORT"
}
stop_runtime_backend() {
stop_managed_service backend
stop_managed_service runtime
stop_port_listeners backend "$(port_for_addr "$BACKEND_LISTEN")"
if [[ "$RUNTIME_ENABLED" != "0" ]]; then
stop_port_listeners runtime "$(port_for_addr "$RUNTIME_BIND")"
fi
}
start_runtime_backend() {
start_runtime
start_backend
}
start_all() {
ensure_dirs
start_runtime_backend
start_frontend
}
stop_all() {
ensure_dirs
stop_managed_service frontend
stop_runtime_backend
stop_port_listeners frontend "$FRONTEND_PORT"
}
restart_runtime_backend() {
ensure_dirs
log "restarting runtime/backend only; frontend is left untouched"
stop_runtime_backend
start_runtime_backend
}
status_service() {
local name="$1"
local port="$2"
local managed="-"
local listeners="-"
if service_pid "$name" >/dev/null; then
managed="$(service_pid "$name")"
fi
mapfile -t pids < <(listener_pids_for_port "$port" || true)
if [[ "${#pids[@]}" -gt 0 ]]; then
listeners="${pids[*]}"
fi
printf '%-8s managed_pid=%-8s port=%-6s listener_pids=%s\n' "$name" "$managed" "$port" "$listeners"
}
status_all() {
ensure_dirs
status_service runtime "$(port_for_addr "$RUNTIME_BIND")"
status_service backend "$(port_for_addr "$BACKEND_LISTEN")"
status_service frontend "$FRONTEND_PORT"
}
main() {
local action="${1:-}"
case "$action" in
start)
start_all
;;
stop)
stop_all
;;
restart)
restart_runtime_backend
;;
status)
status_all
;;
-h|--help|help)
usage
;;
"")
usage
exit 2
;;
*)
printf 'unknown action: %s\n\n' "$action" >&2
usage >&2
exit 2
;;
esac
}
main "$@"