Skip to content

Commit b249327

Browse files
committed
fix: isolate install and uninstall entrypoints
1 parent 3b3a5be commit b249327

7 files changed

Lines changed: 54 additions & 46 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ $ErrorActionPreference='Stop'; & ([scriptblock]::Create((Invoke-WebRequest https
3333

3434
- 변경되지 않는 버전별 설치기: `https://install.ctxa.ai/<installer-tag>/install.sh` 또는 `install.ps1`
3535
- 고정 CLI 릴리스 설치: `CONTEXA_VERSION=<release-tag>`
36-
- 직전 정상 바이너리로 롤백: `CONTEXA_INSTALL_ACTION=rollback`
36+
- 직전 정상 바이너리로 롤백: 내려받은 설치 파일을 `install.ps1 -Action rollback` 또는 `install.sh rollback`으로 실행
3737
- Windows CLI 바이너리 제거: `irm https://install.ctxa.ai/uninstall.ps1 | iex`
3838
- Linux/macOS CLI 바이너리 제거: `curl -fsSL https://install.ctxa.ai/uninstall.sh | sh`
3939
- 사용자 프로젝트 원복: `contexa reset` — CLI 제거와 다른 작업입니다.

install.ps1

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#Requires -Version 5.1
22

3+
param([string]$Action = 'install')
4+
35
& {
6+
$RequestedAction = if ([string]::IsNullOrWhiteSpace($Action)) { 'install' } else { $Action.Trim().ToLowerInvariant() }
47
$DirectFileInvocation = -not [string]::IsNullOrWhiteSpace($PSCommandPath)
58
$InstallerFailed = $false
69
$ErrorActionPreference = 'Stop'
@@ -519,14 +522,14 @@ function Invoke-ContexaInstaller {
519522
$finalPath = Join-Path $installDir 'contexa.exe'
520523
$backupPath = $finalPath + '.previous'
521524
$markerPath = $finalPath + '.install-transaction.json'
522-
$action = if ([string]::IsNullOrWhiteSpace($env:CONTEXA_INSTALL_ACTION)) { 'install' } else { $env:CONTEXA_INSTALL_ACTION.Trim().ToLowerInvariant() }
525+
$action = $RequestedAction
523526

524527
if (-not (Test-Path -LiteralPath $installDir)) { New-Item -ItemType Directory -Path $installDir -Force | Out-Null }
525528
Invoke-InstallerTransactionRecovery $installDir $finalPath $backupPath $markerPath
526529
if ($action -eq 'rollback') { Invoke-Rollback $finalPath $backupPath; Ensure-CommandPath $installDir $finalPath; return }
527530
if ($action -eq 'uninstall') { Invoke-Uninstall $installDir $finalPath $backupPath; return }
528531
if ($action -ne 'install') {
529-
throw ((Select-InstallerText 'Unsupported CONTEXA_INSTALL_ACTION' '7KeA7JuQ7ZWY7KeAIOyViuuKlCBDT05URVhBX0lOU1RBTExfQUNUSU9O') + ': ' + $action)
532+
throw ((Select-InstallerText 'Unsupported installer action' '7KeA7JuQ7ZWY7KeAIOyViuuKlCDshKTsuZgg7J6R7JeF') + ': ' + $action)
530533
}
531534

532535
Write-Host (' ' + (Select-InstallerText 'Starting Contexa CLI installation.' 'Q29udGV4YSBDTEkg7ISk7LmY66W8IOyLnOyeke2VqeuLiOuLpC4='))
@@ -641,7 +644,7 @@ function Invoke-ContexaInstaller {
641644
Write-Host ' contexa init --simulate'
642645
Write-Host ' contexa reset --simulate'
643646
Write-Host (' ' + (Select-InstallerText 'Immutable reinstall: set CONTEXA_VERSION=' '64+Z7J28IOuyhOyghCDsnqzshKTsuZg6IENPTlRFWEFfVkVSU0lPTj0=') + $version + (Select-InstallerText ' and run this installer again.' '7J2EIOyEpOygle2VmOqzoCDshKTsuZgg7ZSE66Gc6re4656o7J2EIOuLpOyLnCDsi6TtlontlZjshLjsmpQu'))
644-
Write-Host (' ' + (Select-InstallerText 'Rollback: set CONTEXA_INSTALL_ACTION=rollback and run this installer.' '66Gk67CxOiBDT05URVhBX0lOU1RBTExfQUNUSU9OPXJvbGxiYWNr7J2EIOyEpOygle2VmOqzoCDshKTsuZgg7ZSE66Gc6re4656o7J2EIOyLpO2Wie2VmOyEuOyalC4='))
647+
Write-Host (' ' + (Select-InstallerText 'Rollback: run the downloaded install.ps1 file with -Action rollback.' '66Gk67CxOiBpbnN0YWxsLnBzMSDtjIzsnbzsnYQgLUFjdGlvbiByb2xsYmFjayDsmLXshZjsnLzroZwg7Iuk7ZaJ7ZWY7Iut7Iuc7JikLg=='))
645648
Write-Host (' ' + (Select-InstallerText 'Uninstall: irm https://install.ctxa.ai/uninstall.ps1 | iex (project reset is separate).' '7KCc6rGwOiBpcm0gaHR0cHM6Ly9pbnN0YWxsLmN0eGEuYWkvdW5pbnN0YWxsLnBzMSB8IGlleCAo7ZSE66Gc7KCd7Yq4IHJlc2V07J2AIOuzhOuPhCk='))
646649
} catch {
647650
if ($oldMoved -and -not (Test-Path -LiteralPath $finalPath) -and (Test-Path -LiteralPath $backupPath)) {

install.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ msg() {
2323
installed) printf '%s' ' 설치와 검증을 완료했습니다: ' ;;
2424
primary) printf '%s' '주요 명령:' ;;
2525
immutable) printf '%s' '동일 버전 재설치: CONTEXA_VERSION=' ;;
26-
rollback) printf '%s' '롤백: CONTEXA_INSTALL_ACTION=rollback' ;;
26+
rollback) printf '%s' '롤백: install.sh rollback' ;;
2727
uninstall) printf '%s' '제거: curl -fsSL https://install.ctxa.ai/uninstall.sh | sh (프로젝트 reset은 별도)' ;;
28-
unsupported_action) printf '%s' '지원하지 않는 CONTEXA_INSTALL_ACTION' ;;
28+
unsupported_action) printf '%s' '지원하지 않는 설치 작업' ;;
2929
esac
3030
else
3131
case "$key" in
@@ -37,9 +37,9 @@ msg() {
3737
installed) printf '%s' ' installed and verified for ' ;;
3838
primary) printf '%s' 'Primary commands:' ;;
3939
immutable) printf '%s' 'Immutable reinstall: CONTEXA_VERSION=' ;;
40-
rollback) printf '%s' 'Rollback: CONTEXA_INSTALL_ACTION=rollback' ;;
40+
rollback) printf '%s' 'Rollback: install.sh rollback' ;;
4141
uninstall) printf '%s' 'Uninstall: curl -fsSL https://install.ctxa.ai/uninstall.sh | sh (project reset is separate)' ;;
42-
unsupported_action) printf '%s' 'Unsupported CONTEXA_INSTALL_ACTION' ;;
42+
unsupported_action) printf '%s' 'Unsupported installer action' ;;
4343
esac
4444
fi
4545
}
@@ -416,7 +416,7 @@ INSTALL_DIR=$(cd "$INSTALL_DIR" && pwd -P)
416416
INSTALL_PATH="$INSTALL_DIR/contexa"
417417
BACKUP_PATH="$INSTALL_PATH.previous"
418418
MARKER_PATH="$INSTALL_PATH.install-transaction"
419-
ACTION="${CONTEXA_INSTALL_ACTION:-install}"
419+
ACTION="${1:-install}"
420420

