From 379abc69f2f830391b26c422bf9a5c070ce62f1f Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 11 May 2026 00:13:38 +0200 Subject: [PATCH] Add Conductor workspace scripts --- conductor.json | 7 ++ scripts/conductor-run.sh | 98 +++++++++++++++++++++++++ scripts/conductor-scripts.test.mjs | 40 +++++++++++ scripts/conductor-setup.sh | 110 +++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+) create mode 100644 conductor.json create mode 100755 scripts/conductor-run.sh create mode 100644 scripts/conductor-scripts.test.mjs create mode 100755 scripts/conductor-setup.sh diff --git a/conductor.json b/conductor.json new file mode 100644 index 00000000..e1a79ff9 --- /dev/null +++ b/conductor.json @@ -0,0 +1,7 @@ +{ + "scripts": { + "setup": "bash scripts/conductor-setup.sh", + "run": "bash scripts/conductor-run.sh" + }, + "runScriptMode": "nonconcurrent" +} diff --git a/scripts/conductor-run.sh b/scripts/conductor-run.sh new file mode 100755 index 00000000..33b40b20 --- /dev/null +++ b/scripts/conductor-run.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# conductor-run.sh - Starts the long-lived local KTX daemon for Conductor. +# +# Uses a fixed port because Conductor runs this workspace in nonconcurrent mode. + +set -e +set -o pipefail + +read_required_uv_version() { + local project_file="$1" + + if [ ! -f "$project_file" ]; then + return 1 + fi + + sed -nE 's/^[[:space:]]*required-version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$project_file" | head -n 1 +} + +uv_version() { + local uv_bin="$1" + + "$uv_bin" --version 2>/dev/null | awk '{print $2}' +} + +install_workspace_uv() { + local required_version="$1" + local install_dir="$PWD/.context/bin/uv-$required_version" + + mkdir -p "$install_dir" + + if [ ! -x "$install_dir/uv" ] || [ "$(uv_version "$install_dir/uv")" != "$required_version" ]; then + echo "Installing workspace-local uv $required_version..." >&2 + curl -LsSf "https://astral.sh/uv/$required_version/install.sh" | + env UV_INSTALL_DIR="$install_dir" UV_NO_MODIFY_PATH=1 sh >&2 + fi + + printf '%s\n' "$install_dir/uv" +} + +resolve_uv_for_project() { + local project_file="$1" + local required_version + local system_uv + local system_version + local workspace_uv + + required_version="$(read_required_uv_version "$project_file" || true)" + required_version="${required_version#==}" + + if [ -z "$required_version" ]; then + command -v uv + return + fi + + if ! [[ "$required_version" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "WARNING: Unsupported uv required-version '$required_version'; using uv from PATH." >&2 + command -v uv + return + fi + + if command -v uv >/dev/null 2>&1; then + system_uv="$(command -v uv)" + system_version="$(uv_version "$system_uv")" + + if [ "$system_version" = "$required_version" ]; then + printf '%s\n' "$system_uv" + return + fi + + echo "Found uv $system_version at $system_uv; $project_file requires uv $required_version." >&2 + else + echo "uv is not installed on PATH; $project_file requires uv $required_version." >&2 + fi + + workspace_uv="$(install_workspace_uv "$required_version")" + + if [ "$(uv_version "$workspace_uv")" != "$required_version" ]; then + echo "ERROR: Expected uv $required_version at $workspace_uv, got $("$workspace_uv" --version 2>&1 || true)." >&2 + return 1 + fi + + printf '%s\n' "$workspace_uv" +} + +echo "=== Starting KTX for Conductor ===" + +echo "Building KTX packages..." +pnpm run build + +KTX_UV_BIN="$(resolve_uv_for_project "pyproject.toml")" +export PATH="$(dirname "$KTX_UV_BIN"):$PATH" + +if [ -f ".venv/bin/activate" ]; then + source .venv/bin/activate +fi + +echo "KTX daemon: http://127.0.0.1:8765" +exec uv run ktx-daemon serve-http --host 127.0.0.1 --port 8765 diff --git a/scripts/conductor-scripts.test.mjs b/scripts/conductor-scripts.test.mjs new file mode 100644 index 00000000..be9c64c2 --- /dev/null +++ b/scripts/conductor-scripts.test.mjs @@ -0,0 +1,40 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { describe, it } from 'node:test'; + +async function readText(relativePath) { + return readFile(new URL(`../${relativePath}`, import.meta.url), 'utf8'); +} + +describe('Conductor workspace scripts', () => { + it('registers setup and run scripts in nonconcurrent mode', async () => { + const manifest = JSON.parse(await readText('conductor.json')); + + assert.deepEqual(manifest.scripts, { + setup: 'bash scripts/conductor-setup.sh', + run: 'bash scripts/conductor-run.sh', + }); + assert.equal(manifest.runScriptMode, 'nonconcurrent'); + }); + + it('sets up exact uv, Python packages, JS packages, and the built CLI', async () => { + const setupScript = await readText('scripts/conductor-setup.sh'); + + assert.match(setupScript, /read_required_uv_version\(\)/); + assert.match(setupScript, /\.context\/bin\/uv-\$required_version/); + assert.match(setupScript, /uv sync --all-packages --all-groups/); + assert.match(setupScript, /pnpm install --frozen-lockfile --prefer-offline/); + assert.match(setupScript, /pnpm run native:rebuild/); + assert.match(setupScript, /pnpm run build/); + assert.match(setupScript, /packages\/cli\/dist\/bin\.js dev doctor setup --no-input/); + }); + + it('runs the KTX daemon on the documented fixed local port', async () => { + const runScript = await readText('scripts/conductor-run.sh'); + + assert.match(runScript, /pnpm run build/); + assert.match(runScript, /source \.venv\/bin\/activate/); + assert.match(runScript, /uv run ktx-daemon serve-http --host 127\.0\.0\.1 --port 8765/); + assert.doesNotMatch(runScript, /frontend|@kaelio\/server|python-service|npx/); + }); +}); diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh new file mode 100755 index 00000000..729b03b0 --- /dev/null +++ b/scripts/conductor-setup.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# conductor-setup.sh - Runs once when Conductor creates a KTX workspace. +# +# Prepares the standalone pnpm + uv workspace and builds the local CLI. + +set -e +set -o pipefail + +read_required_uv_version() { + local project_file="$1" + + if [ ! -f "$project_file" ]; then + return 1 + fi + + sed -nE 's/^[[:space:]]*required-version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$project_file" | head -n 1 +} + +uv_version() { + local uv_bin="$1" + + "$uv_bin" --version 2>/dev/null | awk '{print $2}' +} + +install_workspace_uv() { + local required_version="$1" + local install_dir="$PWD/.context/bin/uv-$required_version" + + mkdir -p "$install_dir" + + if [ ! -x "$install_dir/uv" ] || [ "$(uv_version "$install_dir/uv")" != "$required_version" ]; then + echo "Installing workspace-local uv $required_version..." >&2 + curl -LsSf "https://astral.sh/uv/$required_version/install.sh" | + env UV_INSTALL_DIR="$install_dir" UV_NO_MODIFY_PATH=1 sh >&2 + fi + + printf '%s\n' "$install_dir/uv" +} + +resolve_uv_for_project() { + local project_file="$1" + local required_version + local system_uv + local system_version + local workspace_uv + + required_version="$(read_required_uv_version "$project_file" || true)" + required_version="${required_version#==}" + + if [ -z "$required_version" ]; then + command -v uv + return + fi + + if ! [[ "$required_version" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + echo "WARNING: Unsupported uv required-version '$required_version'; using uv from PATH." >&2 + command -v uv + return + fi + + if command -v uv >/dev/null 2>&1; then + system_uv="$(command -v uv)" + system_version="$(uv_version "$system_uv")" + + if [ "$system_version" = "$required_version" ]; then + printf '%s\n' "$system_uv" + return + fi + + echo "Found uv $system_version at $system_uv; $project_file requires uv $required_version." >&2 + else + echo "uv is not installed on PATH; $project_file requires uv $required_version." >&2 + fi + + workspace_uv="$(install_workspace_uv "$required_version")" + + if [ "$(uv_version "$workspace_uv")" != "$required_version" ]; then + echo "ERROR: Expected uv $required_version at $workspace_uv, got $("$workspace_uv" --version 2>&1 || true)." >&2 + return 1 + fi + + printf '%s\n' "$workspace_uv" +} + +echo "=== Conductor KTX workspace setup ===" + +if [ -n "${CONDUCTOR_ROOT_PATH:-}" ] && [ -f "$CONDUCTOR_ROOT_PATH/.env" ]; then + ln -sf "$CONDUCTOR_ROOT_PATH/.env" .env + echo "Linked .env" +fi + +KTX_UV_BIN="$(resolve_uv_for_project "pyproject.toml")" +export PATH="$(dirname "$KTX_UV_BIN"):$PATH" + +echo "Installing KTX Python dependencies..." +uv sync --all-packages --all-groups + +echo "Installing KTX JS dependencies..." +pnpm install --frozen-lockfile --prefer-offline + +echo "Rebuilding native JS dependencies..." +pnpm run native:rebuild + +echo "Building KTX packages..." +pnpm run build + +echo "Running KTX setup doctor..." +node packages/cli/dist/bin.js dev doctor setup --no-input + +echo "=== Setup complete ==="