@bitkyc08/opencodex 2.43.0 → 2.44.0
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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
package/src/cli/integrations.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CliUsageError,
|
|
3
|
+
RuntimeApiError,
|
|
3
4
|
csv,
|
|
4
5
|
printData,
|
|
5
6
|
rejectArgs,
|
|
@@ -28,10 +29,25 @@ const GROK_USAGE = `Usage:
|
|
|
28
29
|
ocx grok apply [--json]`;
|
|
29
30
|
|
|
30
31
|
const CLIENT_USAGE = `Usage:
|
|
31
|
-
ocx integration client [status] [--client <id>] [--json]
|
|
32
|
-
ocx integration client <enable|disable> --client <id> [--overwrite-conflict] [--json]
|
|
33
|
-
ocx integration client history [--client <id>] [--json]
|
|
34
|
-
ocx integration client restore --op <opId> [--confirm-drift] [--json]
|
|
32
|
+
ocx integration client [status] [--client <id>] [--profile <id>] [--json]
|
|
33
|
+
ocx integration client <enable|disable> --client <id> [--profile <id>] [--overwrite-conflict] [--json]
|
|
34
|
+
ocx integration client history [--client <id>] [--profile <id>] [--json]
|
|
35
|
+
ocx integration client restore --op <opId> [--client aside --profile <id>] [--confirm-drift] [--json]
|
|
36
|
+
--profile selects one Aside account-backed profile; omitted Aside toggles affect all profiles.`;
|
|
37
|
+
|
|
38
|
+
function validateAsideProfile(profile: string | undefined, client: string | undefined): void {
|
|
39
|
+
if (profile === undefined) return;
|
|
40
|
+
if (client !== "aside") throw new CliUsageError("--profile requires --client aside", CLIENT_USAGE);
|
|
41
|
+
if (!/^(0|[1-9][0-9]*)$/.test(profile) || !Number.isSafeInteger(Number(profile))) {
|
|
42
|
+
throw new CliUsageError("--profile must be a nonnegative integer account ID", CLIENT_USAGE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function clientIntegrationPath(client: string, profile?: string): string {
|
|
48
|
+
const base = `/api/client-integrations/${encodeURIComponent(client)}`;
|
|
49
|
+
return client === "aside" ? `${base}/profiles${profile === undefined ? "" : `/${encodeURIComponent(profile)}`}` : base;
|
|
50
|
+
}
|
|
35
51
|
|
|
36
52
|
function parseMap(raw: string): Record<string, string> {
|
|
37
53
|
if (raw === "-") return {};
|
|
@@ -163,16 +179,23 @@ export async function handleClientIntegrationCommand(
|
|
|
163
179
|
const args = [...argv];
|
|
164
180
|
const action = (args.shift() ?? "status").toLowerCase();
|
|
165
181
|
const wantsJson = takeFlag(args, "--json");
|
|
182
|
+
const profile = takeOption(args, "--profile");
|
|
166
183
|
|
|
167
184
|
if (action === "status" || action === "show" || action === "list") {
|
|
168
185
|
const client = takeOption(args, "--client");
|
|
186
|
+
validateAsideProfile(profile, client);
|
|
169
187
|
rejectArgs(args, CLIENT_USAGE);
|
|
170
188
|
const path = client
|
|
171
|
-
?
|
|
189
|
+
? clientIntegrationPath(client, profile)
|
|
172
190
|
: "/api/client-integrations";
|
|
173
191
|
const result = await runtimeRequest(path, {}, deps);
|
|
174
192
|
const rows = (result as { clients?: Array<Record<string, unknown>> }).clients;
|
|
175
|
-
|
|
193
|
+
const profiles = (result as { profiles?: Array<Record<string, unknown>> }).profiles;
|
|
194
|
+
printData(result, wantsJson, profiles
|
|
195
|
+
? profiles.length > 0
|
|
196
|
+
? profiles.map(row => `${String(row.profileId)} ${String(row.name ?? "Aside")}: ${row.enabled ? "on" : "off"} (${String(row.state)})${row.current ? " [current]" : ""}`)
|
|
197
|
+
: [String((result as { error?: string }).error ?? "No Aside profiles found.")]
|
|
198
|
+
: rows
|
|
176
199
|
? rows.map(row => `${String(row.clientId)}: ${String(row.state)}${row.installed ? "" : " (not installed)"}`)
|
|
177
200
|
: summaryLines(result));
|
|
178
201
|
return;
|
|
@@ -180,9 +203,11 @@ export async function handleClientIntegrationCommand(
|
|
|
180
203
|
|
|
181
204
|
if (action === "history" || action === "journal") {
|
|
182
205
|
const client = takeOption(args, "--client");
|
|
206
|
+
validateAsideProfile(profile, client);
|
|
183
207
|
rejectArgs(args, CLIENT_USAGE);
|
|
184
|
-
const
|
|
185
|
-
|
|
208
|
+
const path = client === "aside" ? `${clientIntegrationPath(client, profile)}/journal`
|
|
209
|
+
: `/api/client-integrations/journal${client ? `?client=${encodeURIComponent(client)}` : ""}`;
|
|
210
|
+
const result = await runtimeRequest(path, {}, deps);
|
|
186
211
|
const operations = (result as { operations?: Array<Record<string, unknown>> }).operations ?? [];
|
|
187
212
|
printData(result, wantsJson, operations.length === 0
|
|
188
213
|
? ["No integration operations recorded yet."]
|
|
@@ -190,7 +215,8 @@ export async function handleClientIntegrationCommand(
|
|
|
190
215
|
// `snapshot` is resolved against the disk by the route, so "expired"
|
|
191
216
|
// here means the bytes are genuinely gone, not merely old.
|
|
192
217
|
const backup = row.snapshot === "expired" ? "backup expired" : `op ${String(row.opId)}`;
|
|
193
|
-
|
|
218
|
+
const owner = row.profileId === undefined ? String(row.clientId) : `${String(row.clientId)}:${String(row.profileId)}`;
|
|
219
|
+
return `${String(row.at)} ${owner} ${String(row.kind)} (${backup})`;
|
|
194
220
|
}));
|
|
195
221
|
return;
|
|
196
222
|
}
|
|
@@ -198,9 +224,12 @@ export async function handleClientIntegrationCommand(
|
|
|
198
224
|
if (action === "restore") {
|
|
199
225
|
const opId = takeOption(args, "--op") ?? takeOption(args, "--op-id");
|
|
200
226
|
const confirmDrift = takeFlag(args, "--confirm-drift");
|
|
227
|
+
const client = takeOption(args, "--client");
|
|
228
|
+
validateAsideProfile(profile, client);
|
|
229
|
+
if (client !== undefined && profile === undefined) throw new CliUsageError("restore --client requires --profile", CLIENT_USAGE);
|
|
201
230
|
rejectArgs(args, CLIENT_USAGE);
|
|
202
231
|
if (!opId) throw new CliUsageError("--op <opId> is required", CLIENT_USAGE);
|
|
203
|
-
const result = await runtimeRequest("/api/client-integrations/restore", {
|
|
232
|
+
const result = await runtimeRequest(profile === undefined ? "/api/client-integrations/restore" : `${clientIntegrationPath("aside", profile)}/restore`, {
|
|
204
233
|
method: "POST",
|
|
205
234
|
body: JSON.stringify({ opId, confirmDrift }),
|
|
206
235
|
}, deps);
|
|
@@ -212,6 +241,7 @@ export async function handleClientIntegrationCommand(
|
|
|
212
241
|
throw new CliUsageError(`unknown client integration command ${action}`, CLIENT_USAGE);
|
|
213
242
|
}
|
|
214
243
|
const client = takeOption(args, "--client");
|
|
244
|
+
validateAsideProfile(profile, client);
|
|
215
245
|
/*
|
|
216
246
|
* The conflict escape hatch, spelled the way `restore --confirm-drift` is: the
|
|
217
247
|
* refusal is the default and the waiver has to be typed.
|
|
@@ -232,7 +262,7 @@ export async function handleClientIntegrationCommand(
|
|
|
232
262
|
if (overwriteConflict && action === "disable") {
|
|
233
263
|
throw new CliUsageError("--overwrite-conflict applies only to enable", CLIENT_USAGE);
|
|
234
264
|
}
|
|
235
|
-
const result = await runtimeRequest(
|
|
265
|
+
const result = await runtimeRequest(clientIntegrationPath(client, profile), {
|
|
236
266
|
method: "PUT",
|
|
237
267
|
// Sent only when asked for, so a proxy on an older build sees the request it
|
|
238
268
|
// has always seen rather than an unknown field.
|
|
@@ -240,7 +270,11 @@ export async function handleClientIntegrationCommand(
|
|
|
240
270
|
? { enabled: true, overwriteConflict: true }
|
|
241
271
|
: { enabled: action === "enable" }),
|
|
242
272
|
}, deps);
|
|
243
|
-
|
|
273
|
+
const batch = result as { ok?: boolean; message?: string; results?: Array<Record<string, unknown>> };
|
|
274
|
+
printData(result, wantsJson, batch.results
|
|
275
|
+
? batch.results.map(row => `aside:${String(row.profileId)} ${String(row.message ?? (row.ok ? "updated" : "refused"))}${row.residual === true ? " Recovery did not finish." : ""}${typeof row.snapshotPath === "string" ? ` Backup: ${row.snapshotPath}` : ""}`)
|
|
276
|
+
: [String(batch.message ?? `${client} ${action}d.`)]);
|
|
277
|
+
if (batch.ok === false) throw new RuntimeApiError(batch.message ?? "Some Aside profiles could not be updated", 207, result);
|
|
244
278
|
});
|
|
245
279
|
}
|
|
246
280
|
|
package/src/cli/opencode.ts
CHANGED
|
@@ -39,7 +39,7 @@ import type {
|
|
|
39
39
|
OpencodeProviderBlocks,
|
|
40
40
|
OpencodeV2ProviderBlock,
|
|
41
41
|
} from "../clients/config-export";
|
|
42
|
-
import { visibleNativeSlugs } from "../codex/catalog";
|
|
42
|
+
import { filterCatalogVisibleModels, visibleNativeSlugs } from "../codex/catalog";
|
|
43
43
|
import { commandInvocation } from "../lib/win-exec";
|
|
44
44
|
import { loadServiceTokenFromFile, serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
45
45
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
@@ -376,12 +376,17 @@ export function opencodeCatalogFromProxyRows(
|
|
|
376
376
|
config: OcxConfig,
|
|
377
377
|
): OpencodeCatalogModel[] {
|
|
378
378
|
const omitNative = providerCodexAccountMode("openai", config.providers?.openai) === "direct";
|
|
379
|
+
const routedRows = rows.filter((row): row is OpencodeProxyModelRow & { provider: string; id: string } =>
|
|
380
|
+
row.native !== true && typeof row.provider === "string" && typeof row.id === "string");
|
|
381
|
+
const visibleRouted = new Set<OpencodeProxyModelRow>(filterCatalogVisibleModels(routedRows, config));
|
|
379
382
|
const seen = new Set<string>();
|
|
380
383
|
const catalog: OpencodeCatalogModel[] = [];
|
|
381
384
|
for (const row of rows) {
|
|
382
385
|
const namespaced = row.namespaced?.trim();
|
|
383
386
|
if (!namespaced || row.disabled === true) continue;
|
|
384
387
|
if (omitNative && row.native === true) continue;
|
|
388
|
+
if (row.native !== true && typeof row.provider === "string" && typeof row.id === "string"
|
|
389
|
+
&& !visibleRouted.has(row)) continue;
|
|
385
390
|
if (seen.has(namespaced)) continue;
|
|
386
391
|
seen.add(namespaced);
|
|
387
392
|
catalog.push({
|
|
@@ -632,14 +637,14 @@ export function opencodeNotFoundHint(
|
|
|
632
637
|
}
|
|
633
638
|
|
|
634
639
|
export async function cmdOpencode(args: string[]): Promise<number> {
|
|
635
|
-
const
|
|
636
|
-
const live = await ensureProxyForOpencode(
|
|
640
|
+
const startupConfig = loadConfig();
|
|
641
|
+
const live = await ensureProxyForOpencode(startupConfig);
|
|
637
642
|
if (!live) {
|
|
638
643
|
console.error("❌ Proxy did not become healthy after starting.");
|
|
639
644
|
return 1;
|
|
640
645
|
}
|
|
641
646
|
|
|
642
|
-
const apiKey = opencodeApiKey(
|
|
647
|
+
const apiKey = opencodeApiKey(startupConfig);
|
|
643
648
|
let proxyModels: OpencodeProxyModelRow[];
|
|
644
649
|
try {
|
|
645
650
|
proxyModels = await fetchOpencodeProxyModels(live, apiKey);
|
|
@@ -648,6 +653,8 @@ export async function cmdOpencode(args: string[]): Promise<number> {
|
|
|
648
653
|
console.error(`❌ Could not fetch the model catalog from the proxy: ${reason}`);
|
|
649
654
|
return 1;
|
|
650
655
|
}
|
|
656
|
+
// /api/models may have completed and persisted initial provider selection.
|
|
657
|
+
const config = loadConfig();
|
|
651
658
|
const catalog = opencodeCatalogFromProxyRows(proxyModels, config);
|
|
652
659
|
const blocks = buildOpencodeProviderBlocksFromCatalog(live.port, catalog, live.hostname, config);
|
|
653
660
|
const baseUrl = blocks.v1.options.baseURL;
|
package/src/cli/runtime-api.ts
CHANGED
|
@@ -44,7 +44,7 @@ export class RuntimeApiError extends Error {
|
|
|
44
44
|
|
|
45
45
|
export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string> {
|
|
46
46
|
if (deps.baseUrl) return deps.baseUrl.replace(/\/$/, "");
|
|
47
|
-
const live = await findLiveProxy();
|
|
47
|
+
const live = await (deps.findLiveProxy ?? findLiveProxy)();
|
|
48
48
|
if (!live) throw new RuntimeApiError("Proxy is not running. Start it with: ocx start", 503, null);
|
|
49
49
|
return `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
50
50
|
}
|
|
@@ -79,7 +79,12 @@ function responseMessage(body: unknown, status: number): string {
|
|
|
79
79
|
if (reason && reason !== primary) parts.push(`reason: ${reason}`);
|
|
80
80
|
const hint = stringField(record, "hint");
|
|
81
81
|
if (hint && hint !== primary) parts.push(`hint: ${hint}`);
|
|
82
|
-
|
|
82
|
+
const snapshotPath = stringField(record, "snapshotPath");
|
|
83
|
+
const recovery = [
|
|
84
|
+
...(snapshotPath ? [`Backup: ${snapshotPath.slice(0, 32768)}`] : []),
|
|
85
|
+
...(record.residual === true ? ["Automatic recovery did not finish; check the client configuration before retrying."] : []),
|
|
86
|
+
];
|
|
87
|
+
return [parts.join("\n").slice(0, 1200), ...recovery].join("\n");
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
export async function runtimeRequest<T = unknown>(
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { getConfigDir } from "../config";
|
|
2
|
+
import { disconnectClient } from "../client/connect";
|
|
3
|
+
import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
|
|
4
|
+
import { assertClientLifecycleHeld, withClientLifecycle } from "../client/lifecycle-lock";
|
|
5
|
+
import { inspectRemoteDesktopCleanup, readDesktopDisconnectReceipt } from "../claude/desktop-remote-store";
|
|
6
|
+
import { removeOwnedConfigState, type ConfigRemovalResult } from "../lib/config-ownership";
|
|
7
|
+
import { sharedTeardownAuthorized, type UninstallObservation } from "./uninstall-plan";
|
|
8
|
+
|
|
9
|
+
export interface UninstallClientStateDeps {
|
|
10
|
+
readConnection: typeof readClientConnectionState;
|
|
11
|
+
inspectDesktop: typeof inspectRemoteDesktopCleanup;
|
|
12
|
+
readReceipt: typeof readDesktopDisconnectReceipt;
|
|
13
|
+
disconnect: (options?: Parameters<typeof disconnectClient>[0]) => Promise<unknown>;
|
|
14
|
+
withLifecycle: typeof withClientLifecycle;
|
|
15
|
+
remove: () => ConfigRemovalResult;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const defaults: UninstallClientStateDeps = {
|
|
19
|
+
readConnection: readClientConnectionState,
|
|
20
|
+
inspectDesktop: inspectRemoteDesktopCleanup,
|
|
21
|
+
readReceipt: readDesktopDisconnectReceipt,
|
|
22
|
+
disconnect: options => disconnectClient(options),
|
|
23
|
+
withLifecycle: withClientLifecycle,
|
|
24
|
+
remove: () => removeOwnedConfigState(getConfigDir()),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Restore connection-owned client artifacts before removing their ownership/recovery records. */
|
|
28
|
+
export async function removeOwnedConfigAfterDesktopCleanup(
|
|
29
|
+
observed: UninstallObservation,
|
|
30
|
+
deps: UninstallClientStateDeps = defaults,
|
|
31
|
+
): Promise<ConfigRemovalResult> {
|
|
32
|
+
if (!sharedTeardownAuthorized(observed)) {
|
|
33
|
+
throw new Error("Client cleanup refused: service or proxy teardown is not proven.");
|
|
34
|
+
}
|
|
35
|
+
const state = deps.readConnection();
|
|
36
|
+
if (state.kind !== "connected" && state.kind !== "disconnected") {
|
|
37
|
+
throw new Error("Client cleanup refused: connection state is invalid or mismatched.");
|
|
38
|
+
}
|
|
39
|
+
const desktop = deps.inspectDesktop();
|
|
40
|
+
const receipt = deps.readReceipt();
|
|
41
|
+
if (desktop.kind === "unsafe" || receipt.kind === "unsafe") {
|
|
42
|
+
throw new Error("Client cleanup refused: Desktop recovery state is unsafe.");
|
|
43
|
+
}
|
|
44
|
+
const interrupted = receipt.kind === "valid" && receipt.value.phase !== "complete" ? receipt.value : undefined;
|
|
45
|
+
if ((state.kind === "connected" && desktop.kind !== "absent" && !sameClientConnectionOwner(state.value, desktop.owner))
|
|
46
|
+
|| (interrupted && state.kind === "connected" && !sameClientConnectionOwner(state.value, interrupted.owner))
|
|
47
|
+
|| (interrupted && desktop.kind !== "absent" && !sameClientConnectionOwner(desktop.owner, interrupted.owner))) {
|
|
48
|
+
throw new Error("Client cleanup refused: Desktop recovery ownership is mismatched.");
|
|
49
|
+
}
|
|
50
|
+
if (state.kind === "connected" || interrupted) {
|
|
51
|
+
// Disconnect owns its N/L/C ordering. Do not hold L across this call, and
|
|
52
|
+
// preserve the frozen catalog choice when resuming an interrupted operation.
|
|
53
|
+
await deps.disconnect({
|
|
54
|
+
expectedOwner: state.kind === "connected" ? {
|
|
55
|
+
serverUrl: state.value.serverUrl, apiKeyId: state.value.apiKeyId, connectedAt: state.value.connectedAt,
|
|
56
|
+
} : interrupted!.owner,
|
|
57
|
+
...(interrupted ? { keepCatalog: interrupted.keepCatalog } : {}),
|
|
58
|
+
});
|
|
59
|
+
} else if (desktop.kind !== "absent") {
|
|
60
|
+
throw new Error("Client cleanup refused: finish Desktop recovery before uninstalling.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A connection can appear while asynchronous cleanup is finishing. The final
|
|
64
|
+
// inspection and actual remover share L, but never hold C while deleting its DB.
|
|
65
|
+
return deps.withLifecycle(async held => {
|
|
66
|
+
assertClientLifecycleHeld(held);
|
|
67
|
+
const latest = deps.readConnection();
|
|
68
|
+
const latestDesktop = deps.inspectDesktop();
|
|
69
|
+
const latestReceipt = deps.readReceipt();
|
|
70
|
+
if (latest.kind !== "disconnected"
|
|
71
|
+
|| latestDesktop.kind !== "absent"
|
|
72
|
+
|| latestReceipt.kind === "unsafe"
|
|
73
|
+
|| (latestReceipt.kind === "valid" && latestReceipt.value.phase !== "complete")) {
|
|
74
|
+
throw new Error("Client cleanup refused: connection or Desktop state changed before removal.");
|
|
75
|
+
}
|
|
76
|
+
return deps.remove();
|
|
77
|
+
});
|
|
78
|
+
}
|