diff --git a/CHANGELOG.md b/CHANGELOG.md index f83a2c3fe..3c6b4b82f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,16 @@ All notable changes to Agent Relay will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased - Major] ### Removed - Python workflows, the `agent-relay.py` node provider, and the `agent-relay-sdk` package. Local workflows are YAML, TypeScript, JavaScript, or shell. Cloud and worker workflows are YAML or TypeScript. Node definitions are TypeScript or JavaScript. SDKs are TypeScript and Swift. +### Fixed + +- `agent-relay uninstall --dry-run` previews broker termination without sending a signal to the running process. + ## [14.0.0] - 2026-10-13 ### Changed diff --git a/packages/cli/src/cli/commands/core.test.ts b/packages/cli/src/cli/commands/core.test.ts index a81a3b8bb..da82353c2 100644 --- a/packages/cli/src/cli/commands/core.test.ts +++ b/packages/cli/src/cli/commands/core.test.ts @@ -2833,6 +2833,46 @@ describe('registerCoreCommands', () => { }); }); + it.each([ + ['dry-run', true], + ['normal uninstall', false], + ])('uninstall %s handles a recorded broker process safely', async (_mode, dryRun) => { + const projectRoot = nodeFs.mkdtempSync(nodePath.join(os.tmpdir(), 'relay-uninstall-process-')); + const dataDir = nodePath.join(projectRoot, '.agentworkforce', 'relay'); + const connectionPath = nodePath.join(dataDir, 'connection.json'); + nodeFs.mkdirSync(dataDir, { recursive: true }); + nodeFs.writeFileSync(connectionPath, connectionFile(54321)); + + try { + const { deps } = createHarness(); + vi.mocked(deps.getProjectPaths).mockReturnValue({ + projectRoot, + dataDir, + teamDir: nodePath.join(dataDir, 'teams'), + dbPath: nodePath.join(dataDir, 'messages.db'), + projectId: 'test-uninstall', + }); + const program = new Command(); + registerCoreMaintenance(program, deps); + + const exitCode = await runCommand(program, ['uninstall', ...(dryRun ? ['--dry-run'] : [])]); + + expect(exitCode).toBeUndefined(); + if (dryRun) { + expect(deps.log).toHaveBeenCalledWith('[dry-run] Would send SIGTERM to broker (pid: 54321)'); + expect(deps.killProcess).not.toHaveBeenCalled(); + expect(deps.fs.rmSync).not.toHaveBeenCalled(); + expect(deps.fs.unlinkSync).not.toHaveBeenCalled(); + expect(deps.execCommand).not.toHaveBeenCalled(); + expect(nodeFs.existsSync(connectionPath)).toBe(true); + } else { + expect(deps.killProcess).toHaveBeenCalledExactlyOnceWith(54321, 'SIGTERM'); + } + } finally { + nodeFs.rmSync(projectRoot, { recursive: true, force: true }); + } + }); + it('uninstall dry-run covers renamed and legacy installer bin directories', async () => { const { deps } = createHarness(); const program = new Command(); diff --git a/packages/cli/src/cli/lib/core-maintenance.ts b/packages/cli/src/cli/lib/core-maintenance.ts index 85e1502da..72e271783 100644 --- a/packages/cli/src/cli/lib/core-maintenance.ts +++ b/packages/cli/src/cli/lib/core-maintenance.ts @@ -223,18 +223,22 @@ export async function runUninstallCommand( const paths = deps.getProjectPaths(); const connectionPath = path.join(paths.dataDir, 'connection.json'); - // Kill the broker if running + const isDryRun = options.dryRun === true; + + // Preview broker shutdown without signalling a running process. const conn = readBrokerConnection(paths.dataDir); if (conn && conn.pid > 0) { - try { - deps.killProcess(conn.pid, 'SIGTERM'); - } catch { - // Ignore dead processes. + if (isDryRun) { + deps.log(`[dry-run] Would send SIGTERM to broker (pid: ${conn.pid})`); + } else { + try { + deps.killProcess(conn.pid, 'SIGTERM'); + } catch { + // Ignore dead processes. + } } } - const isDryRun = options.dryRun === true; - // --- Data directory cleanup --- if (isDryRun) { if (options.keepData) {