import type { HermesGatewayConnectionState, ProviderDriverKind } from "@t3tools/contracts"; export type WizardNavigation = | { readonly kind: "navigate"; readonly step: number } | { readonly kind: "blocked"; readonly step: number; readonly error: string }; const IDENTITY_STEP = 1;const INSTANCE_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/; export const ADD_PROVIDER_WIZARD_STEPS = ["Driver", "Identity", "Config"] as const; /** * Hermes thread bindings outlive a removed gateway, so a new gateway must * never derive its routing identity from a reusable display name. The UUID is * generated once by the dialog and remains stable across enrollment retries. */export function createHermesProviderInstanceId(label: string, randomUuid: () => string) { const suffix = randomUuid() .replace(/[^a-zA-Z0-9]/gu, "") .toLowerCase(); if (suffix.length === 0) { throw new Error("Could not generate a Hermes instance ID."); } const labelSlug = label .trim() .toLowerCase() .replace(/[^a-z0-9]+/gu, "-") .replace(/^-+|-+$/gu, "") .slice(0, 44); const shortSuffix = suffix.slice(0, 12); return labelSlug ? `hermes-${labelSlug}-${shortSuffix}` : `hermes-${shortSuffix}`;} /** * Command that installs the gateway plugin into the Hermes profile. * * Installation is a manual step performed on the Hermes host — T3 has no * remote access to it — so the dialog documents the command rather than * implying anything happens automatically. The script symlinks this repository * checkout into `~/.hermes/plugins` and enables the plugin. */export const HERMES_PLUGIN_INSTALL_COMMAND = "./integrations/hermes-t3-gateway/install.sh" as const; /** * Activates the pairing. The gateway only constructs the platform adapter at * startup, so it keeps reporting offline until it is restarted. */export const HERMES_GATEWAY_RESTART_COMMAND = "hermes gateway restart" as const; export function isHermesInstanceRemovedError(error: unknown): boolean { return ( typeof error === "object" && error !== null && "code" in error && error.code === "instance-removed" );} /** * Drop an instance the dialog itself just wrote, without clobbering anything * that changed underneath it. * * Used to roll back the settings write when Hermes enrollment fails. The * server is authoritative between the write and the rollback, so this reads * the *latest* map rather than restoring a captured pre-write snapshot — that * would silently revert instances added by another client in between. If the * id is already gone, or has been replaced by a different config object, the * write is not ours to undo and `null` signals "no rollback needed". */export function rollbackProviderInstances<T>(input: { readonly latest: Readonly<Record<string, T>>; readonly instanceId: string; readonly written: T;}): Record<string, T> | null { const current = input.latest[input.instanceId]; if (current === undefined) return null; if (current !== input.written) return null; const next = { ...input.latest }; delete next[input.instanceId]; return next;} export function isOwnedHermesEnrollmentRetry(input: { readonly driver: ProviderDriverKind; readonly instanceId: string; readonly createdHermesInstanceId: string | null;}): boolean { return ( input.driver === "hermes" && input.createdHermesInstanceId !== null && input.createdHermesInstanceId === input.instanceId );} /** * Presentation state of the post-enrollment "waiting for Hermes" panel. * * `waiting` is the expected steady state immediately after the command is * generated — the user still has to paste it and restart the gateway — so it * is deliberately not modelled as an error. `expired` and `blocked` are the * two states that need a fresh command, and `connected` is terminal success. */export type HermesWaitingPhase = "waiting" | "connected" | "blocked" | "expired"; export interface HermesWaitingState { readonly phase: HermesWaitingPhase; readonly statusLabelState: HermesGatewayConnectionState; /** True while T3 legitimately has nothing to report yet. */ readonly isPending: boolean; /** True when the only way forward is minting a replacement command. */ readonly needsNewCommand: boolean;} /** * Resolve what the waiting panel should show. * * Connection wins over expiry: once the gateway has dialled in, the one-time * token has already been consumed and its expiry is irrelevant. `revoked` and * `upgrade-required` are surfaced ahead of the generic wait because neither * resolves by waiting longer. */export function resolveHermesWaitingState(input: { readonly connectionState: HermesGatewayConnectionState | null; readonly isExpired: boolean;}): HermesWaitingState { if (input.connectionState === "connected") { return { phase: "connected", statusLabelState: "connected", isPending: false, needsNewCommand: false, }; } if (input.connectionState === "upgrade-required" || input.connectionState === "revoked") { return { phase: "blocked", statusLabelState: input.connectionState, isPending: false, needsNewCommand: input.connectionState === "revoked", }; } if (input.isExpired) { return { phase: "expired", statusLabelState: input.connectionState ?? "offline", isPending: false, needsNewCommand: true, }; } return { phase: "waiting", statusLabelState: input.connectionState ?? "offline", isPending: true, needsNewCommand: false, };} export function validateProviderInstanceIdForWizard(input: { readonly driver: ProviderDriverKind; readonly instanceId: string; readonly existingIds: ReadonlySet<string>; readonly createdHermesInstanceId: string | null;}): string | null { if (input.instanceId.length === 0) return "Instance ID is required."; if (input.instanceId.length > 64) return "Instance ID must be 64 characters or fewer."; if (!INSTANCE_ID_PATTERN.test(input.instanceId)) { return "Instance ID must start with a letter and use only letters, digits, '-', or '_'."; } if ( input.existingIds.has(input.instanceId) && !isOwnedHermesEnrollmentRetry({ driver: input.driver, instanceId: input.instanceId, createdHermesInstanceId: input.createdHermesInstanceId, }) ) { return `An instance named '${input.instanceId}' already exists.`; } return null;} /** * Resolve navigation within the add-provider wizard. * * Moving forward past Identity requires a valid instance id, whether the user * advances one step at a time or skips directly to Config from a step header. * A blocked skip lands on Identity so its existing inline validation is * visible. Backward navigation is always preserved. */export function resolveWizardNavigation( currentStep: number, requestedStep: number, stepCount: number, validation: { readonly instanceIdError: string | null },): WizardNavigation { const lastStep = Math.max(0, stepCount - 1); const targetStep = Math.max(0, Math.min(lastStep, requestedStep)); const movesForwardPastIdentity = currentStep <= IDENTITY_STEP && targetStep > IDENTITY_STEP; if (movesForwardPastIdentity && validation.instanceIdError !== null) { return { kind: "blocked", step: Math.min(IDENTITY_STEP, lastStep), error: validation.instanceIdError, }; } return { kind: "navigate", step: targetStep };}
import type { HermesGatewayConnectionState, ProviderDriverKind } from "@t3tools/contracts";
export type WizardNavigation =
| { readonly kind: "navigate"; readonly step: number }
| { readonly kind: "blocked"; readonly step: number; readonly error: string };
const IDENTITY_STEP = 1;
const INSTANCE_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
export const ADD_PROVIDER_WIZARD_STEPS = ["Driver", "Identity", "Config"] as const;
/**
* Hermes thread bindings outlive a removed gateway, so a new gateway must
* never derive its routing identity from a reusable display name. The UUID is
* generated once by the dialog and remains stable across enrollment retries.
*/
export function createHermesProviderInstanceId(label: string, randomUuid: () => string) {
const suffix = randomUuid()
.replace(/[^a-zA-Z0-9]/gu, "")
.toLowerCase();
if (suffix.length === 0) {
throw new Error("Could not generate a Hermes instance ID.");
}
const labelSlug = label
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/gu, "-")
.replace(/^-+|-+$/gu, "")
.slice(0, 44);
const shortSuffix = suffix.slice(0, 12);
return labelSlug ? `hermes-${labelSlug}-${shortSuffix}` : `hermes-${shortSuffix}`;
}
/**
* Command that installs the gateway plugin into the Hermes profile.
*
* Installation is a manual step performed on the Hermes host — T3 has no
* remote access to it — so the dialog documents the command rather than
* implying anything happens automatically. The script symlinks this repository
* checkout into `~/.hermes/plugins` and enables the plugin.
*/
export const HERMES_PLUGIN_INSTALL_COMMAND = "./integrations/hermes-t3-gateway/install.sh" as const;
/**
* Activates the pairing. The gateway only constructs the platform adapter at
* startup, so it keeps reporting offline until it is restarted.
*/
export const HERMES_GATEWAY_RESTART_COMMAND = "hermes gateway restart" as const;
export function isHermesInstanceRemovedError(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "instance-removed"
);
}
/**
* Drop an instance the dialog itself just wrote, without clobbering anything
* that changed underneath it.
*
* Used to roll back the settings write when Hermes enrollment fails. The
* server is authoritative between the write and the rollback, so this reads
* the *latest* map rather than restoring a captured pre-write snapshot — that
* would silently revert instances added by another client in between. If the
* id is already gone, or has been replaced by a different config object, the
* write is not ours to undo and `null` signals "no rollback needed".
*/
export function rollbackProviderInstances<T>(input: {
readonly latest: Readonly<Record<string, T>>;
readonly instanceId: string;
readonly written: T;
}): Record<string, T> | null {
const current = input.latest[input.instanceId];
if (current === undefined) return null;
if (current !== input.written) return null;
const next = { ...input.latest };
delete next[input.instanceId];
return next;
}
export function isOwnedHermesEnrollmentRetry(input: {
readonly driver: ProviderDriverKind;
readonly instanceId: string;
readonly createdHermesInstanceId: string | null;
}): boolean {
return (
input.driver === "hermes" &&
input.createdHermesInstanceId !== null &&
input.createdHermesInstanceId === input.instanceId
);
}
/**
* Presentation state of the post-enrollment "waiting for Hermes" panel.
*
* `waiting` is the expected steady state immediately after the command is
* generated — the user still has to paste it and restart the gateway — so it
* is deliberately not modelled as an error. `expired` and `blocked` are the
* two states that need a fresh command, and `connected` is terminal success.
*/
export type HermesWaitingPhase = "waiting" | "connected" | "blocked" | "expired";
export interface HermesWaitingState {
readonly phase: HermesWaitingPhase;
readonly statusLabelState: HermesGatewayConnectionState;
/** True while T3 legitimately has nothing to report yet. */
readonly isPending: boolean;
/** True when the only way forward is minting a replacement command. */
readonly needsNewCommand: boolean;
}
/**
* Resolve what the waiting panel should show.
*
* Connection wins over expiry: once the gateway has dialled in, the one-time
* token has already been consumed and its expiry is irrelevant. `revoked` and
* `upgrade-required` are surfaced ahead of the generic wait because neither
* resolves by waiting longer.
*/
export function resolveHermesWaitingState(input: {
readonly connectionState: HermesGatewayConnectionState | null;
readonly isExpired: boolean;
}): HermesWaitingState {
if (input.connectionState === "connected") {
return {
phase: "connected",
statusLabelState: "connected",
isPending: false,
needsNewCommand: false,
};
}
if (input.connectionState === "upgrade-required" || input.connectionState === "revoked") {
return {
phase: "blocked",
statusLabelState: input.connectionState,
isPending: false,
needsNewCommand: input.connectionState === "revoked",
};
}
if (input.isExpired) {
return {
phase: "expired",
statusLabelState: input.connectionState ?? "offline",
isPending: false,
needsNewCommand: true,
};
}
return {
phase: "waiting",
statusLabelState: input.connectionState ?? "offline",
isPending: true,
needsNewCommand: false,
};
}
export function validateProviderInstanceIdForWizard(input: {
readonly driver: ProviderDriverKind;
readonly instanceId: string;
readonly existingIds: ReadonlySet<string>;
readonly createdHermesInstanceId: string | null;
}): string | null {
if (input.instanceId.length === 0) return "Instance ID is required.";
if (input.instanceId.length > 64) return "Instance ID must be 64 characters or fewer.";
if (!INSTANCE_ID_PATTERN.test(input.instanceId)) {
return "Instance ID must start with a letter and use only letters, digits, '-', or '_'.";
}
if (
input.existingIds.has(input.instanceId) &&
!isOwnedHermesEnrollmentRetry({
driver: input.driver,
instanceId: input.instanceId,
createdHermesInstanceId: input.createdHermesInstanceId,
})
) {
return `An instance named '${input.instanceId}' already exists.`;
}
return null;
}
/**
* Resolve navigation within the add-provider wizard.
*
* Moving forward past Identity requires a valid instance id, whether the user
* advances one step at a time or skips directly to Config from a step header.
* A blocked skip lands on Identity so its existing inline validation is
* visible. Backward navigation is always preserved.
*/
export function resolveWizardNavigation(
currentStep: number,
requestedStep: number,
stepCount: number,
validation: { readonly instanceIdError: string | null },
): WizardNavigation {
const lastStep = Math.max(0, stepCount - 1);
const targetStep = Math.max(0, Math.min(lastStep, requestedStep));
const movesForwardPastIdentity = currentStep <= IDENTITY_STEP && targetStep > IDENTITY_STEP;
if (movesForwardPastIdentity && validation.instanceIdError !== null) {
return {
kind: "blocked",
step: Math.min(IDENTITY_STEP, lastStep),
error: validation.instanceIdError,
};
}
return { kind: "navigate", step: targetStep };
}