Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-cli-symlink-resolution.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@archastro/redline": patch
---

Fix symlink resolution in redline-pull, redline-watch, and redline-tail CLI scripts
10 changes: 9 additions & 1 deletion runtime/bin/redline-pull
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ set -o pipefail

PORT="${REDLINE_PORT:-7878}"
BASE="http://127.0.0.1:$PORT"
HERE="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$0"
while [ -L "$SCRIPT" ]; do
link="$(readlink "$SCRIPT")"
case "$link" in
/*) SCRIPT="$link" ;;
*) SCRIPT="$(cd "$(dirname "$SCRIPT")" && pwd)/$link" ;;
esac
done
HERE="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
HTTP_CLIENT="$HERE/lib/cli-http.js"
ACK=1
ORIGIN=""
Expand Down
10 changes: 9 additions & 1 deletion runtime/bin/redline-tail
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,13 @@
set -e
set -o pipefail
PORT="${REDLINE_PORT:-7878}"
HERE="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$0"
while [ -L "$SCRIPT" ]; do
link="$(readlink "$SCRIPT")"
case "$link" in
/*) SCRIPT="$link" ;;
*) SCRIPT="$(cd "$(dirname "$SCRIPT")" && pwd)/$link" ;;
esac
done
HERE="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
node "$HERE/lib/cli-http.js" "$PORT" GET "/redlines" | (command -v jq >/dev/null && jq . || cat)
10 changes: 9 additions & 1 deletion runtime/bin/redline-watch
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@ set -o pipefail

PORT="${REDLINE_PORT:-7878}"
BASE="http://127.0.0.1:$PORT"
HERE="$(cd "$(dirname "$0")/.." && pwd)"
SCRIPT="$0"
while [ -L "$SCRIPT" ]; do
link="$(readlink "$SCRIPT")"
case "$link" in
/*) SCRIPT="$link" ;;
*) SCRIPT="$(cd "$(dirname "$SCRIPT")" && pwd)/$link" ;;
esac
done
HERE="$(cd "$(dirname "$SCRIPT")/.." && pwd)"
HTTP_CLIENT="$HERE/lib/cli-http.js"
INTERVAL=3
ORIGIN=""
Expand Down
32 changes: 21 additions & 11 deletions runtime/lib/sidecar-lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,29 @@ function removeLifecycleFile(file, label, expected) {
}

function readPrivateIdentity(file, label) {
const inspected = inspectLifecycleFile(file, label);
if (!inspected) return null;
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
try {
const opened = fs.fstatSync(fd);
if (opened.nlink !== 1 || opened.dev !== inspected.before.dev || opened.ino !== inspected.before.ino) {
throw new Error(`${label} changed before permission repair`);
for (let attempt = 0; attempt < 20; attempt += 1) {
try {
const inspected = inspectLifecycleFile(file, label);
if (!inspected) return null;
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
try {
const opened = fs.fstatSync(fd);
if (opened.nlink !== 1 || opened.dev !== inspected.before.dev || opened.ino !== inspected.before.ino) {
throw new Error(`${label} changed before permission repair`);
}
fs.fchmodSync(fd, 0o600);
} finally {
fs.closeSync(fd);
}
return inspected.contents;
} catch (error) {
if (attempt < 19 && (error.message.includes('has multiple links') || error.message.includes('changed before permission repair') || error.message.includes('changed while it was being opened'))) {
sleepSync(10);
continue;
}
throw error;
}
fs.fchmodSync(fd, 0o600);
} finally {
fs.closeSync(fd);
}
return inspected.contents;
}

function publishPrivateFile(file, contents) {
Expand Down
44 changes: 44 additions & 0 deletions tests/cli-auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,3 +240,47 @@ test('redline skill uses the authenticated screenshot command instead of curl',
assert.match(skill, /redline-screenshot <screenshot_id> <output\.png>/);
assert.doesNotMatch(skill, /curl <url> -o/);
});

test('runtime bin scripts resolve module paths when invoked via symlinks', async (t) => {
const context = await startSidecar(t);
const symlinkBinDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redline-symlink-bin-'));
t.after(() => fs.rmSync(symlinkBinDir, { recursive: true, force: true }));

const scripts = ['redline-pull', 'redline-tail', 'redline-sidecar', 'redline-watch', 'redline-clear', 'redline-screenshot'];
for (const script of scripts) {
fs.symlinkSync(path.join(ROOT, 'runtime/bin', script), path.join(symlinkBinDir, script));
}

const pull = spawnSync(path.join(symlinkBinDir, 'redline-pull'), ['--no-ack'], {
cwd: symlinkBinDir,
env: { ...process.env, REDLINE_DIR: context.dir, REDLINE_PORT: String(context.port) },
encoding: 'utf8',
timeout: 3000,
});
assert.equal(pull.status, 0, pull.stderr || pull.stdout);

const tail = spawnSync(path.join(symlinkBinDir, 'redline-tail'), [], {
cwd: symlinkBinDir,
env: { ...process.env, REDLINE_DIR: context.dir, REDLINE_PORT: String(context.port) },
encoding: 'utf8',
timeout: 3000,
});
assert.equal(tail.status, 0, tail.stderr || tail.stdout);

const clear = spawnSync(path.join(symlinkBinDir, 'redline-clear'), [], {
cwd: symlinkBinDir,
env: { ...process.env, REDLINE_DIR: context.dir, REDLINE_PORT: String(context.port) },
encoding: 'utf8',
timeout: 3000,
});
assert.equal(clear.status, 0, clear.stderr || clear.stdout);

const sidecarHelp = spawnSync(path.join(symlinkBinDir, 'redline-sidecar'), ['--help'], {
cwd: symlinkBinDir,
env: { ...process.env, REDLINE_DIR: context.dir, REDLINE_PORT: String(context.port) },
encoding: 'utf8',
timeout: 3000,
});
assert.equal(sidecarHelp.status, 0, sidecarHelp.stderr || sidecarHelp.stdout);
assert.match(sidecarHelp.stdout, /usage:\s*redline-sidecar/i);
});
Loading