From 6b753bf4ebf69d157170c477b96a9ae8daa19b16 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 27 May 2026 15:18:22 -0700 Subject: [PATCH 1/4] docs(onboard): document FSM migration target Signed-off-by: Carlos Villela --- src/lib/onboard/machine/README.md | 111 ++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/lib/onboard/machine/README.md diff --git a/src/lib/onboard/machine/README.md b/src/lib/onboard/machine/README.md new file mode 100644 index 0000000000..752e8959f4 --- /dev/null +++ b/src/lib/onboard/machine/README.md @@ -0,0 +1,111 @@ + + + +# Onboard finite-state machine + +This directory contains the transitional onboarding finite-state-machine (FSM) layer. The current implementation records coarse state snapshots and emits machine events while the legacy `src/lib/onboard.ts` entrypoint is split into explicit state handlers. + +## Target architecture + +The target shape is a machine-driven onboarding runner: + +1. Normalize CLI flags, environment, session locking, and consent in `src/lib/onboard.ts`. +2. Build an onboarding context that contains sanitized operator choices, runtime dependencies, and mutable values returned by states. +3. Enter `runOnboardMachine(context)`. +4. Dispatch the current machine state to a handler. +5. Let the handler return an explicit state result such as advance, retry, branch, complete, or failed. +6. Apply the result through `OnboardRuntime`, which validates the transition, updates the persisted session snapshot, and emits redacted machine events. +7. Continue until the machine reaches `complete` or `failed`. + +In that final shape, `src/lib/onboard.ts` should be a thin entrypoint. State handlers should own state-specific prompts, resume validation, repair decisions, and side effects. + +## State ownership + +Machine states are coarse user-visible onboarding phases, not every subprocess or probe inside a phase. The current vocabulary is intentionally limited to major boundaries: + +- `init` +- `preflight` +- `gateway` +- `provider_selection` +- `inference` +- `sandbox` +- `openclaw` or `agent_setup` +- `policies` +- `finalizing` +- `post_verify` +- `complete` or `failed` + +A state handler may perform many smaller operations, but it should expose only stable, redacted state transitions and context updates to the FSM. + +## Session steps versus machine state + +The persisted onboarding session still tracks step-level progress for resumability. Step recording is older than the FSM and is currently used as a compatibility bridge. + +Long term: + +- `OnboardRuntime` should own machine transitions and machine revision increments. +- Session step helpers should record only step status (`pending`, `in_progress`, `complete`, `failed`, `skipped`). +- State handlers should return explicit results instead of implicitly moving the machine by calling step helpers. + +Until that migration completes, step helpers may still infer machine snapshots for compatibility with older sessions and tests. + +## Handler contract + +Each state handler should eventually follow this shape: + +```ts +type OnboardStateHandler = (context: OnboardContext) => Promise; +``` + +A handler should: + +- validate whether the state can be resumed or skipped; +- run state-local repairs before declaring a cached step reusable; +- perform the phase side effects; +- return the next state explicitly; +- keep secrets out of returned metadata and event context. + +A handler should not: + +- mutate the machine snapshot directly; +- jump to states outside the declared transition graph; +- rely on console output as the only observable diagnostic; +- store raw credentials, provider URLs with secrets, or other sensitive values in machine context. + +## Runtime responsibilities + +`OnboardRuntime` is the intended authority for: + +- validating transitions against `transitions.ts`; +- applying safe session context updates; +- marking terminal states; +- emitting redacted lifecycle, state, repair, resume-conflict, and hook events; +- preserving compatibility with normalized older sessions. + +The runtime should reject invalid transitions before they can be persisted. + +## Event semantics + +Machine events are diagnostics and automation hooks. They must be safe to write to JSONL logs and attach to CI/E2E artifacts. + +Event payloads should include only stable, redacted context such as: + +- selected agent; +- sandbox name; +- provider and model names; +- endpoint origin, not full secret-bearing URLs; +- credential environment variable name, not credential value; +- policy presets and messaging channel names. + +Observers and hooks must not change onboarding behavior. A failing hook should emit hook failure diagnostics and let onboarding continue. + +## Migration stages + +The FSM migration is considered complete when: + +1. state metadata is defined once and derived by session, event, progress, and transition code; +2. live onboarding emits `onboard.started`, `onboard.resumed`, `resume.conflict`, terminal, state, skip, repair, and context events consistently; +3. handlers return explicit state results; +4. the runner applies all handler results through `OnboardRuntime`; +5. step helpers no longer implicitly own machine transitions; +6. `src/lib/onboard.ts` contains entrypoint setup and dependency wiring rather than state sequencing. From fb1b32d0a8725934d3c49e77ca375abdcedf2c81 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 27 May 2026 15:19:42 -0700 Subject: [PATCH 2/4] refactor(onboard): centralize machine state metadata Signed-off-by: Carlos Villela --- src/lib/onboard/machine/definition.test.ts | 85 ++++++++++++++++ src/lib/onboard/machine/definition.ts | 108 +++++++++++++++++++++ src/lib/onboard/machine/types.ts | 43 +++----- 3 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 src/lib/onboard/machine/definition.test.ts create mode 100644 src/lib/onboard/machine/definition.ts diff --git a/src/lib/onboard/machine/definition.test.ts b/src/lib/onboard/machine/definition.test.ts new file mode 100644 index 0000000000..7fffa49ec5 --- /dev/null +++ b/src/lib/onboard/machine/definition.test.ts @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + getOnboardMachineStateDefinition, + ONBOARD_MACHINE_NON_TERMINAL_STATE_IDS, + ONBOARD_MACHINE_STATE_DEFINITIONS, + ONBOARD_MACHINE_STATE_IDS, + ONBOARD_MACHINE_TERMINAL_STATE_IDS, +} from "./definition"; + +const expectedStateOrder = [ + "init", + "preflight", + "gateway", + "provider_selection", + "inference", + "sandbox", + "agent_setup", + "openclaw", + "policies", + "finalizing", + "post_verify", + "complete", + "failed", +]; + +describe("onboard machine definition", () => { + it("is the canonical ordered state catalog", () => { + expect(ONBOARD_MACHINE_STATE_IDS).toEqual(expectedStateOrder); + expect(ONBOARD_MACHINE_STATE_DEFINITIONS.map((definition) => definition.state)).toEqual( + expectedStateOrder, + ); + }); + + it("derives terminal and non-terminal state catalogs from the same vocabulary", () => { + const terminalFromDefinitions = ONBOARD_MACHINE_STATE_DEFINITIONS.filter( + (definition) => definition.terminal, + ).map((definition) => definition.state); + const nonTerminalFromDefinitions = ONBOARD_MACHINE_STATE_DEFINITIONS.filter( + (definition) => !definition.terminal, + ).map((definition) => definition.state); + + expect(ONBOARD_MACHINE_TERMINAL_STATE_IDS).toEqual(terminalFromDefinitions); + expect(ONBOARD_MACHINE_NON_TERMINAL_STATE_IDS).toEqual(nonTerminalFromDefinitions); + }); + + it("keeps resumable step names unique", () => { + const stepNames = ONBOARD_MACHINE_STATE_DEFINITIONS.flatMap((definition) => + "stepName" in definition ? [definition.stepName] : [], + ); + + expect(new Set(stepNames).size).toBe(stepNames.length); + expect(stepNames).toEqual([ + "preflight", + "gateway", + "provider_selection", + "inference", + "sandbox", + "agent_setup", + "openclaw", + "policies", + ]); + }); + + it("keeps progress metadata attached only to state-backed steps", () => { + for (const definition of ONBOARD_MACHINE_STATE_DEFINITIONS) { + if (!("progress" in definition)) continue; + expect("stepName" in definition).toBe(true); + expect(definition.progress.total).toBe(8); + expect(definition.progress.number).toBeGreaterThanOrEqual(1); + expect(definition.progress.number).toBeLessThanOrEqual(definition.progress.total); + expect(definition.progress.title).not.toHaveLength(0); + } + }); + + it("looks up definitions by state", () => { + expect(getOnboardMachineStateDefinition("gateway")).toMatchObject({ + state: "gateway", + stepName: "gateway", + }); + }); +}); diff --git a/src/lib/onboard/machine/definition.ts b/src/lib/onboard/machine/definition.ts new file mode 100644 index 0000000000..0f873edf0b --- /dev/null +++ b/src/lib/onboard/machine/definition.ts @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * Canonical metadata for the coarse onboard finite-state machine. + * + * Keep this file free of imports from the rest of the machine package so the + * core state vocabulary can be reused by type, transition, event, session, and + * progress helpers without introducing circular dependencies. + */ + +export const ONBOARD_MACHINE_STATE_DEFINITIONS = [ + { state: "init", terminal: false }, + { + state: "preflight", + terminal: false, + stepName: "preflight", + progress: { number: 1, total: 8, title: "Preflight checks" }, + }, + { + state: "gateway", + terminal: false, + stepName: "gateway", + progress: { number: 2, total: 8, title: "Starting OpenShell gateway" }, + }, + { + state: "provider_selection", + terminal: false, + stepName: "provider_selection", + progress: { number: 3, total: 8, title: "Configuring inference (NIM)" }, + }, + { + state: "inference", + terminal: false, + stepName: "inference", + progress: { number: 4, total: 8, title: "Setting up inference provider" }, + }, + { + state: "sandbox", + terminal: false, + stepName: "sandbox", + progress: { number: 6, total: 8, title: "Creating sandbox" }, + }, + { + state: "agent_setup", + terminal: false, + stepName: "agent_setup", + progress: { number: 7, total: 8, title: "Setting up agent inside sandbox" }, + }, + { + state: "openclaw", + terminal: false, + stepName: "openclaw", + progress: { number: 7, total: 8, title: "Setting up agent inside sandbox" }, + }, + { + state: "policies", + terminal: false, + stepName: "policies", + progress: { number: 8, total: 8, title: "Policy presets" }, + }, + { state: "finalizing", terminal: false }, + { state: "post_verify", terminal: false }, + { state: "complete", terminal: true }, + { state: "failed", terminal: true }, +] as const; + +export const ONBOARD_MACHINE_STATE_IDS = ONBOARD_MACHINE_STATE_DEFINITIONS.map( + (definition) => definition.state, +) as readonly OnboardMachineStateId[]; + +export const ONBOARD_MACHINE_TERMINAL_STATE_IDS = ["complete", "failed"] as const; + +export type OnboardTerminalMachineStateId = (typeof ONBOARD_MACHINE_TERMINAL_STATE_IDS)[number]; + +export type OnboardMachineStateId = (typeof ONBOARD_MACHINE_STATE_DEFINITIONS)[number]["state"]; + +export type OnboardNonTerminalMachineStateId = Exclude< + OnboardMachineStateId, + OnboardTerminalMachineStateId +>; + +export const ONBOARD_MACHINE_NON_TERMINAL_STATE_IDS = ONBOARD_MACHINE_STATE_DEFINITIONS.filter( + (definition): definition is Extract< + (typeof ONBOARD_MACHINE_STATE_DEFINITIONS)[number], + { terminal: false } + > => definition.terminal === false, +).map((definition) => definition.state) as readonly OnboardNonTerminalMachineStateId[]; + +export type OnboardMachineStateDefinition = (typeof ONBOARD_MACHINE_STATE_DEFINITIONS)[number]; + +export type OnboardMachineStateWithStepDefinition = Extract< + OnboardMachineStateDefinition, + { stepName: string } +>; + +export type OnboardMachineStateWithProgressDefinition = Extract< + OnboardMachineStateDefinition, + { progress: { number: number; total: number; title: string } } +>; + +export function getOnboardMachineStateDefinition( + state: OnboardMachineStateId, +): OnboardMachineStateDefinition { + const definition = ONBOARD_MACHINE_STATE_DEFINITIONS.find((entry) => entry.state === state); + if (!definition) throw new Error(`Unknown onboarding machine state: ${state}`); + return definition; +} diff --git a/src/lib/onboard/machine/types.ts b/src/lib/onboard/machine/types.ts index e1dca21e72..d5f00477ee 100644 --- a/src/lib/onboard/machine/types.ts +++ b/src/lib/onboard/machine/types.ts @@ -9,39 +9,26 @@ * probes, or policy application is out of scope for the initial FSM shell. */ -export const ONBOARD_MACHINE_STATES = [ - "init", - "preflight", - "gateway", - "provider_selection", - "inference", - "sandbox", - "agent_setup", - "openclaw", - "policies", - "finalizing", - "post_verify", - "complete", - "failed", -] as const; +import { + ONBOARD_MACHINE_NON_TERMINAL_STATE_IDS, + ONBOARD_MACHINE_STATE_IDS, + ONBOARD_MACHINE_TERMINAL_STATE_IDS, + type OnboardMachineStateId, + type OnboardNonTerminalMachineStateId, + type OnboardTerminalMachineStateId, +} from "./definition"; + +export const ONBOARD_MACHINE_STATES = ONBOARD_MACHINE_STATE_IDS; -export type OnboardMachineState = (typeof ONBOARD_MACHINE_STATES)[number]; +export type OnboardMachineState = OnboardMachineStateId; -export const ONBOARD_TERMINAL_MACHINE_STATES = ["complete", "failed"] as const; +export const ONBOARD_TERMINAL_MACHINE_STATES = ONBOARD_MACHINE_TERMINAL_STATE_IDS; -export type OnboardTerminalMachineState = - (typeof ONBOARD_TERMINAL_MACHINE_STATES)[number]; +export type OnboardTerminalMachineState = OnboardTerminalMachineStateId; -export type OnboardNonTerminalMachineState = Exclude< - OnboardMachineState, - OnboardTerminalMachineState ->; +export type OnboardNonTerminalMachineState = OnboardNonTerminalMachineStateId; -export const ONBOARD_NON_TERMINAL_MACHINE_STATES: readonly OnboardNonTerminalMachineState[] = - ONBOARD_MACHINE_STATES.filter( - (state): state is OnboardNonTerminalMachineState => - !ONBOARD_TERMINAL_MACHINE_STATES.includes(state as OnboardTerminalMachineState), - ); +export const ONBOARD_NON_TERMINAL_MACHINE_STATES = ONBOARD_MACHINE_NON_TERMINAL_STATE_IDS; export const ONBOARD_MACHINE_EVENT_TYPES = [ "onboard.started", From c3e4ad63b31738ffad7f7c7bb306dfa8d6eca39a Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 27 May 2026 15:21:58 -0700 Subject: [PATCH 3/4] refactor(onboard): derive session step mapping from FSM metadata Signed-off-by: Carlos Villela --- src/lib/onboard/machine/definition.test.ts | 11 ++++++++ src/lib/onboard/machine/events.ts | 32 ++++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/lib/onboard/machine/definition.test.ts b/src/lib/onboard/machine/definition.test.ts index 7fffa49ec5..6c5d87c6f7 100644 --- a/src/lib/onboard/machine/definition.test.ts +++ b/src/lib/onboard/machine/definition.test.ts @@ -10,6 +10,7 @@ import { ONBOARD_MACHINE_STATE_IDS, ONBOARD_MACHINE_TERMINAL_STATE_IDS, } from "./definition"; +import { ONBOARD_SESSION_STEP_TO_MACHINE_STATE } from "./events"; const expectedStateOrder = [ "init", @@ -65,6 +66,16 @@ describe("onboard machine definition", () => { ]); }); + it("derives the session step mapping from state definitions", () => { + const mappingFromDefinitions = Object.fromEntries( + ONBOARD_MACHINE_STATE_DEFINITIONS.flatMap((definition) => + "stepName" in definition ? [[definition.stepName, definition.state]] : [], + ), + ); + + expect(ONBOARD_SESSION_STEP_TO_MACHINE_STATE).toEqual(mappingFromDefinitions); + }); + it("keeps progress metadata attached only to state-backed steps", () => { for (const definition of ONBOARD_MACHINE_STATE_DEFINITIONS) { if (!("progress" in definition)) continue; diff --git a/src/lib/onboard/machine/events.ts b/src/lib/onboard/machine/events.ts index f6b7dca47c..2ce746167a 100644 --- a/src/lib/onboard/machine/events.ts +++ b/src/lib/onboard/machine/events.ts @@ -4,24 +4,32 @@ import type { JsonObject, JsonValue } from "../../core/json-types"; import { redactSensitiveText, redactUrl } from "../../security/redact"; import type { HermesAuthMethod, Session } from "../../state/onboard-session"; +import { + ONBOARD_MACHINE_STATE_DEFINITIONS, + type OnboardMachineStateWithStepDefinition, +} from "./definition"; import type { OnboardMachineContext, OnboardMachineEventType, OnboardMachineState, } from "./types"; -export const ONBOARD_SESSION_STEP_TO_MACHINE_STATE = { - preflight: "preflight", - gateway: "gateway", - provider_selection: "provider_selection", - inference: "inference", - sandbox: "sandbox", - agent_setup: "agent_setup", - openclaw: "openclaw", - policies: "policies", -} as const satisfies Readonly>; - -export type OnboardSessionStepName = keyof typeof ONBOARD_SESSION_STEP_TO_MACHINE_STATE; +type OnboardSessionStepDefinition = OnboardMachineStateWithStepDefinition; + +export type OnboardSessionStepName = OnboardSessionStepDefinition["stepName"]; + +type OnboardSessionStepToMachineState = { + readonly [StepName in OnboardSessionStepName]: Extract< + OnboardSessionStepDefinition, + { stepName: StepName } + >["state"]; +}; + +export const ONBOARD_SESSION_STEP_TO_MACHINE_STATE = Object.fromEntries( + ONBOARD_MACHINE_STATE_DEFINITIONS.flatMap((definition) => + "stepName" in definition ? [[definition.stepName, definition.state]] : [], + ), +) as OnboardSessionStepToMachineState; export interface OnboardMachineEvent { version: 1; From 603832c0c5d9e2ea2e9a8b27158ee00b8fd9bc93 Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Wed, 27 May 2026 15:24:39 -0700 Subject: [PATCH 4/4] refactor(onboard): derive progress labels from FSM metadata Signed-off-by: Carlos Villela --- src/lib/onboard.ts | 23 +++++--------- src/lib/onboard/machine/definition.ts | 1 - src/lib/onboard/machine/progress.test.ts | 38 ++++++++++++++++++++++++ src/lib/onboard/machine/progress.ts | 38 ++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 src/lib/onboard/machine/progress.test.ts create mode 100644 src/lib/onboard/machine/progress.ts diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 761c3c2454..4fef39b2aa 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -419,6 +419,7 @@ const { handlePoliciesState }: typeof import("./onboard/machine/handlers/policie const { handlePreflightState }: typeof import("./onboard/machine/handlers/preflight") = require("./onboard/machine/handlers/preflight"); const { handleProviderInferenceState }: typeof import("./onboard/machine/handlers/provider-inference") = require("./onboard/machine/handlers/provider-inference"); const { handleSandboxState }: typeof import("./onboard/machine/handlers/sandbox") = require("./onboard/machine/handlers/sandbox"); +const { getOnboardProgressStep }: typeof import("./onboard/machine/progress") = require("./onboard/machine/progress"); const policies: typeof import("./policy") = require("./policy"); const tiers: typeof import("./policy/tiers") = require("./policy/tiers"); const { ensureUsageNoticeConsent } = require("./onboard/usage-notice"); @@ -6390,28 +6391,18 @@ const recordRepairEvent = onboardRuntimeBoundary.recordRepairEvent.bind(onboardR const recordPostVerifyStarted = onboardRuntimeBoundary.recordPostVerifyStarted.bind(onboardRuntimeBoundary); const recordSessionComplete = onboardRuntimeBoundary.recordSessionComplete.bind(onboardRuntimeBoundary); -const ONBOARD_STEP_INDEX: Record = { - preflight: { number: 1, title: "Preflight checks" }, - gateway: { number: 2, title: "Starting OpenShell gateway" }, - provider_selection: { number: 3, title: "Configuring inference (NIM)" }, - inference: { number: 4, title: "Setting up inference provider" }, - messaging: { number: 5, title: "Messaging channels" }, - sandbox: { number: 6, title: "Creating sandbox" }, - openclaw: { number: 7, title: "Setting up agent inside sandbox" }, - policies: { number: 8, title: "Policy presets" }, -}; - function skippedStepMessage( stepName: string, detail?: string | null, reason: "resume" | "reuse" = "resume", ): void { - let stepInfo = ONBOARD_STEP_INDEX[stepName]; - if (stepInfo && stepName === "openclaw") { - stepInfo = { ...stepInfo, title: `Setting up ${agentProductName()} inside sandbox` }; - } + const progressStep = getOnboardProgressStep(stepName); + const stepInfo = + progressStep && stepName === "openclaw" + ? { ...progressStep, title: `Setting up ${agentProductName()} inside sandbox` } + : progressStep; if (stepInfo) { - step(stepInfo.number, 8, stepInfo.title); + step(stepInfo.number, stepInfo.total, stepInfo.title); } const prefix = reason === "reuse" ? "[reuse]" : "[resume]"; console.log(` ${prefix} Skipping ${stepName}${detail ? ` (${detail})` : ""}`); diff --git a/src/lib/onboard/machine/definition.ts b/src/lib/onboard/machine/definition.ts index 0f873edf0b..03903bfb34 100644 --- a/src/lib/onboard/machine/definition.ts +++ b/src/lib/onboard/machine/definition.ts @@ -45,7 +45,6 @@ export const ONBOARD_MACHINE_STATE_DEFINITIONS = [ state: "agent_setup", terminal: false, stepName: "agent_setup", - progress: { number: 7, total: 8, title: "Setting up agent inside sandbox" }, }, { state: "openclaw", diff --git a/src/lib/onboard/machine/progress.test.ts b/src/lib/onboard/machine/progress.test.ts new file mode 100644 index 0000000000..4c1ee2e99d --- /dev/null +++ b/src/lib/onboard/machine/progress.test.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { ONBOARD_MACHINE_STATE_DEFINITIONS } from "./definition"; +import { getOnboardProgressStep, ONBOARD_PROGRESS_STEPS } from "./progress"; + +describe("onboard progress metadata", () => { + it("derives state-backed progress labels from machine definitions", () => { + for (const definition of ONBOARD_MACHINE_STATE_DEFINITIONS) { + if (!("progress" in definition)) continue; + expect(ONBOARD_PROGRESS_STEPS[definition.stepName]).toEqual(definition.progress); + } + }); + + it("preserves the existing eight-step onboarding labels", () => { + expect(ONBOARD_PROGRESS_STEPS).toEqual({ + preflight: { number: 1, total: 8, title: "Preflight checks" }, + gateway: { number: 2, total: 8, title: "Starting OpenShell gateway" }, + provider_selection: { number: 3, total: 8, title: "Configuring inference (NIM)" }, + inference: { number: 4, total: 8, title: "Setting up inference provider" }, + messaging: { number: 5, total: 8, title: "Messaging channels" }, + sandbox: { number: 6, total: 8, title: "Creating sandbox" }, + openclaw: { number: 7, total: 8, title: "Setting up agent inside sandbox" }, + policies: { number: 8, total: 8, title: "Policy presets" }, + }); + }); + + it("looks up known labels and ignores unknown steps", () => { + expect(getOnboardProgressStep("gateway")).toEqual({ + number: 2, + total: 8, + title: "Starting OpenShell gateway", + }); + expect(getOnboardProgressStep("not-a-step")).toBeNull(); + }); +}); diff --git a/src/lib/onboard/machine/progress.ts b/src/lib/onboard/machine/progress.ts new file mode 100644 index 0000000000..2cf485655e --- /dev/null +++ b/src/lib/onboard/machine/progress.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + ONBOARD_MACHINE_STATE_DEFINITIONS, + type OnboardMachineStateWithProgressDefinition, +} from "./definition"; + +export interface OnboardProgressStep { + number: number; + total: number; + title: string; +} + +export type OnboardMachineProgressStepName = + OnboardMachineStateWithProgressDefinition["stepName"]; + +export type OnboardProgressStepName = OnboardMachineProgressStepName | "messaging"; + +const EXTRA_PROGRESS_STEPS = [ + { + stepName: "messaging", + progress: { number: 5, total: 8, title: "Messaging channels" }, + }, +] as const; + +export const ONBOARD_PROGRESS_STEPS = Object.fromEntries([ + ...ONBOARD_MACHINE_STATE_DEFINITIONS.flatMap((definition) => + "progress" in definition ? [[definition.stepName, definition.progress]] : [], + ), + ...EXTRA_PROGRESS_STEPS.map((definition) => [definition.stepName, definition.progress]), +]) as Readonly>; + +export function getOnboardProgressStep(stepName: string): OnboardProgressStep | null { + return Object.prototype.hasOwnProperty.call(ONBOARD_PROGRESS_STEPS, stepName) + ? ONBOARD_PROGRESS_STEPS[stepName as OnboardProgressStepName] + : null; +}