Skip to content

Commit f057090

Browse files
Add config CLI command (#695)
1 parent b5054e3 commit f057090

File tree

11 files changed

+374
-199
lines changed

11 files changed

+374
-199
lines changed

.changeset/early-bottles-flash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@effect/language-service": minor
3+
---
4+
5+
Add a `config` CLI command for updating diagnostic rule severities without rerunning the full setup flow.

.changeset/tiny-rats-rule.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@effect/language-service": minor
33
---
44

5-
Add setup CLI preset management for diagnostic severities, including preset metadata and preset-aware customization.
5+
Add setup CLI preset management for diagnostic severities, including preset metadata, preset-aware customization, and a dedicated `config` command for adjusting rule severities without rerunning full setup.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ The effect language service plugin comes with a builtin CLI tool that can be use
249249
### `effect-language-service setup`
250250
Runs through a wizard to setup/update some basic functionalities of the LSP in an interactive way. This also keeps the `tsconfig.json` `$schema` aligned with the published Effect Language Service schema.
251251
252+
### `effect-language-service config`
253+
After selecting a tsconfig.json file, jumps to the interactive configuration of rules severities.
254+
252255
### `effect-language-service codegen`
253256
Automatically updates Effect codegens in your TypeScript files. This command scans files for `@effect-codegens` directives and applies the necessary code transformations. Use `--file` to update a specific file, or `--project` with a tsconfig file to update an entire project. The `--verbose` flag provides detailed output about which files are being processed and updated.
254257

packages/language-service/src/cli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Command } from "effect/unstable/cli"
99
import packageJson from "../package.json"
1010
import { check } from "./cli/check"
1111
import { codegen } from "./cli/codegen"
12+
import { config } from "./cli/config"
1213
import { diagnostics } from "./cli/diagnostics"
1314
import { layerInfo } from "./cli/layerinfo"
1415
import { overview } from "./cli/overview"
@@ -25,7 +26,7 @@ const cliCommand = Command.make(
2526
).pipe(Command.withSubcommands([
2627
{
2728
group: "Getting started",
28-
commands: [setup]
29+
commands: [setup, config]
2930
},
3031
{
3132
group: "Diagnostics at compile-time",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as Effect from "effect/Effect"
2+
import * as Option from "effect/Option"
3+
import * as Path from "effect/Path"
4+
import { Command } from "effect/unstable/cli"
5+
import * as Assessment from "./setup/assessment"
6+
import * as Changes from "./setup/changes"
7+
import { getAllDiagnostics } from "./setup/diagnostic-info"
8+
import { createDiagnosticPrompt } from "./setup/diagnostic-prompt"
9+
import * as Target from "./setup/target"
10+
import { selectTsConfigFile } from "./setup/tsconfig-prompt"
11+
12+
export const config = Command.make(
13+
"config",
14+
{},
15+
() =>
16+
Effect.gen(function*() {
17+
const path = yield* Path.Path
18+
const currentDir = path.resolve(process.cwd())
19+
const tsconfigInput = yield* selectTsConfigFile(currentDir)
20+
const assessmentInput = yield* Assessment.createAssessmentInput(currentDir, tsconfigInput)
21+
const assessmentState = yield* Assessment.assess(assessmentInput)
22+
23+
const allDiagnostics = getAllDiagnostics()
24+
const currentDiagnosticSeverities = Option.match(assessmentState.tsconfig.currentOptions, {
25+
onNone: () => ({}),
26+
onSome: (options) => options.diagnosticSeverity
27+
})
28+
29+
const diagnosticSeverities = yield* createDiagnosticPrompt(allDiagnostics, currentDiagnosticSeverities)
30+
const targetState = Target.withDiagnosticSeverities(Target.fromAssessment(assessmentState), diagnosticSeverities)
31+
const result = yield* Changes.computeChanges(assessmentState, targetState)
32+
33+
yield* Changes.reviewAndApplyChanges(result, assessmentState, {
34+
confirmMessage: "Apply diagnostic configuration changes?",
35+
cancelMessage: "Configuration cancelled. No changes were made."
36+
})
37+
})
38+
).pipe(
39+
Command.withDescription("Configure diagnostic severities for an existing tsconfig using the interactive rule picker.")
40+
)

packages/language-service/src/cli/setup.ts

Lines changed: 7 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,11 @@
1-
import * as Console from "effect/Console"
21
import * as Effect from "effect/Effect"
3-
import * as FileSystem from "effect/FileSystem"
4-
import * as Option from "effect/Option"
52
import * as Path from "effect/Path"
6-
import type * as PlatformError from "effect/PlatformError"
73
import { Command } from "effect/unstable/cli"
8-
import * as Prompt from "effect/unstable/cli/Prompt"
94
import 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"
147
import { gatherTargetState } from "./setup/target-prompt"
158
import { 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

Comments
 (0)