Repository navigation
dev: run checkout and production sessions in parallel #56
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| usage() { | ||
| cat <<'EOF' | ||
| Usage: scripts/relay-dev.sh [run] [claude arguments] | ||
| scripts/relay-dev.sh env | ||
| scripts/relay-dev.sh status | ||
|
|
||
| Run this checkout's relay plugin alongside an installed production release. | ||
| The development process uses its own jobs and admission homes and disables | ||
| relay@libar-relay only for that process. Normal Claude sessions are unchanged. | ||
|
|
||
| RELAY_DEV_HOME overrides the development state root (default: | ||
| ${XDG_STATE_HOME:-$HOME/.local/state}/relay-dev/<checkout-id>). | ||
| RELAY_DEV_WORKDIR optionally changes the session's working directory. | ||
| EOF | ||
| } | ||
|
|
||
| script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) | ||
| repo=$(cd -- "$script_dir/.." && pwd -P) | ||
| if [[ ${1:-} == --help || ${1:-} == -h ]]; then usage; exit 0; fi | ||
|
|
||
| command=run | ||
| if [[ $# -gt 0 ]]; then | ||
| case "$1" in | ||
| run|env|status) command=$1; shift ;; | ||
| esac | ||
| fi | ||
|
|
||
| checkout_id=$(node - "$repo" <<'NODE' | ||
| const { createHash } = require('node:crypto'); | ||
| console.log(createHash('sha256').update(process.argv[2]).digest('hex').slice(0, 16)); | ||
| NODE | ||
| ) | ||
| dev_root=${RELAY_DEV_HOME:-${XDG_STATE_HOME:-$HOME/.local/state}/relay-dev/$checkout_id} | ||
| jobs_home=$dev_root/jobs | ||
| admission_home=$dev_root/admission | ||
|
Comment on lines
+36
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If Resolve Knowledge Base Used: Runtime harness and isolation ArtifactsExecutable relative-home validation harness
Executed commands and output capture wrapper
Control output without a session directory change
Reproduction output with a changed session directory
Head revision and relevant numbered source
|
||
| plugin_dir=$repo/plugins/relay | ||
| settings='{"enabledPlugins":{"relay@libar-relay":false}}' | ||
|
|
||
| if [[ ! -f $plugin_dir/.claude-plugin/plugin.json ]]; then | ||
| printf 'Not a relay checkout: %s\n' "$repo" >&2 | ||
| exit 2 | ||
| fi | ||
| mkdir -p "$jobs_home" "$admission_home" | ||
| export RELAY_JOBS_HOME=$jobs_home | ||
| export RELAY_ADMISSION_HOME=$admission_home | ||
|
|
||
| case "$command" in | ||
| env) | ||
| printf 'export RELAY_JOBS_HOME=%q\n' "$RELAY_JOBS_HOME" | ||
| printf 'export RELAY_ADMISSION_HOME=%q\n' "$RELAY_ADMISSION_HOME" | ||
| printf 'export RELAY_DEV_PLUGIN_DIR=%q\n' "$plugin_dir" | ||
| printf 'export RELAY_DEV_SETTINGS=%q\n' "$settings" | ||
| ;; | ||
| status) | ||
| printf 'Development checkout: %s\n' "$repo" | ||
| printf 'Development state: %s\n' "$dev_root" | ||
| node "$plugin_dir/scripts/companion.mjs" status --all | ||
| ;; | ||
| run) | ||
| if [[ -n ${RELAY_DEV_WORKDIR:-} ]]; then cd -- "$RELAY_DEV_WORKDIR"; fi | ||
| exec claude --plugin-dir "$plugin_dir" --settings "$settings" "$@" | ||
| ;; | ||
| esac | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import test from 'node:test'; | ||
| import assert from 'node:assert/strict'; | ||
| import fs from 'node:fs'; | ||
| import os from 'node:os'; | ||
| import path from 'node:path'; | ||
| import { fileURLToPath } from 'node:url'; | ||
| import { spawnSync } from 'node:child_process'; | ||
|
|
||
| const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); | ||
| const script = path.join(root, 'scripts/relay-dev.sh'); | ||
|
|
||
| function fixture(t) { | ||
| const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'relay-dev-session-')); | ||
| t.after(() => fs.rmSync(dir, { recursive: true, force: true })); | ||
| const bin = path.join(dir, 'bin'); | ||
| fs.mkdirSync(bin); | ||
| const capture = path.join(dir, 'claude.json'); | ||
| fs.writeFileSync(path.join(bin, 'claude'), `#!${process.execPath}\n` + | ||
| 'const fs = require("node:fs");\n' + | ||
| 'fs.writeFileSync(process.env.FAKE_CLAUDE_CAPTURE, JSON.stringify({ args: process.argv.slice(2), cwd: process.cwd(), jobs: process.env.RELAY_JOBS_HOME, admission: process.env.RELAY_ADMISSION_HOME }));\n', | ||
| { mode: 0o755 }); | ||
| return { dir, capture, env: { | ||
| ...process.env, | ||
| HOME: path.join(dir, 'home'), | ||
|
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a developer exports This is a non-blocking test-isolation concern that causes misleading local failures and leftover state. Clear both overrides in the fixture, then set them explicitly only in the override test. Knowledge Base Used: Runtime harness and isolation ArtifactsExecuted fixture isolation validation source
Launcher tests with both development overrides absent
Launcher tests with each absolute development override exported separately
Execution record for fixture isolation validation
|
||
| XDG_STATE_HOME: path.join(dir, 'state with spaces'), | ||
| PATH: `${bin}${path.delimiter}${process.env.PATH}`, | ||
| FAKE_CLAUDE_CAPTURE: capture, | ||
| RELAY_JOBS_HOME: '/production/jobs', | ||
| RELAY_ADMISSION_HOME: '/production/admission' | ||
| } }; | ||
| } | ||
|
|
||
| test('help does not invoke Claude or create development state', t => { | ||
| const { dir, env } = fixture(t); | ||
| const result = spawnSync('bash', [script, '--help'], { env, encoding: 'utf8' }); | ||
| assert.equal(result.status, 0, result.stderr); | ||
| assert.match(result.stdout, /Usage:/); | ||
| assert.equal(fs.existsSync(path.join(dir, 'state with spaces')), false); | ||
| }); | ||
|
|
||
| test('run loads only the checkout plugin with isolated persistent state and preserves arguments', t => { | ||
| const { dir, capture, env } = fixture(t); | ||
| const args = ['--continue', '--debug-file', 'debug with spaces.log']; | ||
| const first = spawnSync('bash', [script, ...args], { env, encoding: 'utf8' }); | ||
| assert.equal(first.status, 0, first.stderr); | ||
| const call = JSON.parse(fs.readFileSync(capture, 'utf8')); | ||
| const state = call.jobs.replace(/\/jobs$/, ''); | ||
| assert.notEqual(call.jobs, '/production/jobs'); | ||
| assert.notEqual(call.admission, '/production/admission'); | ||
| assert.equal(call.admission, path.join(state, 'admission')); | ||
| assert.ok(state.includes('state with spaces')); | ||
| assert.deepEqual(call.args, [ | ||
| '--plugin-dir', path.join(root, 'plugins/relay'), | ||
| '--settings', '{"enabledPlugins":{"relay@libar-relay":false}}', | ||
| ...args | ||
| ]); | ||
| assert.equal(call.cwd, root); | ||
| assert.equal(fs.existsSync(call.jobs), true); | ||
| assert.equal(fs.existsSync(call.admission), true); | ||
|
|
||
| const second = spawnSync('bash', [script], { env, encoding: 'utf8' }); | ||
| assert.equal(second.status, 0, second.stderr); | ||
| assert.equal(JSON.parse(fs.readFileSync(capture, 'utf8')).jobs, call.jobs, 'the checkout keeps one development home'); | ||
| }); | ||
|
|
||
| test('env reports only development paths and status uses the development home', t => { | ||
| const { capture, env } = fixture(t); | ||
| const printed = spawnSync('bash', [script, 'env'], { env, encoding: 'utf8' }); | ||
| assert.equal(printed.status, 0, printed.stderr); | ||
| assert.match(printed.stdout, /export RELAY_JOBS_HOME=/); | ||
| assert.match(printed.stdout, /export RELAY_ADMISSION_HOME=/); | ||
| assert.match(printed.stdout, /export RELAY_DEV_PLUGIN_DIR=/); | ||
| assert.doesNotMatch(printed.stdout, /\/production\//); | ||
|
|
||
| const status = spawnSync('bash', [script, 'status'], { env, encoding: 'utf8' }); | ||
| assert.equal(status.status, 0, status.stderr); | ||
| assert.match(status.stdout, /Development checkout:/); | ||
| assert.doesNotMatch(status.stdout, /\/production\//); | ||
| assert.equal(fs.existsSync(capture), false, 'status does not invoke Claude'); | ||
| }); | ||
|
|
||
| test('RELAY_DEV_HOME and RELAY_DEV_WORKDIR are honored without changing production settings', t => { | ||
| const { dir, capture, env } = fixture(t); | ||
| const work = path.join(dir, 'work with spaces'); | ||
| fs.mkdirSync(work); | ||
| const custom = path.join(dir, 'custom dev home'); | ||
| const result = spawnSync('bash', [script], { env: { ...env, RELAY_DEV_HOME: custom, RELAY_DEV_WORKDIR: work }, encoding: 'utf8' }); | ||
| assert.equal(result.status, 0, result.stderr); | ||
| const call = JSON.parse(fs.readFileSync(capture, 'utf8')); | ||
| assert.equal(call.jobs, path.join(custom, 'jobs')); | ||
| assert.equal(call.admission, path.join(custom, 'admission')); | ||
| assert.equal(call.cwd, work); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Resolve the development home before changing directories.
If
RELAY_DEV_HOMEis relative andRELAY_DEV_WORKDIRchanges directories,mkdircreates state under the launch directory, but the plugin resolves the exported paths under the session directory. A laterstatuscall then inspects a different jobs home and cannot show those jobs. Convertdev_rootto an absolute path before exporting either home.