/ projectOnboardingState.ts
projectOnboardingState.ts
 1  import memoize from 'lodash-es/memoize.js'
 2  import { join } from 'path'
 3  import {
 4    getCurrentProjectConfig,
 5    saveCurrentProjectConfig,
 6  } from './utils/config.js'
 7  import { getCwd } from './utils/cwd.js'
 8  import { isDirEmpty } from './utils/file.js'
 9  import { getFsImplementation } from './utils/fsOperations.js'
10  
11  export type Step = {
12    key: string
13    text: string
14    isComplete: boolean
15    isCompletable: boolean
16    isEnabled: boolean
17  }
18  
19  export function getSteps(): Step[] {
20    const hasClaudeMd = getFsImplementation().existsSync(
21      join(getCwd(), 'CLAUDE.md'),
22    )
23    const isWorkspaceDirEmpty = isDirEmpty(getCwd())
24  
25    return [
26      {
27        key: 'workspace',
28        text: 'Ask Claude to create a new app or clone a repository',
29        isComplete: false,
30        isCompletable: true,
31        isEnabled: isWorkspaceDirEmpty,
32      },
33      {
34        key: 'claudemd',
35        text: 'Run /init to create a CLAUDE.md file with instructions for Claude',
36        isComplete: hasClaudeMd,
37        isCompletable: true,
38        isEnabled: !isWorkspaceDirEmpty,
39      },
40    ]
41  }
42  
43  export function isProjectOnboardingComplete(): boolean {
44    return getSteps()
45      .filter(({ isCompletable, isEnabled }) => isCompletable && isEnabled)
46      .every(({ isComplete }) => isComplete)
47  }
48  
49  export function maybeMarkProjectOnboardingComplete(): void {
50    // Short-circuit on cached config — isProjectOnboardingComplete() hits
51    // the filesystem, and REPL.tsx calls this on every prompt submit.
52    if (getCurrentProjectConfig().hasCompletedProjectOnboarding) {
53      return
54    }
55    if (isProjectOnboardingComplete()) {
56      saveCurrentProjectConfig(current => ({
57        ...current,
58        hasCompletedProjectOnboarding: true,
59      }))
60    }
61  }
62  
63  export const shouldShowProjectOnboarding = memoize((): boolean => {
64    const projectConfig = getCurrentProjectConfig()
65    // Short-circuit on cached config before isProjectOnboardingComplete()
66    // hits the filesystem — this runs during first render.
67    if (
68      projectConfig.hasCompletedProjectOnboarding ||
69      projectConfig.projectOnboardingSeenCount >= 4 ||
70      process.env.IS_DEMO
71    ) {
72      return false
73    }
74  
75    return !isProjectOnboardingComplete()
76  })
77  
78  export function incrementProjectOnboardingSeenCount(): void {
79    saveCurrentProjectConfig(current => ({
80      ...current,
81      projectOnboardingSeenCount: current.projectOnboardingSeenCount + 1,
82    }))
83  }