1- import * as Console from "effect/Console"
21import * as Effect from "effect/Effect"
3- import * as FileSystem from "effect/FileSystem"
4- import * as Option from "effect/Option"
52import * as Path from "effect/Path"
6- import type * as PlatformError from "effect/PlatformError"
73import { Command } from "effect/unstable/cli"
8- import * as Prompt from "effect/unstable/cli/Prompt"
94import packageJson from "../../package.json"
10- import { assess , type Assessment } from "./setup/assessment"
11- import { computeChanges } from "./setup/changes"
12- import { renderCodeActions } from "./setup/diff-renderer"
13- import { FileReadError , PackageJsonNotFoundError } from "./setup/errors"
5+ import * as Assessment from "./setup/assessment"
6+ import * as Changes from "./setup/changes"
147import { gatherTargetState } from "./setup/target-prompt"
158import { selectTsConfigFile } from "./setup/tsconfig-prompt"
16- import { type FileInput } from "./utils"
17-
18- /**
19- * Read files from file system and create assessment input
20- */
21- const createAssessmentInput = (
22- currentDir : string ,
23- tsconfigInput : FileInput
24- ) : Effect . Effect <
25- Assessment . Input ,
26- PackageJsonNotFoundError | FileReadError | PlatformError . PlatformError ,
27- FileSystem . FileSystem | Path . Path
28- > =>
29- Effect . gen ( function * ( ) {
30- const fs = yield * FileSystem . FileSystem
31- const path = yield * Path . Path
32-
33- // Check package.json
34- const packageJsonPath = path . join ( currentDir , "package.json" )
35- const packageJsonExists = yield * fs . exists ( packageJsonPath )
36-
37- if ( ! packageJsonExists ) {
38- return yield * new PackageJsonNotFoundError ( { path : packageJsonPath } )
39- }
40-
41- const packageJsonText = yield * fs . readFileString ( packageJsonPath ) . pipe (
42- Effect . mapError ( ( cause ) => new FileReadError ( { path : packageJsonPath , cause } ) )
43- )
44- const packageJsonInput : FileInput = {
45- fileName : packageJsonPath ,
46- text : packageJsonText
47- }
48-
49- // Check .vscode/settings.json (optional)
50- const vscodeSettingsPath = path . join ( currentDir , ".vscode" , "settings.json" )
51- const vscodeSettingsExists = yield * fs . exists ( vscodeSettingsPath )
52-
53- let vscodeSettingsInput = Option . none < FileInput > ( )
54- if ( vscodeSettingsExists ) {
55- const vscodeSettingsText = yield * fs . readFileString ( vscodeSettingsPath ) . pipe (
56- Effect . mapError ( ( cause ) => new FileReadError ( { path : vscodeSettingsPath , cause } ) )
57- )
58- vscodeSettingsInput = Option . some ( {
59- fileName : vscodeSettingsPath ,
60- text : vscodeSettingsText
61- } )
62- }
63-
64- // Check agents.md / AGENTS.md (optional, case-insensitive, skip symlinks)
65- const agentsMdLowerPath = path . join ( currentDir , "agents.md" )
66- const agentsMdUpperPath = path . join ( currentDir , "AGENTS.md" )
67- const agentsMdLowerExists = yield * fs . exists ( agentsMdLowerPath )
68- const agentsMdUpperExists = yield * fs . exists ( agentsMdUpperPath )
69- const agentsMdPath = agentsMdUpperExists ? agentsMdUpperPath : agentsMdLowerPath
70- const agentsMdExists = agentsMdUpperExists || agentsMdLowerExists
71-
72- let agentsMdInput = Option . none < FileInput > ( )
73- if ( agentsMdExists ) {
74- // Check if it's a symlink - skip if it is
75- const agentsMdStat = yield * fs . stat ( agentsMdPath ) . pipe ( Effect . option )
76- const isAgentsMdSymlink = Option . isSome ( agentsMdStat ) &&
77- agentsMdStat . value . type === "SymbolicLink"
78-
79- if ( ! isAgentsMdSymlink ) {
80- const agentsMdText = yield * fs . readFileString ( agentsMdPath ) . pipe (
81- Effect . mapError ( ( cause ) => new FileReadError ( { path : agentsMdPath , cause } ) )
82- )
83- agentsMdInput = Option . some ( {
84- fileName : agentsMdPath ,
85- text : agentsMdText
86- } )
87- }
88- }
89-
90- // Check claude.md / CLAUDE.md (optional, case-insensitive, skip symlinks)
91- const claudeMdLowerPath = path . join ( currentDir , "claude.md" )
92- const claudeMdUpperPath = path . join ( currentDir , "CLAUDE.md" )
93- const claudeMdLowerExists = yield * fs . exists ( claudeMdLowerPath )
94- const claudeMdUpperExists = yield * fs . exists ( claudeMdUpperPath )
95- const claudeMdPath = claudeMdUpperExists ? claudeMdUpperPath : claudeMdLowerPath
96- const claudeMdExists = claudeMdUpperExists || claudeMdLowerExists
97-
98- let claudeMdInput = Option . none < FileInput > ( )
99- if ( claudeMdExists ) {
100- // Check if it's a symlink - skip if it is
101- const claudeMdStat = yield * fs . stat ( claudeMdPath ) . pipe ( Effect . option )
102- const isClaudeMdSymlink = Option . isSome ( claudeMdStat ) &&
103- claudeMdStat . value . type === "SymbolicLink"
104-
105- if ( ! isClaudeMdSymlink ) {
106- const claudeMdText = yield * fs . readFileString ( claudeMdPath ) . pipe (
107- Effect . mapError ( ( cause ) => new FileReadError ( { path : claudeMdPath , cause } ) )
108- )
109- claudeMdInput = Option . some ( {
110- fileName : claudeMdPath ,
111- text : claudeMdText
112- } )
113- }
114- }
115-
116- return {
117- packageJson : packageJsonInput ,
118- tsconfig : tsconfigInput ,
119- vscodeSettings : vscodeSettingsInput ,
120- agentsMd : agentsMdInput ,
121- claudeMd : claudeMdInput
122- }
123- } )
1249
12510/**
12611 * Main setup command
@@ -142,13 +27,13 @@ export const setup = Command.make(
14227 // Phase 2: Read files and create assessment input
14328 // ========================================================================
14429
145- const assessmentInput = yield * createAssessmentInput ( currentDir , tsconfigInput )
30+ const assessmentInput = yield * Assessment . createAssessmentInput ( currentDir , tsconfigInput )
14631
14732 // ========================================================================
14833 // Phase 3: Perform assessment
14934 // ========================================================================
15035
151- const assessmentState = yield * assess ( assessmentInput )
36+ const assessmentState = yield * Assessment . assess ( assessmentInput )
15237
15338 // ========================================================================
15439 // Phase 4: Gather target state from user
@@ -160,86 +45,15 @@ export const setup = Command.make(
16045 // ========================================================================
16146 // Phase 5: Compute changes
16247 // ========================================================================
163- const result = yield * computeChanges ( assessmentState , targetState )
48+ const result = yield * Changes . computeChanges ( assessmentState , targetState )
16449
16550 // ========================================================================
16651 // Phase 6: Review changes
16752 // ========================================================================
168- yield * renderCodeActions ( result , assessmentState )
169-
170- if ( result . codeActions . length === 0 ) {
171- return
172- }
17353
174- const shouldProceed = yield * Prompt . confirm ( {
175- message : "Apply all changes?" ,
176- initial : true
54+ yield * Changes . reviewAndApplyChanges ( result , assessmentState , {
55+ cancelMessage : "Setup cancelled. No changes were made."
17756 } )
178-
179- if ( ! shouldProceed ) {
180- yield * Console . log ( "Setup cancelled. No changes were made." )
181- return
182- }
183-
184- // ========================================================================
185- // Phase 7: Apply changes
186- // ========================================================================
187- yield * Console . log ( "" )
188- yield * Console . log ( "Applying changes..." )
189-
190- const fs = yield * FileSystem . FileSystem
191-
192- // Apply each code action
193- for ( const codeAction of result . codeActions ) {
194- for ( const fileChange of codeAction . changes ) {
195- const fileName = fileChange . fileName
196-
197- // Check if file exists or if this is a new file
198- const fileExists = yield * fs . exists ( fileName )
199-
200- if ( ! fileExists && fileChange . isNewFile ) {
201- // Create new file - ensure directory exists first
202- const dirName = path . dirname ( fileName )
203- yield * fs . makeDirectory ( dirName , { recursive : true } ) . pipe (
204- Effect . ignore // Ignore error if directory already exists
205- )
206-
207- // For new files, just write the newText from the first change
208- // (assumption: new files have a single TextChange spanning the entire file)
209- const newContent = fileChange . textChanges . length > 0
210- ? fileChange . textChanges [ 0 ] . newText
211- : ""
212-
213- yield * fs . writeFileString ( fileName , newContent )
214- } else if ( fileExists ) {
215- // Read existing file
216- const existingContent = yield * fs . readFileString ( fileName )
217-
218- // Apply all text changes to the file
219- // Sort changes in reverse order by position to avoid offset issues
220- const sortedChanges = [ ...fileChange . textChanges ] . sort ( ( a , b ) => b . span . start - a . span . start )
221-
222- let newContent = existingContent
223- for ( const textChange of sortedChanges ) {
224- const start = textChange . span . start
225- const end = start + textChange . span . length
226-
227- newContent = newContent . slice ( 0 , start ) + textChange . newText + newContent . slice ( end )
228- }
229-
230- // Write the modified content back
231- yield * fs . writeFileString ( fileName , newContent )
232- }
233- }
234- }
235-
236- yield * Console . log ( "Changes applied successfully!" )
237- yield * Console . log ( "" )
238-
239- // Display any additional messages (e.g., editor setup instructions)
240- for ( const message of result . messages ) {
241- yield * Console . log ( message )
242- }
24357 } )
24458) . pipe (
24559 Command . withDescription ( "Setup the effect-language-service for the given project using an interactive cli." )
0 commit comments