diff --git a/scripts/conductor-scripts.test.mjs b/scripts/conductor-scripts.test.mjs index 77ab9c4d..ba0ac88e 100644 --- a/scripts/conductor-scripts.test.mjs +++ b/scripts/conductor-scripts.test.mjs @@ -17,22 +17,45 @@ describe('Conductor workspace scripts', () => { assert.equal(manifest.runScriptMode, 'nonconcurrent'); }); - it('sets up exact uv, Python packages, JS packages, and the built CLI', async () => { + it('orchestrates setup through focused step scripts', 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/); - assert.match(setupScript, /link_agent_overlays/); - assert.match(setupScript, /sh scripts\/link-agent-overlays\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/link-agent-overlays\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/link-root-env-file\.sh/); + assert.match(setupScript, /source scripts\/conductor\/activate-workspace-uv\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/install-python-dependencies\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/install-js-dependencies\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/rebuild-native-dependencies\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/build-workspace\.sh/); + assert.match(setupScript, /sh scripts\/conductor\/run-setup-doctor\.sh/); + assert.doesNotMatch(setupScript, /read_required_uv_version\(\)/); + assert.doesNotMatch(setupScript, /uv sync --all-packages --all-groups/); + assert.doesNotMatch(setupScript, /pnpm install --frozen-lockfile --prefer-offline/); + assert.doesNotMatch(setupScript, /packages\/cli\/dist\/bin\.js dev doctor setup --no-input/); + }); + + it('keeps concrete setup commands in step scripts', async () => { + const resolveUvScript = await readText('scripts/conductor/resolve-uv.sh'); + const activateUvScript = await readText('scripts/conductor/activate-workspace-uv.sh'); + const pythonScript = await readText('scripts/conductor/install-python-dependencies.sh'); + const jsScript = await readText('scripts/conductor/install-js-dependencies.sh'); + const nativeScript = await readText('scripts/conductor/rebuild-native-dependencies.sh'); + const buildScript = await readText('scripts/conductor/build-workspace.sh'); + const doctorScript = await readText('scripts/conductor/run-setup-doctor.sh'); + + assert.match(resolveUvScript, /read_required_uv_version\(\)/); + assert.match(resolveUvScript, /\.context\/bin\/uv-\$required_version/); + assert.match(activateUvScript, /bash scripts\/conductor\/resolve-uv\.sh pyproject\.toml/); + assert.match(activateUvScript, /export PATH="\$\(dirname "\$KTX_UV_BIN"\):\$PATH"/); + assert.match(pythonScript, /uv sync --all-packages --all-groups/); + assert.match(jsScript, /pnpm install --frozen-lockfile --prefer-offline/); + assert.match(nativeScript, /pnpm run native:rebuild/); + assert.match(buildScript, /pnpm run build/); + assert.match(doctorScript, /packages\/cli\/dist\/bin\.js dev doctor setup --no-input/); }); it('links private agent overlays when KAELIO_SKILLS_ROOT is set', async () => { - const workspaceScript = await readText('scripts/link-agent-overlays.sh'); + const workspaceScript = await readText('scripts/conductor/link-agent-overlays.sh'); assert.match(workspaceScript, /KAELIO_SKILLS_ROOT/); assert.match(workspaceScript, /agents_source="\$\{KAELIO_SKILLS_ROOT\}\/\.agents"/); diff --git a/scripts/conductor-setup.sh b/scripts/conductor-setup.sh index 5cba7a82..8837a9ab 100755 --- a/scripts/conductor-setup.sh +++ b/scripts/conductor-setup.sh @@ -1,138 +1,20 @@ #!/bin/bash # conductor-setup.sh - Runs once when Conductor creates a KTX workspace. # -# Prepares the standalone pnpm + uv workspace and builds the local CLI. +# Orchestrates workspace setup. Step implementation lives in scripts/conductor/. set -e set -o pipefail -read_required_uv_version() { - local project_file="$1" +echo "=== Conductor KTX workspace setup ===" - if [ ! -f "$project_file" ]; then - return 1 - fi +sh scripts/conductor/link-agent-overlays.sh +sh scripts/conductor/link-root-env-file.sh +source scripts/conductor/activate-workspace-uv.sh +sh scripts/conductor/install-python-dependencies.sh +sh scripts/conductor/install-js-dependencies.sh +sh scripts/conductor/rebuild-native-dependencies.sh +sh scripts/conductor/build-workspace.sh +sh scripts/conductor/run-setup-doctor.sh - 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" -} - -link_agent_overlays() { - sh scripts/link-agent-overlays.sh -} - -link_root_env_file() { - if [ -n "${CONDUCTOR_ROOT_PATH:-}" ] && [ -f "$CONDUCTOR_ROOT_PATH/.env" ]; then - ln -sf "$CONDUCTOR_ROOT_PATH/.env" .env - echo "Linked .env" - fi -} - -install_python_dependencies() { - 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 -} - -install_js_dependencies() { - echo "Installing KTX JS dependencies..." - pnpm install --frozen-lockfile --prefer-offline -} - -rebuild_native_dependencies() { - echo "Rebuilding native JS dependencies..." - pnpm run native:rebuild -} - -build_workspace() { - echo "Building KTX packages..." - pnpm run build -} - -run_setup_doctor() { - echo "Running KTX setup doctor..." - node packages/cli/dist/bin.js dev doctor setup --no-input -} - -main() { - echo "=== Conductor KTX workspace setup ===" - - link_agent_overlays - link_root_env_file - install_python_dependencies - install_js_dependencies - rebuild_native_dependencies - build_workspace - run_setup_doctor - - echo "=== Setup complete ===" -} - -main "$@" +echo "=== Setup complete ===" diff --git a/scripts/conductor/activate-workspace-uv.sh b/scripts/conductor/activate-workspace-uv.sh new file mode 100755 index 00000000..306e0eb7 --- /dev/null +++ b/scripts/conductor/activate-workspace-uv.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e +set -o pipefail + +KTX_UV_BIN="$(bash scripts/conductor/resolve-uv.sh pyproject.toml)" +export PATH="$(dirname "$KTX_UV_BIN"):$PATH" diff --git a/scripts/conductor/build-workspace.sh b/scripts/conductor/build-workspace.sh new file mode 100755 index 00000000..06ce97db --- /dev/null +++ b/scripts/conductor/build-workspace.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +echo "Building KTX packages..." +pnpm run build diff --git a/scripts/conductor/install-js-dependencies.sh b/scripts/conductor/install-js-dependencies.sh new file mode 100755 index 00000000..0cee55f1 --- /dev/null +++ b/scripts/conductor/install-js-dependencies.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +echo "Installing KTX JS dependencies..." +pnpm install --frozen-lockfile --prefer-offline diff --git a/scripts/conductor/install-python-dependencies.sh b/scripts/conductor/install-python-dependencies.sh new file mode 100755 index 00000000..aa297dac --- /dev/null +++ b/scripts/conductor/install-python-dependencies.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +echo "Installing KTX Python dependencies..." +uv sync --all-packages --all-groups diff --git a/scripts/link-agent-overlays.sh b/scripts/conductor/link-agent-overlays.sh similarity index 100% rename from scripts/link-agent-overlays.sh rename to scripts/conductor/link-agent-overlays.sh diff --git a/scripts/conductor/link-root-env-file.sh b/scripts/conductor/link-root-env-file.sh new file mode 100755 index 00000000..27f6f2dc --- /dev/null +++ b/scripts/conductor/link-root-env-file.sh @@ -0,0 +1,7 @@ +#!/bin/sh +set -eu + +if [ -n "${CONDUCTOR_ROOT_PATH:-}" ] && [ -f "$CONDUCTOR_ROOT_PATH/.env" ]; then + ln -sf "$CONDUCTOR_ROOT_PATH/.env" .env + echo "Linked .env" +fi diff --git a/scripts/conductor/rebuild-native-dependencies.sh b/scripts/conductor/rebuild-native-dependencies.sh new file mode 100755 index 00000000..7a5651ef --- /dev/null +++ b/scripts/conductor/rebuild-native-dependencies.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +echo "Rebuilding native JS dependencies..." +pnpm run native:rebuild diff --git a/scripts/conductor/resolve-uv.sh b/scripts/conductor/resolve-uv.sh new file mode 100755 index 00000000..9bb70815 --- /dev/null +++ b/scripts/conductor/resolve-uv.sh @@ -0,0 +1,81 @@ +#!/bin/bash +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" +} + +resolve_uv_for_project "${1:-pyproject.toml}" diff --git a/scripts/conductor/run-setup-doctor.sh b/scripts/conductor/run-setup-doctor.sh new file mode 100755 index 00000000..285aab00 --- /dev/null +++ b/scripts/conductor/run-setup-doctor.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -eu + +echo "Running KTX setup doctor..." +node packages/cli/dist/bin.js dev doctor setup --no-input