421421
recover_installer_transaction
422422
case "$ACTION" in

test/installer-runtime.test.js

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,12 @@ function windowsChannelInstallerEnv(base, installDir, keyXml) {
222222
}
223223

224224
function runWindowsInstaller(env, timeout = 10000) {
225+
const action = env.CONTEXA_INSTALL_ACTION || 'install';
226+
const runtimeEnv = { ...env };
227+
delete runtimeEnv.CONTEXA_INSTALL_ACTION;
225228
return run(powershell,
226-
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ps1], env, timeout);
229+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ps1, '-Action', action],
230+
runtimeEnv, timeout);
227231
}
228232

229233
function runWindowsInstallerViaIex(env, timeout = 10000, catchFailure = false) {
@@ -236,7 +240,12 @@ function runWindowsInstallerViaIex(env, timeout = 10000, catchFailure = false) {
236240
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command], env, timeout);
237241
}
238242
function runPwshInstaller(env) {
239-
return run(pwsh, ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ps1], env);
243+
const action = env.CONTEXA_INSTALL_ACTION || 'install';
244+
const runtimeEnv = { ...env };
245+
delete runtimeEnv.CONTEXA_INSTALL_ACTION;
246+
return run(pwsh,
247+
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ps1, '-Action', action],
248+
runtimeEnv);
240249
}
241250

