dev: enable default ticket tools and workdir helpers
This commit is contained in:
parent
60410f29a1
commit
5e91f98ae0
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,6 +1,7 @@
|
|||
/target
|
||||
/result
|
||||
/.direnv
|
||||
/.yoi/dev
|
||||
.worktree
|
||||
*.local*
|
||||
.env
|
||||
|
|
|
|||
0
.yoi/tickets/00001KXSJXMR5/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KXSJXMR5/artifacts/.gitkeep
Normal file
31
.yoi/tickets/00001KXSJXMR5/item.md
Normal file
31
.yoi/tickets/00001KXSJXMR5/item.md
Normal 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 が通る。
|
||||
1
.yoi/tickets/00001KXSJXMR5/resolution.md
Normal file
1
.yoi/tickets/00001KXSJXMR5/resolution.md
Normal 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.
|
||||
85
.yoi/tickets/00001KXSJXMR5/thread.md
Normal file
85
.yoi/tickets/00001KXSJXMR5/thread.md
Normal 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.
|
||||
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KXSKP1AB/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KXSKP1AB/artifacts/.gitkeep
Normal file
33
.yoi/tickets/00001KXSKP1AB/item.md
Normal file
33
.yoi/tickets/00001KXSKP1AB/item.md
Normal 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 内で確認できる。
|
||||
1
.yoi/tickets/00001KXSKP1AB/resolution.md
Normal file
1
.yoi/tickets/00001KXSKP1AB/resolution.md
Normal 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/`.
|
||||
98
.yoi/tickets/00001KXSKP1AB/thread.md
Normal file
98
.yoi/tickets/00001KXSKP1AB/thread.md
Normal 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/`.
|
||||
|
||||
|
||||
---
|
||||
0
.yoi/tickets/00001KXT5FEFR/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KXT5FEFR/artifacts/.gitkeep
Normal file
28
.yoi/tickets/00001KXT5FEFR/item.md
Normal file
28
.yoi/tickets/00001KXT5FEFR/item.md
Normal 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 が通る。
|
||||
1
.yoi/tickets/00001KXT5FEFR/resolution.md
Normal file
1
.yoi/tickets/00001KXT5FEFR/resolution.md
Normal 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.
|
||||
82
.yoi/tickets/00001KXT5FEFR/thread.md
Normal file
82
.yoi/tickets/00001KXT5FEFR/thread.md
Normal 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.
|
||||
|
||||
|
||||
---
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
308
scripts/dev-workspace.sh
Executable 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 "$@"
|
||||
Loading…
Reference in New Issue
Block a user