From 603f326b67d333d6a75f5d2d840802a73f7c5766 Mon Sep 17 00:00:00 2001 From: forthfate Date: Sat, 12 Sep 2026 19:34:27 +0900 Subject: [PATCH] feat: support project-local app data --- .gitignore | 2 +- CONTRIBUTING.md | 4 +++- README.md | 21 +++++++++++++++++++-- bin/orbit.mjs | 23 +++++++++++++++++------ i18n/ko/CONTRIBUTING.md | 4 +++- i18n/ko/README.md | 11 +++++++++-- orbit/cli.py | 12 ++++++++++++ tests/test_cli.py | 31 ++++++++++++++++++++++++++++++- 8 files changed, 94 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 41106a5..f6cc555 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ frontend/test-results/ test-results/ openorbit-state.zip conversations/ -.orbit/conversations/ +.orbit/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db418aa..d457494 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,9 @@ pnpm install pnpm --filter agent-improvement-console-ui run build ``` -For the packaged local-app path, run `orbit run`. +For the packaged local-app path, run `orbit run [PATH]`. Supplying `PATH` +stores the control room's local data in `PATH/.orbit`; omit it to use the +default application-data directory. On Windows, activate the environment with `.venv\\Scripts\\Activate.ps1` and use `.venv\\Scripts\\python.exe`. diff --git a/README.md b/README.md index dd0ece4..f733308 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,15 @@ orbit run The wheel already includes the bundled control-room UI, so Node.js and pnpm are not required at runtime. +To keep one control room's operational data with a project or another chosen +directory, pass that directory to `run`. OpenOrbit creates and uses its +`.orbit` subdirectory: + +```bash +orbit run . # Store data in the current directory's .orbit/ +orbit run ./my-project # Store data in ./my-project/.orbit/ +``` + > PyPI publication is made possible with the support of insighta cloud Inc. To use the latest development version, install directly from the main OpenOrbit @@ -246,13 +255,21 @@ OpenOrbit, create assets, review runs, or use non-browser runners. ## Safety and local data -OpenOrbit is local-first. Operational state is stored outside the repository in platform AppData: +OpenOrbit is local-first. By default, operational state is stored outside the +repository in platform AppData: - Windows: `%LOCALAPPDATA%\\Orbit` - macOS: `~/Library/Application Support/Orbit` - Linux: `${XDG_DATA_HOME:-~/.local/share}/orbit` -Set `ORBIT_APP_DATA` to use another location. Model profiles store the name of the environment variable that contains a secret, never the secret itself. Review workflow commands, approved workspace boundaries, and network exposure before connecting a production AI system. +Use `orbit run PATH` to keep the data in `PATH/.orbit`; this takes precedence +over a previously selected data location and `ORBIT_APP_DATA` for that run. Add +`.orbit/` to the target project's `.gitignore` when it is not meant to be +version-controlled. Set `ORBIT_APP_DATA` to use another location without a +command-line path. Model profiles store the name of the environment variable +that contains a secret, never the secret itself. Review workflow commands, +approved workspace boundaries, and network exposure before connecting a +production AI system. ## API and extensibility diff --git a/bin/orbit.mjs b/bin/orbit.mjs index 33b58aa..4315166 100755 --- a/bin/orbit.mjs +++ b/bin/orbit.mjs @@ -1,8 +1,8 @@ #!/usr/bin/env node -import { existsSync } from 'node:fs' +import { existsSync, statSync } from 'node:fs' import net from 'node:net' import { platform } from 'node:os' -import { join } from 'node:path' +import { join, resolve } from 'node:path' import { spawn, spawnSync } from 'node:child_process' const root = new URL('..', import.meta.url).pathname @@ -32,14 +32,21 @@ async function nextAvailablePort(startPort, host) { } if (process.argv.includes('--help') || process.argv.includes('-h')) { - console.log('Usage: orbit run') + console.log('Usage: orbit run [PATH]') process.exit(0) } -if (process.argv[2] !== 'run') { - console.log('Usage: orbit run') +if (process.argv[2] !== 'run' || process.argv.length > 4) { + console.log('Usage: orbit run [PATH]') process.exit(1) } +const dataPath = process.argv[3] +const dataRoot = dataPath ? resolve(dataPath) : undefined +if (dataRoot && existsSync(dataRoot) && !statSync(dataRoot).isDirectory()) { + throw new Error(`PATH must be a directory: ${dataRoot}`) +} +const appData = dataRoot ? join(dataRoot, '.orbit') : undefined + if (!existsSync(venvPython)) { console.log('Creating Python virtual environment…') run(python, [...pythonVersionArgs, '-m', 'venv', '.venv']) @@ -66,7 +73,11 @@ if (port !== requestedPort) { if (process.env.ORBIT_PUBLIC_URL) { console.log(`Public URL: ${process.env.ORBIT_PUBLIC_URL.replace('{port}', String(port))}`) } -const server = spawn(venvPython, ['-m', 'uvicorn', 'app.main:app', '--app-dir', 'backend', '--host', host, '--port', port], { cwd: root, stdio: 'inherit' }) +const server = spawn(venvPython, ['-m', 'uvicorn', 'app.main:app', '--app-dir', 'backend', '--host', host, '--port', port], { + cwd: root, + stdio: 'inherit', + env: { ...process.env, ...(appData ? { ORBIT_APP_DATA: appData } : {}) }, +}) process.on('SIGINT', () => server.kill('SIGINT')) process.on('SIGTERM', () => server.kill('SIGTERM')) server.on('exit', code => process.exit(code ?? 0)) diff --git a/i18n/ko/CONTRIBUTING.md b/i18n/ko/CONTRIBUTING.md index b3f6c38..bff50e3 100644 --- a/i18n/ko/CONTRIBUTING.md +++ b/i18n/ko/CONTRIBUTING.md @@ -26,7 +26,9 @@ pnpm install pnpm --filter agent-improvement-console-ui run build ``` -패키지형 로컬 앱 경로로 실행하려면 `orbit run`을 사용하세요. +패키지형 로컬 앱 경로로 실행하려면 `orbit run [PATH]`를 사용하세요. +`PATH`를 전달하면 컨트롤룸의 로컬 데이터는 `PATH/.orbit`에 저장되고, +생략하면 기본 애플리케이션 데이터 디렉터리를 사용합니다. Windows에서는 `.venv\Scripts\Activate.ps1`로 환경을 활성화하고 `.venv\Scripts\python.exe`를 사용하세요. diff --git a/i18n/ko/README.md b/i18n/ko/README.md index 5e41048..b68b554 100644 --- a/i18n/ko/README.md +++ b/i18n/ko/README.md @@ -147,6 +147,13 @@ orbit run wheel에는 번들된 컨트롤룸 UI가 이미 포함되어 있으므로, 런타임에 Node.js와 pnpm이 필요하지 않습니다. +프로젝트나 원하는 디렉터리와 함께 하나의 컨트롤룸 운영 데이터를 보관하려면 `run`에 해당 디렉터리를 전달하세요. OpenOrbit는 그 안에 `.orbit` 하위 디렉터리를 만들어 사용합니다. + +```bash +orbit run . # 현재 디렉터리의 .orbit/에 저장 +orbit run ./my-project # ./my-project/.orbit/에 저장 +``` + > PyPI 배포는 insighta cloud Inc.의 지원으로 이루어집니다. 최신 개발 버전을 사용하려면 OpenOrbit 메인 저장소에서 직접 설치하세요. 이 소스 설치에는 Node.js 24+와 pnpm이 필요합니다. @@ -221,13 +228,13 @@ OpenOrbit는 독립 실행형(standalone)이며 로컬 우선(local-first) 컨 ## 안전성과 로컬 데이터 -OpenOrbit는 로컬 우선입니다. 운영 상태는 저장소 바깥, 플랫폼 AppData에 저장됩니다. +OpenOrbit는 로컬 우선입니다. 기본적으로 운영 상태는 저장소 바깥, 플랫폼 AppData에 저장됩니다. - Windows: `%LOCALAPPDATA%\Orbit` - macOS: `~/Library/Application Support/Orbit` - Linux: `${XDG_DATA_HOME:-~/.local/share}/orbit` -다른 위치를 사용하려면 `ORBIT_APP_DATA`를 설정하세요. 모델 프로필은 비밀 값이 담긴 환경 변수의 이름을 저장하며, 비밀 값 자체는 절대 저장하지 않습니다. 프로덕션 AI 시스템을 연결하기 전에 워크플로 명령, 승인된 워크스페이스 경계, 네트워크 노출을 검토하세요. +`orbit run PATH`를 사용하면 `PATH/.orbit`에 데이터를 보관합니다. 이 경로는 해당 실행에서 기존에 선택한 데이터 위치와 `ORBIT_APP_DATA`보다 우선합니다. Git으로 관리하지 않을 데이터라면 대상 프로젝트의 `.gitignore`에 `.orbit/`을 추가하세요. 명령줄 경로 없이 다른 위치를 사용하려면 `ORBIT_APP_DATA`를 설정하세요. 모델 프로필은 비밀 값이 담긴 환경 변수의 이름을 저장하며, 비밀 값 자체는 절대 저장하지 않습니다. 프로덕션 AI 시스템을 연결하기 전에 워크플로 명령, 승인된 워크스페이스 경계, 네트워크 노출을 검토하세요. ## API 및 확장성 diff --git a/orbit/cli.py b/orbit/cli.py index dc0d23f..cdfefea 100644 --- a/orbit/cli.py +++ b/orbit/cli.py @@ -10,6 +10,7 @@ import urllib.error import urllib.request import webbrowser +from pathlib import Path from typing import Any @@ -60,6 +61,11 @@ def parser() -> argparse.ArgumentParser: root.add_argument("--url", default=os.environ.get("ORBIT_URL", "http://127.0.0.1:8787")) commands = root.add_subparsers(dest="command", required=True) run = commands.add_parser("run", help="Start the local OpenOrbit web server.") + run.add_argument( + "path", + nargs="?", + help="Store OpenOrbit data in PATH/.orbit instead of the default application-data directory.", + ) run.add_argument("--host", default=os.environ.get("ORBIT_HOST", "127.0.0.1")) run.add_argument("--port", type=int, default=int(os.environ.get("ORBIT_PORT", "3000"))) run.add_argument("--reload", action="store_true", help="Reload the server when Python sources change.") @@ -122,6 +128,12 @@ def main() -> None: if not 1 <= args.port <= 65535: print("error: --port must be an integer from 1 through 65535.", file=sys.stderr) raise SystemExit(2) + if args.path: + root = Path(args.path).expanduser().resolve() + if root.exists() and not root.is_dir(): + print(f"error: PATH must be a directory: {root}", file=sys.stderr) + raise SystemExit(2) + os.environ["ORBIT_APP_DATA"] = str(root / ".orbit") if args.open: webbrowser.open(f"http://{args.host}:{args.port}") import uvicorn diff --git a/tests/test_cli.py b/tests/test_cli.py index 5941db8..a7d7def 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,8 @@ -from orbit.cli import parser +import os +import sys +from types import SimpleNamespace + +from orbit.cli import main, parser def test_cli_exposes_control_room_commands(): @@ -15,6 +19,31 @@ def test_cli_exposes_local_web_server_command(): assert args.reload is True +def test_cli_accepts_an_optional_data_directory_for_the_local_web_server(): + args = parser().parse_args(["run", "."]) + assert args.path == "." + + +def test_cli_run_path_overrides_app_data_for_the_server(monkeypatch, tmp_path): + received = {} + + def run(*args, **kwargs): + received["args"] = args + received["kwargs"] = kwargs + + monkeypatch.setattr(sys, "argv", ["orbit", "run", str(tmp_path)]) + monkeypatch.setitem(sys.modules, "uvicorn", SimpleNamespace(run=run)) + monkeypatch.setenv("ORBIT_APP_DATA", "/previous-location") + + main() + + assert received == { + "args": ("app.main:app",), + "kwargs": {"host": "127.0.0.1", "port": 3000, "reload": False}, + } + assert os.environ["ORBIT_APP_DATA"] == str(tmp_path / ".orbit") + + def test_cli_exposes_task_test_with_waiting(): args = parser().parse_args(["tasks", "test", "insighta-user-simulation", "--wait", "--timeout", "42"]) assert args.command == "tasks"