242251
function toPosixPath(value) {
@@ -281,10 +290,19 @@ function posixInstallerEnv(base, harness, publicKeyPath, version = '') {
281290
};
282291
}
283292

284-
function runPosixInstaller(env, installer = sh) {
293+
function runPosixInstaller(env, installer = sh, explicitAction = undefined) {
285294
const quotedPath = env.PATH.replace(/'/g, `'"'"'`);
286295
const scriptPath = toPosixPath(installer).replace(/'/g, `'"'"'`);
287-
return run(gitSh, ['-c', `PATH='${quotedPath}'; export PATH; exec '${scriptPath}'`], env);
296+
const runtimeEnv = { ...env };
297+
let action = '';
298+
if (explicitAction === null) {
299+
// Keep a leaked legacy environment value to prove the public install entrypoint ignores it.
300+
} else {
301+
action = explicitAction === undefined ? (env.CONTEXA_INSTALL_ACTION || '') : explicitAction;
302+
delete runtimeEnv.CONTEXA_INSTALL_ACTION;
303+
}
304+
const actionArgument = action ? ` '${String(action).replace(/'/g, `'"'"'`)}'` : '';
305+
return run(gitSh, ['-c', `PATH='${quotedPath}'; export PATH; exec '${scriptPath}'${actionArgument}`], runtimeEnv);
288306
}
289307

290308
function buildPosixCli(version, options = {}) {
@@ -402,7 +420,8 @@ test('PowerShell installer performs install, no-op, update, rollback and uninsta
402420
await withServer(releaseHandler(files), async (base) => {
403421
for (let iteration = 1; iteration <= lifecycleRepeats; iteration += 1) {
404422
const installDir = path.join(temp, `설치 경로 with space ${iteration}`);
405-
const first = await runWindowsInstallerViaIex(windowsInstallerEnv(base, installDir, '9.9.1-test', xml));
423+
const leakedActionEnv = windowsInstallerEnv(base, installDir, '9.9.1-test', xml, 'uninstall');
424+
const first = await runWindowsInstallerViaIex(leakedActionEnv);
406425
assert.match(first.stdout, /__SHELL_ALIVE__/);
407426
assert.equal(first.code, 0, first.stderr || first.stdout);
408427
assert.match(first.stdout, /Starting Contexa CLI installation/);
@@ -990,7 +1009,11 @@ test('POSIX installer performs lifecycle and preserves the existing binary for t
9901009
await withServer(releaseHandler(lifecycleFiles), async (base) => {
9911010
for (let iteration = 1; iteration <= lifecycleRepeats; iteration += 1) {
9921011
const lifecycleHarness = createPosixHarness(path.join(temp, `설치 경로 with space ${iteration}`));
993-
const first = await runPosixInstaller(posixInstallerEnv(base, lifecycleHarness, publicKeyPath, currentVersion));
1012+
const leakedActionEnv = {
1013+
...posixInstallerEnv(base, lifecycleHarness, publicKeyPath, currentVersion),
1014+
CONTEXA_INSTALL_ACTION: 'uninstall',
1015+
};
1016+
const first = await runPosixInstaller(leakedActionEnv, sh, null);
9941017
assert.equal(first.code, 0, first.stderr || first.stdout);
9951018
const installed = path.join(lifecycleHarness.installDir, 'contexa');
9961019
assert.equal(spawnSync(gitSh, [toPosixPath(installed), '--version'], { encoding: 'utf8' }).stdout.trim(), currentVersion);
@@ -1215,18 +1238,12 @@ test('POSIX installer emits intact Korean success and error messages', { timeout
12151238
CONTEXA_INSTALL_DIR: path.join(temp, 'bin'),
12161239
CONTEXA_SKIP_PATH_UPDATE: '1',
12171240
};
1218-
const uninstall = await run(gitSh, [scriptPath], {
1219-
...baseEnv,
1220-
CONTEXA_INSTALL_ACTION: 'uninstall',
1221-
});
1241+
const uninstall = await run(gitSh, [scriptPath, 'uninstall'], baseEnv);
12221242
assert.equal(uninstall.code, 0, uninstall.stderr || uninstall.stdout);
12231243
assert.match(uninstall.stdout, /.*/);
12241244
assert.equal((uninstall.stdout + uninstall.stderr).includes('\uFFFD'), false);
12251245

1226-
const invalid = await run(gitSh, [scriptPath], {
1227-
...baseEnv,
1228-
CONTEXA_INSTALL_ACTION: 'invalid',
1229-
});
1246+
const invalid = await run(gitSh, [scriptPath, 'invalid'], baseEnv);
12301247
assert.equal(invalid.code, 1);
12311248
assert.match(invalid.stderr, / \[INSTALLER_OPERATION_FAILED\].* /s);
12321249
assert.equal((invalid.stdout + invalid.stderr).includes('\uFFFD'), false);

test/installers.test.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ test('install.ps1 enforces the signed, bounded and atomic installation contract'
5353
assert.match(src, /\.new\.exe/);
5454
assert.match(src, /\[System\.IO\.File\]::Move\(\$temporaryPath, \$finalPath\)/);
5555
assert.match(src, /\.previous/);
56-
assert.match(src, /CONTEXA_INSTALL_ACTION/);
56+
assert.equal(src.includes('CONTEXA_INSTALL_ACTION'), false);
5757
assert.match(src, /CONTEXA_TRUSTED_PUBLIC_KEY_XML/);
5858
assert.match(src, /IsLoopback/);
5959
assert.match(src, /Test-BinarySmoke \$temporaryPath/);
@@ -93,7 +93,7 @@ test('install.sh enforces supported platforms, bounded download and atomic repla
9393
assert.match(src, /glibc 2\.28 or newer/);
9494
assert.match(src, /\.contexa\.new\.XXXXXX/);
9595
assert.match(src, /BACKUP_PATH="\$INSTALL_PATH\.previous"/);
96-
assert.match(src, /CONTEXA_INSTALL_ACTION/);
96+
assert.equal(src.includes('CONTEXA_INSTALL_ACTION'), false);
9797
assert.match(src, /CONTEXA_TRUSTED_PUBLIC_KEY_PATH/);
9898
assert.match(src, /loopback release server/);
9999
assert.match(src, /smoke_binary "\$NEW_BINARY"/);
@@ -127,11 +127,11 @@ test('public uninstall entrypoints reuse the installer without leaking control s
127127
const ps = read(uninstallPs1Path);
128128
const sh = read(uninstallShPath);
129129
assert.ok(ps.startsWith('#Requires -Version 5.1'));
130-
assert.match(ps, /CONTEXA_INSTALL_ACTION = 'uninstall'/);
130+
assert.match(ps, /-Action uninstall/);
131131
assert.match(ps, /https:\/\/install\.ctxa\.ai\/install\.ps1/);
132-
assert.match(ps, /Remove-Item Env:CONTEXA_INSTALL_ACTION/);
132+
assert.equal(ps.includes('CONTEXA_INSTALL_ACTION'), false);
133133
assert.ok(sh.startsWith('#!/bin/sh'));
134-
assert.match(sh, /CONTEXA_INSTALL_ACTION=uninstall sh/);
134+
assert.match(sh, /sh "\$installer_file" uninstall/);
135135
assert.match(sh, /https:\/\/install\.ctxa\.ai\/install\.sh/);
136136
});
137137

@@ -181,11 +181,11 @@ test('api prioritizes explicit paths and exposes immutable version URLs', () =>
181181

182182
const uninstallPs = invoke('/uninstall.ps1');
183183
assert.ok(uninstallPs.body.startsWith('#Requires -Version 5.1'));
184-
assert.match(uninstallPs.body, /CONTEXA_INSTALL_ACTION = 'uninstall'/);
184+
assert.match(uninstallPs.body, /-Action uninstall/);
185185

186186
const uninstallSh = invoke('/uninstall.sh', 'WindowsPowerShell/5.1');
187187
assert.ok(uninstallSh.body.startsWith('#!/bin/sh'));
188-
assert.match(uninstallSh.body, /CONTEXA_INSTALL_ACTION=uninstall sh/);
188+
assert.match(uninstallSh.body, /sh "\$installer_file" uninstall/);
189189

190190
const immutable = invoke('/v9.9.9-installer-test/install.ps1');
191191
assert.equal(immutable.statusCode, 302);
@@ -207,7 +207,7 @@ test('api prioritizes explicit paths and exposes immutable version URLs', () =>
207207
assert.equal(stable.headers['x-content-type-options'], 'nosniff');
208208
const stableUninstall = invoke('/uninstall.ps1');
209209
assert.equal(stableUninstall.statusCode, 200);
210-
assert.match(stableUninstall.body, /CONTEXA_INSTALL_ACTION = 'uninstall'/);
210+
assert.match(stableUninstall.body, /-Action uninstall/);
211211
} finally {
212212
if (originalStableRef === undefined) delete process.env.CONTEXA_STABLE_INSTALLER_REF;
213213
else process.env.CONTEXA_STABLE_INSTALLER_REF = originalStableRef;

uninstall.ps1

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,6 @@
11
#Requires -Version 5.1
22

33
& {
4-
$hadInstallAction = Test-Path Env:CONTEXA_INSTALL_ACTION
5-
$previousInstallAction = $env:CONTEXA_INSTALL_ACTION
6-
7-
try {
8-
$env:CONTEXA_INSTALL_ACTION = 'uninstall'
9-
$installer = Invoke-WebRequest 'https://install.ctxa.ai/install.ps1' -UseBasicParsing
10-
& ([scriptblock]::Create($installer.Content))
11-
} finally {
12-
if ($hadInstallAction) {
13-
$env:CONTEXA_INSTALL_ACTION = $previousInstallAction
14-
} else {
15-
Remove-Item Env:CONTEXA_INSTALL_ACTION -ErrorAction SilentlyContinue
16-
}
17-
}
4+
$installer = Invoke-WebRequest 'https://install.ctxa.ai/install.ps1' -UseBasicParsing
5+
& ([scriptblock]::Create($installer.Content)) -Action uninstall
186
}

uninstall.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ if ! curl -fsSL --connect-timeout 5 --max-time 30 \
99
exit 1
1010
fi
1111

12-
CONTEXA_INSTALL_ACTION=uninstall sh "$installer_file"
12+
sh "$installer_file" uninstall

0 commit comments

Comments
 (0)