remote-codex 0.11.45 → 0.11.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -10
- package/apps/relay-server/dist/index.js +1 -1
- package/apps/supervisor-api/dist/index.js +8370 -1501
- package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
- package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
- package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
- package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
- package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
- package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
- package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
- package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
- package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
- package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
- package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
- package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
- package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
- package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
- package/apps/supervisor-web/dist/index.html +4 -4
- package/docs/windows-device-manager.zh.md +143 -0
- package/docs/windows-device-setup.zh.md +263 -0
- package/docs/windows-one-click-installer-research.zh.md +481 -0
- package/docs/windows.md +6 -0
- package/package.json +7 -2
- package/packages/acp/src/agent-catalog.test.ts +57 -0
- package/packages/acp/src/agent-catalog.ts +361 -0
- package/packages/acp/src/catalog-runtime.test.ts +99 -0
- package/packages/acp/src/catalog-runtime.ts +507 -0
- package/packages/acp/src/codex-environment.test.ts +58 -0
- package/packages/acp/src/codex-environment.ts +33 -0
- package/packages/acp/src/index.ts +5 -0
- package/packages/acp/src/item-mapper.test.ts +137 -0
- package/packages/acp/src/item-mapper.ts +473 -0
- package/packages/acp/src/runtimeAdapter.test.ts +79 -0
- package/packages/acp/src/runtimeAdapter.ts +1193 -0
- package/packages/acp/src/terminal-service.test.ts +31 -0
- package/packages/acp/src/terminal-service.ts +137 -0
- package/packages/agent-runtime/src/runtime-errors.ts +1 -0
- package/packages/agent-runtime/src/types.ts +8 -0
- package/packages/db/migrations/0030_thread_agent_id.sql +9 -0
- package/packages/db/src/repositories.ts +3 -0
- package/packages/db/src/schema.ts +1 -0
- package/packages/shared/src/agent-providers.ts +10 -1
- package/packages/shared/src/index.ts +24 -0
- package/scripts/windows/build-device-manager.ps1 +64 -0
- package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +0 -1
- package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +0 -22
- package/apps/supervisor-web/dist/assets/thread-ui-BWC_ljvN.js +0 -3714
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { AcpTerminalService } from './terminal-service';
|
|
4
|
+
|
|
5
|
+
describe('AcpTerminalService', () => {
|
|
6
|
+
it('parses agents that send command and argv as one ACP command string', async () => {
|
|
7
|
+
const service = new AcpTerminalService(() => process.cwd());
|
|
8
|
+
const created = service.create({
|
|
9
|
+
sessionId: 'session-1',
|
|
10
|
+
command: `"${process.execPath}" -e "process.stdout.write('ACP_TERMINAL_OK')"`,
|
|
11
|
+
args: [],
|
|
12
|
+
outputByteLimit: 1_000,
|
|
13
|
+
});
|
|
14
|
+
const exit = await service.waitForExit({
|
|
15
|
+
sessionId: 'session-1',
|
|
16
|
+
terminalId: created.terminalId,
|
|
17
|
+
});
|
|
18
|
+
const output = service.output({
|
|
19
|
+
sessionId: 'session-1',
|
|
20
|
+
terminalId: created.terminalId,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(exit.exitCode).toBe(0);
|
|
24
|
+
expect(output.output).toBe('ACP_TERMINAL_OK');
|
|
25
|
+
expect(output.truncated).toBe(false);
|
|
26
|
+
service.release({
|
|
27
|
+
sessionId: 'session-1',
|
|
28
|
+
terminalId: created.terminalId,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { ChildProcess } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
import type * as acp from '@agentclientprotocol/sdk';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
parseCommandLine,
|
|
8
|
+
spawnProcess,
|
|
9
|
+
} from '../../process-runtime/src/index';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_OUTPUT_BYTE_LIMIT = 1024 * 1024;
|
|
12
|
+
|
|
13
|
+
interface AcpTerminalState {
|
|
14
|
+
child: ChildProcess;
|
|
15
|
+
chunks: Buffer[];
|
|
16
|
+
outputByteLimit: number;
|
|
17
|
+
exitStatus: acp.TerminalExitStatus | null;
|
|
18
|
+
exitPromise: Promise<acp.WaitForTerminalExitResponse>;
|
|
19
|
+
resolveExit: (status: acp.WaitForTerminalExitResponse) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function retainedOutput(state: AcpTerminalState) {
|
|
23
|
+
const complete = Buffer.concat(state.chunks);
|
|
24
|
+
if (complete.byteLength <= state.outputByteLimit) {
|
|
25
|
+
return { output: complete.toString('utf8'), truncated: false };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let start = complete.byteLength - state.outputByteLimit;
|
|
29
|
+
while (start < complete.byteLength && (complete[start]! & 0xc0) === 0x80) {
|
|
30
|
+
start += 1;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
output: complete.subarray(start).toString('utf8'),
|
|
34
|
+
truncated: true,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AcpTerminalService {
|
|
39
|
+
private readonly terminals = new Map<string, AcpTerminalState>();
|
|
40
|
+
|
|
41
|
+
constructor(private readonly sessionCwd: (sessionId: string) => string | null) {}
|
|
42
|
+
|
|
43
|
+
create(params: acp.CreateTerminalRequest): acp.CreateTerminalResponse {
|
|
44
|
+
const terminalId = randomUUID();
|
|
45
|
+
const cwd = params.cwd ?? this.sessionCwd(params.sessionId) ?? process.cwd();
|
|
46
|
+
const parsed = params.args && params.args.length > 0
|
|
47
|
+
? { command: params.command, args: params.args }
|
|
48
|
+
: parseCommandLine(params.command);
|
|
49
|
+
const child = spawnProcess({
|
|
50
|
+
command: parsed.command,
|
|
51
|
+
args: parsed.args,
|
|
52
|
+
cwd,
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
...Object.fromEntries((params.env ?? []).map((entry) => [entry.name, entry.value])),
|
|
56
|
+
},
|
|
57
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
let resolveExit!: (status: acp.WaitForTerminalExitResponse) => void;
|
|
60
|
+
const exitPromise = new Promise<acp.WaitForTerminalExitResponse>((resolve) => {
|
|
61
|
+
resolveExit = resolve;
|
|
62
|
+
});
|
|
63
|
+
const state: AcpTerminalState = {
|
|
64
|
+
child,
|
|
65
|
+
chunks: [],
|
|
66
|
+
outputByteLimit: Math.max(1, params.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT),
|
|
67
|
+
exitStatus: null,
|
|
68
|
+
exitPromise,
|
|
69
|
+
resolveExit,
|
|
70
|
+
};
|
|
71
|
+
this.terminals.set(terminalId, state);
|
|
72
|
+
|
|
73
|
+
const append = (chunk: Buffer | string) => {
|
|
74
|
+
state.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
75
|
+
};
|
|
76
|
+
child.stdout?.on('data', append);
|
|
77
|
+
child.stderr?.on('data', append);
|
|
78
|
+
child.on('error', (error) => append(`${error.message}\n`));
|
|
79
|
+
child.on('close', (code, signal) => {
|
|
80
|
+
const exitStatus: acp.TerminalExitStatus = {
|
|
81
|
+
exitCode: code,
|
|
82
|
+
signal,
|
|
83
|
+
};
|
|
84
|
+
state.exitStatus = exitStatus;
|
|
85
|
+
state.resolveExit(exitStatus);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return { terminalId };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
output(params: acp.TerminalOutputRequest): acp.TerminalOutputResponse {
|
|
92
|
+
const state = this.require(params.terminalId);
|
|
93
|
+
const output = retainedOutput(state);
|
|
94
|
+
return {
|
|
95
|
+
...output,
|
|
96
|
+
...(state.exitStatus ? { exitStatus: state.exitStatus } : {}),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
waitForExit(params: acp.WaitForTerminalExitRequest) {
|
|
101
|
+
return this.require(params.terminalId).exitPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
kill(params: acp.KillTerminalRequest): acp.KillTerminalResponse {
|
|
105
|
+
const state = this.require(params.terminalId);
|
|
106
|
+
if (!state.exitStatus) {
|
|
107
|
+
state.child.kill('SIGTERM');
|
|
108
|
+
}
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
release(params: acp.ReleaseTerminalRequest): acp.ReleaseTerminalResponse {
|
|
113
|
+
const state = this.require(params.terminalId);
|
|
114
|
+
if (!state.exitStatus) {
|
|
115
|
+
state.child.kill('SIGTERM');
|
|
116
|
+
}
|
|
117
|
+
this.terminals.delete(params.terminalId);
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
stop() {
|
|
122
|
+
for (const state of this.terminals.values()) {
|
|
123
|
+
if (!state.exitStatus) {
|
|
124
|
+
state.child.kill('SIGTERM');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.terminals.clear();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private require(terminalId: string) {
|
|
131
|
+
const state = this.terminals.get(terminalId);
|
|
132
|
+
if (!state) {
|
|
133
|
+
throw new Error(`ACP terminal not found: ${terminalId}`);
|
|
134
|
+
}
|
|
135
|
+
return state;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -73,6 +73,7 @@ export function isRemoteThreadBootstrapError(error: unknown) {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
|
+
(isRecord(error.details) && error.details.historyUnavailable === true) ||
|
|
76
77
|
error.message.includes('includeTurns is unavailable before first user message') ||
|
|
77
78
|
error.message.includes('is not materialized yet') ||
|
|
78
79
|
error.message.includes('no rollout found for thread id') ||
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import type {
|
|
3
|
+
AcpAgentOptionMetadataDto,
|
|
3
4
|
AgentBackendIdDto,
|
|
4
5
|
AgentBackendInstallationDto,
|
|
5
6
|
ThreadHistoryItemDto,
|
|
@@ -159,6 +160,8 @@ export interface AgentModel {
|
|
|
159
160
|
description: string;
|
|
160
161
|
}>;
|
|
161
162
|
defaultReasoningEffort: string | null;
|
|
163
|
+
selectionKind?: 'model' | 'agent';
|
|
164
|
+
acpAgent?: AcpAgentOptionMetadataDto | null;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
export type AgentSessionStatus =
|
|
@@ -228,6 +231,7 @@ export interface ReadAgentSessionOptions {
|
|
|
228
231
|
|
|
229
232
|
export interface StartAgentSessionInput {
|
|
230
233
|
cwd: string;
|
|
234
|
+
agentId?: string | null;
|
|
231
235
|
model: string;
|
|
232
236
|
reasoningEffort?: string | null;
|
|
233
237
|
approvalMode: 'yolo' | 'guarded';
|
|
@@ -237,6 +241,7 @@ export interface StartAgentSessionInput {
|
|
|
237
241
|
|
|
238
242
|
export interface StartAgentSessionResult {
|
|
239
243
|
provider: AgentProviderId;
|
|
244
|
+
agentId?: string | null;
|
|
240
245
|
providerSessionId: string;
|
|
241
246
|
model: string | null;
|
|
242
247
|
reasoningEffort?: string | null;
|
|
@@ -549,6 +554,8 @@ export interface AgentRuntime extends EventEmitter {
|
|
|
549
554
|
stop(): Promise<void>;
|
|
550
555
|
|
|
551
556
|
listModels(): Promise<AgentModel[]>;
|
|
557
|
+
listAgentOptions?(): Promise<AgentModel[]>;
|
|
558
|
+
listModelsForAgent?(agentId: string, cwd: string): Promise<AgentModel[]>;
|
|
552
559
|
listSessions(): Promise<AgentSessionSummary[]>;
|
|
553
560
|
listLoadedSessions(): Promise<string[]>;
|
|
554
561
|
readSession(
|
|
@@ -582,6 +589,7 @@ export interface AgentRuntime extends EventEmitter {
|
|
|
582
589
|
getGoal?(providerSessionId: string): Promise<AgentGoal | null>;
|
|
583
590
|
setGoal?(input: SetAgentGoalInput): Promise<AgentGoal>;
|
|
584
591
|
clearGoal?(providerSessionId: string): Promise<boolean>;
|
|
592
|
+
installModel?(modelId: string): Promise<void>;
|
|
585
593
|
getSubscriptionUsage?(): Promise<{
|
|
586
594
|
provider: 'codex' | 'claude';
|
|
587
595
|
authKind: 'subscription' | 'apiKey' | 'unknown';
|
|
@@ -29,6 +29,7 @@ export interface CreateThreadRecordInput {
|
|
|
29
29
|
workspaceId: string;
|
|
30
30
|
title: string;
|
|
31
31
|
provider?: string;
|
|
32
|
+
agentId?: string | null;
|
|
32
33
|
providerSessionId: string | null;
|
|
33
34
|
providerTurnId?: string | null;
|
|
34
35
|
model?: string | null;
|
|
@@ -47,6 +48,7 @@ export interface CreateThreadRecordInput {
|
|
|
47
48
|
|
|
48
49
|
export interface UpdateThreadRecordInput {
|
|
49
50
|
provider?: string;
|
|
51
|
+
agentId?: string | null;
|
|
50
52
|
providerSessionId?: string | null;
|
|
51
53
|
providerTurnId?: string | null;
|
|
52
54
|
title?: string;
|
|
@@ -278,6 +280,7 @@ export function createThreadRecord(db: DatabaseClient, input: CreateThreadRecord
|
|
|
278
280
|
id: randomUUID(),
|
|
279
281
|
workspaceId: input.workspaceId,
|
|
280
282
|
provider: input.provider ?? 'codex',
|
|
283
|
+
agentId: input.agentId ?? null,
|
|
281
284
|
providerSessionId: input.providerSessionId,
|
|
282
285
|
providerTurnId: input.providerTurnId ?? null,
|
|
283
286
|
source: input.source ?? 'supervisor',
|
|
@@ -23,6 +23,7 @@ export const threads = sqliteTable('threads', {
|
|
|
23
23
|
id: text('id').primaryKey(),
|
|
24
24
|
workspaceId: text('workspace_id').notNull(),
|
|
25
25
|
provider: text('provider').notNull().default('codex'),
|
|
26
|
+
agentId: text('agent_id'),
|
|
26
27
|
providerSessionId: text('provider_session_id'),
|
|
27
28
|
providerTurnId: text('provider_turn_id'),
|
|
28
29
|
source: text('source').notNull().default('supervisor'),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const agentBackendIds = ['codex', 'claude', 'opencode'] as const;
|
|
1
|
+
export const agentBackendIds = ['codex', 'claude', 'opencode', 'acp'] as const;
|
|
2
2
|
|
|
3
3
|
export type AgentBackendIdDto = (typeof agentBackendIds)[number];
|
|
4
4
|
|
|
@@ -42,6 +42,15 @@ export const agentBackendMetadata: Record<AgentBackendIdDto, AgentBackendMetadat
|
|
|
42
42
|
defaultHomeDir: '.opencode',
|
|
43
43
|
defaultCommand: 'opencode',
|
|
44
44
|
},
|
|
45
|
+
acp: {
|
|
46
|
+
displayName: 'ACP Agent',
|
|
47
|
+
description: 'Agent Client Protocol runtime over stdio.',
|
|
48
|
+
defaultTransport: 'stdio',
|
|
49
|
+
homeEnvVar: 'ACP_HOME',
|
|
50
|
+
commandEnvVar: 'ACP_COMMAND',
|
|
51
|
+
defaultHomeDir: '.acp',
|
|
52
|
+
defaultCommand: 'grok agent stdio',
|
|
53
|
+
},
|
|
45
54
|
};
|
|
46
55
|
|
|
47
56
|
export function isAgentBackendId(value: unknown): value is AgentBackendIdDto {
|
|
@@ -716,6 +716,28 @@ export interface ModelOptionDto {
|
|
|
716
716
|
supportsPerformanceMode?: boolean;
|
|
717
717
|
supportedReasoningEfforts: ReasoningEffortOptionDto[];
|
|
718
718
|
defaultReasoningEffort: ReasoningEffortDto | null;
|
|
719
|
+
selectionKind?: 'model' | 'agent';
|
|
720
|
+
acpAgent?: AcpAgentOptionMetadataDto | null;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export type AcpAgentAvailabilityDto =
|
|
724
|
+
| 'ready'
|
|
725
|
+
| 'base_missing'
|
|
726
|
+
| 'adapter_missing'
|
|
727
|
+
| 'server_unavailable';
|
|
728
|
+
|
|
729
|
+
export interface AcpAgentOptionMetadataDto {
|
|
730
|
+
transport: 'native' | 'adapter' | 'custom';
|
|
731
|
+
availability: AcpAgentAvailabilityDto;
|
|
732
|
+
baseCommand: string;
|
|
733
|
+
baseProbeCommand: string;
|
|
734
|
+
serverCommand: string;
|
|
735
|
+
serverProbeCommand: string;
|
|
736
|
+
baseVersion: string | null;
|
|
737
|
+
serverVersion: string | null;
|
|
738
|
+
installCommand: string | null;
|
|
739
|
+
busy: boolean;
|
|
740
|
+
statusMessage: string;
|
|
719
741
|
}
|
|
720
742
|
|
|
721
743
|
export interface VersionDto {
|
|
@@ -929,6 +951,7 @@ export interface ThreadDto {
|
|
|
929
951
|
id: string;
|
|
930
952
|
workspaceId: string;
|
|
931
953
|
provider: AgentBackendIdDto;
|
|
954
|
+
agentId?: string | null;
|
|
932
955
|
providerSessionId: string | null;
|
|
933
956
|
source: ThreadSourceDto;
|
|
934
957
|
title: string;
|
|
@@ -1594,6 +1617,7 @@ export interface CreateThreadInput {
|
|
|
1594
1617
|
workspaceId: string;
|
|
1595
1618
|
title?: string;
|
|
1596
1619
|
provider?: AgentBackendIdDto;
|
|
1620
|
+
agentId?: string;
|
|
1597
1621
|
model: string;
|
|
1598
1622
|
reasoningEffort?: ReasoningEffortDto | null;
|
|
1599
1623
|
approvalMode: ApprovalMode;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[string]$Configuration = 'Release',
|
|
4
|
+
[string]$Runtime = 'win-x64',
|
|
5
|
+
[string]$OutputPath = (Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path 'artifacts\windows-device-manager\win-x64')
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
$repositoryRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
|
10
|
+
$projectPath = Join-Path $repositoryRoot 'apps\windows-device-manager\RemoteCodex.DeviceManager.csproj'
|
|
11
|
+
$productManifestPath = Join-Path $repositoryRoot 'apps\windows-device-manager\ProductManifest.cs'
|
|
12
|
+
$dotnet = (Get-Command dotnet.exe -ErrorAction SilentlyContinue)
|
|
13
|
+
if (-not $dotnet) {
|
|
14
|
+
$dotnet = (Get-Command dotnet -ErrorAction Stop)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
$packageVersion = (Get-Content -LiteralPath (Join-Path $repositoryRoot 'package.json') -Raw | ConvertFrom-Json).version
|
|
18
|
+
$productManifest = Get-Content -LiteralPath $productManifestPath -Raw
|
|
19
|
+
if ($productManifest -notmatch ('RemoteCodexVersion\s*=\s*"{0}"' -f [Regex]::Escape($packageVersion))) {
|
|
20
|
+
throw "ProductManifest.RemoteCodexVersion must match package.json version $packageVersion."
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
& $dotnet.Source publish $projectPath `
|
|
24
|
+
--configuration $Configuration `
|
|
25
|
+
--runtime $Runtime `
|
|
26
|
+
--self-contained true `
|
|
27
|
+
-p:PublishSingleFile=true `
|
|
28
|
+
--output $OutputPath
|
|
29
|
+
if ($LASTEXITCODE -ne 0) {
|
|
30
|
+
throw "dotnet publish failed with exit code $LASTEXITCODE."
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
$executable = Join-Path $OutputPath 'RemoteCodex.DeviceManager.exe'
|
|
34
|
+
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
|
|
35
|
+
throw "Published executable was not found: $executable"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
$selfTest = Start-Process -FilePath $executable -ArgumentList '--self-test' -Wait -PassThru
|
|
39
|
+
if ($selfTest.ExitCode -ne 0) {
|
|
40
|
+
$selfTestLog = Join-Path $env:LOCALAPPDATA 'RemoteCodex\logs\device-manager.log'
|
|
41
|
+
if (Test-Path -LiteralPath $selfTestLog -PathType Leaf) {
|
|
42
|
+
Write-Host 'Self-test diagnostics:'
|
|
43
|
+
Get-Content -LiteralPath $selfTestLog -Tail 50
|
|
44
|
+
}
|
|
45
|
+
throw "Remote Codex Device self-test failed with exit code $($selfTest.ExitCode)."
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$previewPath = Join-Path $OutputPath 'RemoteCodex.DeviceManager.preview.png'
|
|
49
|
+
$previewTest = Start-Process `
|
|
50
|
+
-FilePath $executable `
|
|
51
|
+
-ArgumentList @('--render-preview', ('"{0}"' -f $previewPath)) `
|
|
52
|
+
-Wait `
|
|
53
|
+
-PassThru
|
|
54
|
+
if ($previewTest.ExitCode -ne 0 -or -not (Test-Path -LiteralPath $previewPath -PathType Leaf)) {
|
|
55
|
+
throw "Remote Codex Device preview render failed with exit code $($previewTest.ExitCode)."
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$hash = Get-FileHash -LiteralPath $executable -Algorithm SHA256
|
|
59
|
+
$hashLine = '{0} {1}' -f $hash.Hash.ToLowerInvariant(), (Split-Path -Leaf $executable)
|
|
60
|
+
$hashPath = Join-Path $OutputPath 'RemoteCodex.DeviceManager.exe.sha256'
|
|
61
|
+
[IO.File]::WriteAllText($hashPath, "$hashLine`n", [Text.UTF8Encoding]::new($false))
|
|
62
|
+
|
|
63
|
+
Write-Host "Built: $executable"
|
|
64
|
+
Write-Host "SHA-256: $($hash.Hash.ToLowerInvariant())"
|