184 lines · 5.7 KB · TypeScript
RawDownload
import { normalizeProjectPathForComparison } from "@t3tools/shared/path"; export type FirstRunDecision = "pending" | "app" | "wizard"; export interface FirstRunGateState {  readonly decision: FirstRunDecision;  readonly stalled: boolean;} type FirstRunGateEvent =  | { readonly type: "evidence"; readonly decision: FirstRunDecision }  | { readonly type: "timeout" }; interface FirstRunWorkspaceInput {  readonly primaryEnvironmentId: string | null;  readonly serverCwd: string | null;  readonly bootstrapProjectId?: string | undefined;  readonly bootstrapThreadId?: string | undefined;  readonly bootstrapProjectCreated?: boolean | undefined;  readonly bootstrapThreadCreated?: boolean | undefined;  readonly projects: ReadonlyArray<{    readonly id: string;    readonly environmentId: string;    readonly workspaceRoot: string;  }>;  readonly threads: ReadonlyArray<{    readonly id: string;    readonly projectId: string;    readonly environmentId: string;    readonly latestTurn: unknown;    readonly latestUserMessageAt: string | null;    readonly session: unknown;  }>;} interface FirstRunDecisionInput {  readonly enabled: boolean;  readonly hydrated: boolean;  readonly completed: boolean;  readonly bootstrapped: boolean;  readonly authoritative: boolean;  readonly workspaceAuthoritative: boolean;  readonly workspaceProvenanceAuthoritative: boolean;  readonly catalogReady: boolean;  readonly serverConfigAvailable: boolean;  readonly workspaceFresh: boolean;  readonly projectCount: number;  readonly threadCount: number;} interface HostedFirstRunDecisionInput {  readonly hydrated: boolean;  readonly completed: boolean;  readonly catalogReady: boolean;  readonly environmentCount: number;} export function isFirstRunWorkspaceProvenanceAuthoritative(input: {  readonly welcomeReceived: boolean;  readonly bootstrapStatus: "pending" | "complete" | null;}): boolean {  // An empty catalog is not final while cwd auto-bootstrap is pending. Older  // servers omit bootstrapStatus, so a received welcome with null stays valid.  return input.welcomeReceived && input.bootstrapStatus !== "pending";} /** Keeps the authenticated app unmounted until workspace evidence settles. */export function transitionFirstRunGateState(  state: FirstRunGateState,  event: FirstRunGateEvent,): FirstRunGateState {  if (event.type === "timeout") {    return state.decision === "pending" && !state.stalled ? { ...state, stalled: true } : state;  }   if (    state.decision === "wizard" ||    event.decision === "pending" ||    (state.decision === "app" && event.decision !== "wizard")  ) {    return state;  }   return { decision: event.decision, stalled: false };} /** Only a project and thread created by this startup count as a fresh nonempty workspace. */export function isFreshFirstRunWorkspace(input: FirstRunWorkspaceInput): boolean {  if (input.projects.length > 1 || input.threads.length > 1) {    return false;  }   const bootstrapProject = input.projects[0];  if (bootstrapProject !== undefined) {    if (      input.bootstrapProjectCreated !== true ||      input.bootstrapProjectId !== bootstrapProject.id ||      input.serverCwd === null ||      bootstrapProject.environmentId !== input.primaryEnvironmentId ||      normalizeProjectPathForComparison(bootstrapProject.workspaceRoot) !==        normalizeProjectPathForComparison(input.serverCwd)    ) {      return false;    }  }   const bootstrapThread = input.threads[0];  if (bootstrapThread === undefined) {    return true;  }   return (    bootstrapProject !== undefined &&    input.bootstrapThreadCreated === true &&    input.bootstrapThreadId === bootstrapThread.id &&    bootstrapThread.environmentId === input.primaryEnvironmentId &&    bootstrapThread.projectId === bootstrapProject.id &&    bootstrapThread.latestTurn === null &&    bootstrapThread.latestUserMessageAt === null &&    bootstrapThread.session === null  );} /** Cached projects may open the app, but only live workspace data may complete onboarding. */export function resolveFirstRunDecision(input: FirstRunDecisionInput): {  readonly decision: FirstRunDecision;  readonly persistCompletion: boolean;} {  if (!input.enabled || (input.hydrated && input.completed)) {    return { decision: "app", persistCompletion: false };  }   if (!input.hydrated) {    return { decision: "pending", persistCompletion: false };  }   if (input.projectCount > 1 || input.threadCount > 1) {    return {      decision: "app",      persistCompletion:        input.bootstrapped &&        input.authoritative &&        input.workspaceAuthoritative &&        input.catalogReady &&        input.serverConfigAvailable,    };  }   if (    !input.bootstrapped ||    !input.authoritative ||    !input.workspaceProvenanceAuthoritative ||    !input.catalogReady ||    !input.serverConfigAvailable  ) {    return { decision: "pending", persistCompletion: false };  }   return input.workspaceFresh    ? { decision: "wizard", persistCompletion: false }    : { decision: "app", persistCompletion: input.workspaceAuthoritative };} /** Hosted onboarding depends on saved environments because there is no primary server. */export function resolveHostedFirstRunDecision(input: HostedFirstRunDecisionInput): {  readonly decision: FirstRunDecision;  readonly persistCompletion: boolean;} {  if (!input.hydrated) {    return { decision: "pending", persistCompletion: false };  }   if (input.completed) {    return { decision: "app", persistCompletion: false };  }   if (!input.catalogReady) {    return { decision: "pending", persistCompletion: false };  }   return input.environmentCount === 0    ? { decision: "wizard", persistCompletion: false }    : { decision: "app", persistCompletion: true };}