@remix-run/cli 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -3
- package/bootstrap/AGENTS.md +35 -0
- package/bootstrap/README.md +27 -0
- package/bootstrap/app/controllers/auth.tsx +21 -0
- package/bootstrap/app/controllers/home.tsx +26 -0
- package/bootstrap/app/router.ts +10 -0
- package/bootstrap/app/routes.ts +6 -0
- package/bootstrap/app/ui/document.tsx +21 -0
- package/bootstrap/app/ui/layout.tsx +22 -0
- package/bootstrap/app/utils/render.tsx +7 -0
- package/bootstrap/package.json +22 -0
- package/bootstrap/server.ts +37 -0
- package/bootstrap/tsconfig.json +19 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/lib/bootstrap-project.d.ts +17 -0
- package/dist/lib/bootstrap-project.d.ts.map +1 -0
- package/dist/lib/bootstrap-project.js +137 -0
- package/dist/lib/cli-context.d.ts +11 -0
- package/dist/lib/cli-context.d.ts.map +1 -0
- package/dist/lib/cli-context.js +19 -0
- package/dist/lib/cli.d.ts +6 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +87 -0
- package/dist/lib/commands/completion.d.ts +3 -0
- package/dist/lib/commands/completion.d.ts.map +1 -0
- package/dist/lib/commands/completion.js +51 -0
- package/dist/lib/commands/doctor.d.ts +4 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -0
- package/dist/lib/commands/doctor.js +293 -0
- package/dist/lib/commands/help.d.ts +4 -0
- package/dist/lib/commands/help.d.ts.map +1 -0
- package/dist/lib/commands/help.js +124 -0
- package/dist/lib/commands/new.d.ts +4 -0
- package/dist/lib/commands/new.d.ts.map +1 -0
- package/dist/lib/commands/new.js +70 -0
- package/dist/lib/commands/routes.d.ts +4 -0
- package/dist/lib/commands/routes.d.ts.map +1 -0
- package/dist/lib/commands/routes.js +161 -0
- package/dist/lib/commands/skills.d.ts +6 -0
- package/dist/lib/commands/skills.d.ts.map +1 -0
- package/dist/lib/commands/skills.js +222 -0
- package/dist/lib/commands/test.d.ts +4 -0
- package/dist/lib/commands/test.d.ts.map +1 -0
- package/dist/lib/commands/test.js +19 -0
- package/dist/lib/commands/version.d.ts +4 -0
- package/dist/lib/commands/version.d.ts.map +1 -0
- package/dist/lib/commands/version.js +29 -0
- package/dist/lib/completion.d.ts +12 -0
- package/dist/lib/completion.d.ts.map +1 -0
- package/dist/lib/completion.js +408 -0
- package/dist/lib/contained-path.d.ts +2 -0
- package/dist/lib/contained-path.d.ts.map +1 -0
- package/dist/lib/contained-path.js +10 -0
- package/dist/lib/controller-files.d.ts +14 -0
- package/dist/lib/controller-files.d.ts.map +1 -0
- package/dist/lib/controller-files.js +53 -0
- package/dist/lib/controller-ownership.d.ts +41 -0
- package/dist/lib/controller-ownership.d.ts.map +1 -0
- package/dist/lib/controller-ownership.js +224 -0
- package/dist/lib/display-path.d.ts +2 -0
- package/dist/lib/display-path.d.ts.map +1 -0
- package/dist/lib/display-path.js +12 -0
- package/dist/lib/doctor/controller-findings.d.ts +4 -0
- package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
- package/dist/lib/doctor/controller-findings.js +132 -0
- package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
- package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
- package/dist/lib/doctor/controller-fix-plans.js +130 -0
- package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
- package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
- package/dist/lib/doctor/controller-placeholders.js +183 -0
- package/dist/lib/doctor/controllers.d.ts +8 -0
- package/dist/lib/doctor/controllers.d.ts.map +1 -0
- package/dist/lib/doctor/controllers.js +13 -0
- package/dist/lib/doctor/environment.d.ts +17 -0
- package/dist/lib/doctor/environment.d.ts.map +1 -0
- package/dist/lib/doctor/environment.js +226 -0
- package/dist/lib/doctor/fixes.d.ts +3 -0
- package/dist/lib/doctor/fixes.d.ts.map +1 -0
- package/dist/lib/doctor/fixes.js +65 -0
- package/dist/lib/doctor/project.d.ts +10 -0
- package/dist/lib/doctor/project.d.ts.map +1 -0
- package/dist/lib/doctor/project.js +371 -0
- package/dist/lib/doctor/types.d.ts +48 -0
- package/dist/lib/doctor/types.d.ts.map +1 -0
- package/dist/lib/doctor/types.js +15 -0
- package/dist/lib/errors.d.ts +173 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +318 -0
- package/dist/lib/help-text.d.ts +13 -0
- package/dist/lib/help-text.d.ts.map +1 -0
- package/dist/lib/help-text.js +43 -0
- package/dist/lib/load-route-map-worker.d.ts +2 -0
- package/dist/lib/load-route-map-worker.d.ts.map +1 -0
- package/dist/lib/load-route-map-worker.js +92 -0
- package/dist/lib/parse-args.d.ts +22 -0
- package/dist/lib/parse-args.d.ts.map +1 -0
- package/dist/lib/parse-args.js +35 -0
- package/dist/lib/remix-version.d.ts +2 -0
- package/dist/lib/remix-version.d.ts.map +1 -0
- package/dist/lib/remix-version.js +58 -0
- package/dist/lib/reporter.d.ts +56 -0
- package/dist/lib/reporter.d.ts.map +1 -0
- package/dist/lib/reporter.js +357 -0
- package/dist/lib/route-map.d.ts +38 -0
- package/dist/lib/route-map.d.ts.map +1 -0
- package/dist/lib/route-map.js +195 -0
- package/dist/lib/skills-cache.d.ts +19 -0
- package/dist/lib/skills-cache.d.ts.map +1 -0
- package/dist/lib/skills-cache.js +89 -0
- package/dist/lib/skills.d.ts +30 -0
- package/dist/lib/skills.d.ts.map +1 -0
- package/dist/lib/skills.js +441 -0
- package/dist/lib/terminal.d.ts +17 -0
- package/dist/lib/terminal.d.ts.map +1 -0
- package/dist/lib/terminal.js +66 -0
- package/package.json +48 -5
- package/src/index.ts +1 -0
- package/src/lib/bootstrap-project.ts +222 -0
- package/src/lib/cli-context.ts +38 -0
- package/src/lib/cli.ts +120 -0
- package/src/lib/commands/completion.ts +74 -0
- package/src/lib/commands/doctor.ts +393 -0
- package/src/lib/commands/help.ts +157 -0
- package/src/lib/commands/new.ts +104 -0
- package/src/lib/commands/routes.ts +251 -0
- package/src/lib/commands/skills.ts +306 -0
- package/src/lib/commands/test.ts +25 -0
- package/src/lib/commands/version.ts +39 -0
- package/src/lib/completion.ts +561 -0
- package/src/lib/contained-path.ts +13 -0
- package/src/lib/controller-files.ts +79 -0
- package/src/lib/controller-ownership.ts +397 -0
- package/src/lib/display-path.ts +16 -0
- package/src/lib/doctor/controller-findings.ts +157 -0
- package/src/lib/doctor/controller-fix-plans.ts +184 -0
- package/src/lib/doctor/controller-placeholders.ts +242 -0
- package/src/lib/doctor/controllers.ts +23 -0
- package/src/lib/doctor/environment.ts +289 -0
- package/src/lib/doctor/fixes.ts +84 -0
- package/src/lib/doctor/project.ts +435 -0
- package/src/lib/doctor/types.ts +95 -0
- package/src/lib/errors.ts +379 -0
- package/src/lib/help-text.ts +88 -0
- package/src/lib/load-route-map-worker.ts +110 -0
- package/src/lib/parse-args.ts +82 -0
- package/src/lib/remix-version.ts +80 -0
- package/src/lib/reporter.ts +535 -0
- package/src/lib/route-map.ts +303 -0
- package/src/lib/skills-cache.ts +140 -0
- package/src/lib/skills.ts +706 -0
- package/src/lib/terminal.ts +86 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import * as process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import { checkControllerConventions } from '../doctor/controllers.ts'
|
|
5
|
+
import type { CliContext } from '../cli-context.ts'
|
|
6
|
+
import { checkEnvironment, getEnvironmentFixPlans } from '../doctor/environment.ts'
|
|
7
|
+
import { applyDoctorFixPlans } from '../doctor/fixes.ts'
|
|
8
|
+
import { checkProject, getProjectFixPlans } from '../doctor/project.ts'
|
|
9
|
+
import {
|
|
10
|
+
createDoctorSuite,
|
|
11
|
+
createSkippedDoctorSuite,
|
|
12
|
+
type DoctorAppliedFix,
|
|
13
|
+
type DoctorFinding,
|
|
14
|
+
type DoctorReport,
|
|
15
|
+
type DoctorSuiteName,
|
|
16
|
+
type DoctorSuiteResult,
|
|
17
|
+
} from '../doctor/types.ts'
|
|
18
|
+
import { renderCliError, toCliError } from '../errors.ts'
|
|
19
|
+
import { formatHelpText } from '../help-text.ts'
|
|
20
|
+
import { parseArgs } from '../parse-args.ts'
|
|
21
|
+
import {
|
|
22
|
+
createCommandReporter,
|
|
23
|
+
createStepProgressReporter,
|
|
24
|
+
type CommandReporter,
|
|
25
|
+
type StepProgressReporter,
|
|
26
|
+
} from '../reporter.ts'
|
|
27
|
+
|
|
28
|
+
const DOCTOR_SUITE_LABELS = {
|
|
29
|
+
controllers: {
|
|
30
|
+
complete: 'controllers',
|
|
31
|
+
running: 'Checking controllers',
|
|
32
|
+
},
|
|
33
|
+
environment: {
|
|
34
|
+
complete: 'environment',
|
|
35
|
+
running: 'Checking environment',
|
|
36
|
+
},
|
|
37
|
+
project: {
|
|
38
|
+
complete: 'project',
|
|
39
|
+
running: 'Checking project',
|
|
40
|
+
},
|
|
41
|
+
} satisfies Record<DoctorSuiteName, string | { complete: string; running: string }>
|
|
42
|
+
|
|
43
|
+
interface DoctorCommandOptions {
|
|
44
|
+
fix: boolean
|
|
45
|
+
json: boolean
|
|
46
|
+
strict: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runDoctorCommand(argv: string[], context: CliContext): Promise<number> {
|
|
50
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
51
|
+
process.stdout.write(getDoctorCommandHelpText())
|
|
52
|
+
return 0
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let reporter: CommandReporter | null = null
|
|
56
|
+
let progress: StepProgressReporter<DoctorSuiteName> | null = null
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
let options = parseDoctorCommandArgs(argv)
|
|
60
|
+
reporter = options.json
|
|
61
|
+
? null
|
|
62
|
+
: createCommandReporter({
|
|
63
|
+
remixVersion: context.remixVersion,
|
|
64
|
+
stderr: process.stdout,
|
|
65
|
+
stdout: process.stdout,
|
|
66
|
+
})
|
|
67
|
+
progress = reporter == null ? null : createDoctorProgressReporter(reporter)
|
|
68
|
+
if (reporter != null) {
|
|
69
|
+
await reporter.status.commandHeader('doctor')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let report = await collectDoctorReport(progress, options, reporter, context)
|
|
73
|
+
|
|
74
|
+
if (options.json) {
|
|
75
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
76
|
+
} else if (reporter != null) {
|
|
77
|
+
writeDoctorReport(reporter, report)
|
|
78
|
+
reporter.finish()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if ((options.fix || options.strict) && hasWarningFindings(report.findings)) {
|
|
82
|
+
return 1
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return 0
|
|
86
|
+
} catch (error) {
|
|
87
|
+
progress?.writeSummaryGap()
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
renderCliError(toCliError(error), { helpText: getDoctorCommandHelpText(process.stderr) }),
|
|
90
|
+
)
|
|
91
|
+
reporter?.finish()
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getDoctorCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
|
|
97
|
+
return formatHelpText(
|
|
98
|
+
{
|
|
99
|
+
description: 'Check project environment and Remix app conventions for the current project.',
|
|
100
|
+
examples: [
|
|
101
|
+
'remix doctor',
|
|
102
|
+
'remix doctor --json',
|
|
103
|
+
'remix doctor --strict',
|
|
104
|
+
'remix doctor --fix',
|
|
105
|
+
],
|
|
106
|
+
options: [
|
|
107
|
+
{ description: 'Print doctor findings as JSON', label: '--json' },
|
|
108
|
+
{
|
|
109
|
+
description: 'Exit with status 1 when warning-level findings are present',
|
|
110
|
+
label: '--strict',
|
|
111
|
+
},
|
|
112
|
+
{ description: 'Apply low-risk project and controller fixes', label: '--fix' },
|
|
113
|
+
],
|
|
114
|
+
usage: ['remix doctor [--json] [--strict] [--fix] [--no-color]'],
|
|
115
|
+
},
|
|
116
|
+
target,
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function parseDoctorCommandArgs(argv: string[]): DoctorCommandOptions {
|
|
121
|
+
let parsed = parseArgs(
|
|
122
|
+
argv,
|
|
123
|
+
{
|
|
124
|
+
fix: { flag: '--fix', type: 'boolean' },
|
|
125
|
+
json: { flag: '--json', type: 'boolean' },
|
|
126
|
+
strict: { flag: '--strict', type: 'boolean' },
|
|
127
|
+
},
|
|
128
|
+
{ maxPositionals: 0 },
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
fix: parsed.options.fix,
|
|
133
|
+
json: parsed.options.json,
|
|
134
|
+
strict: parsed.options.strict,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function collectDoctorReport(
|
|
139
|
+
progress: StepProgressReporter<DoctorSuiteName> | null,
|
|
140
|
+
options: DoctorCommandOptions,
|
|
141
|
+
reporter: CommandReporter | null,
|
|
142
|
+
context: CliContext,
|
|
143
|
+
): Promise<DoctorReport> {
|
|
144
|
+
let cwd = context.cwd
|
|
145
|
+
let appliedFixes: DoctorAppliedFix[] = []
|
|
146
|
+
let environment = await runDoctorSuite(progress, 'environment', async () => {
|
|
147
|
+
let result = await checkEnvironment(cwd)
|
|
148
|
+
let suiteAppliedFixes: DoctorAppliedFix[] = []
|
|
149
|
+
let finalResult = result
|
|
150
|
+
|
|
151
|
+
if (options.fix && result.projectRoot != null) {
|
|
152
|
+
let fixPlans = getEnvironmentFixPlans(result, context.remixVersion)
|
|
153
|
+
if (fixPlans.length > 0) {
|
|
154
|
+
suiteAppliedFixes = await applyDoctorFixPlans(result.projectRoot, fixPlans)
|
|
155
|
+
finalResult = await checkEnvironment(result.projectRoot)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let suite = finalResult.suite
|
|
160
|
+
if (suiteAppliedFixes.length > 0) {
|
|
161
|
+
suite = {
|
|
162
|
+
...suite,
|
|
163
|
+
appliedFixes: suiteAppliedFixes,
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
appliedFixes: suiteAppliedFixes,
|
|
169
|
+
projectRoot: finalResult.projectRoot ?? result.projectRoot,
|
|
170
|
+
suite,
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
let findings = [...environment.suite.findings]
|
|
174
|
+
appliedFixes.push(...(environment.appliedFixes ?? []))
|
|
175
|
+
let suites: DoctorSuiteResult[] = [environment.suite]
|
|
176
|
+
let routesFile =
|
|
177
|
+
environment.projectRoot == null
|
|
178
|
+
? undefined
|
|
179
|
+
: path.join(environment.projectRoot, 'app', 'routes.ts')
|
|
180
|
+
writeDoctorSuiteDetails(reporter, environment.suite)
|
|
181
|
+
progress?.writeSummaryGap()
|
|
182
|
+
|
|
183
|
+
if (hasWarningFindings(environment.suite.findings)) {
|
|
184
|
+
let projectSuite = createSkippedDoctorSuite('project', 'Blocked by environment warnings.')
|
|
185
|
+
let controllersSuite = createSkippedDoctorSuite(
|
|
186
|
+
'controllers',
|
|
187
|
+
'Blocked by environment warnings.',
|
|
188
|
+
)
|
|
189
|
+
suites.push(projectSuite, controllersSuite)
|
|
190
|
+
progress?.skip(projectSuite.name, projectSuite.reason)
|
|
191
|
+
writeDoctorSuiteDetails(reporter, projectSuite)
|
|
192
|
+
progress?.writeSummaryGap()
|
|
193
|
+
progress?.skip(controllersSuite.name, controllersSuite.reason)
|
|
194
|
+
writeDoctorSuiteDetails(reporter, controllersSuite)
|
|
195
|
+
progress?.writeSummaryGap()
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
appRoot: environment.projectRoot,
|
|
199
|
+
appliedFixes,
|
|
200
|
+
findings,
|
|
201
|
+
remainingFindings: findings,
|
|
202
|
+
routesFile,
|
|
203
|
+
suites,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let project = await runDoctorSuite(progress, 'project', async () => {
|
|
208
|
+
let result = await checkProject(environment.projectRoot!)
|
|
209
|
+
let suiteAppliedFixes: DoctorAppliedFix[] = []
|
|
210
|
+
let finalResult = result
|
|
211
|
+
|
|
212
|
+
if (options.fix) {
|
|
213
|
+
let fixPlans = await getProjectFixPlans(environment.projectRoot!)
|
|
214
|
+
if (fixPlans.length > 0) {
|
|
215
|
+
suiteAppliedFixes = await applyDoctorFixPlans(environment.projectRoot!, fixPlans)
|
|
216
|
+
finalResult = await checkProject(environment.projectRoot!)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let suite = finalResult.suite
|
|
221
|
+
if (suiteAppliedFixes.length > 0) {
|
|
222
|
+
suite = {
|
|
223
|
+
...suite,
|
|
224
|
+
appliedFixes: suiteAppliedFixes,
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
appliedFixes: suiteAppliedFixes,
|
|
230
|
+
routeManifest: finalResult.routeManifest,
|
|
231
|
+
routesFile: finalResult.routesFile,
|
|
232
|
+
suite,
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
findings.push(...project.suite.findings)
|
|
236
|
+
appliedFixes.push(...(project.appliedFixes ?? []))
|
|
237
|
+
routesFile = project.routesFile
|
|
238
|
+
suites.push(project.suite)
|
|
239
|
+
writeDoctorSuiteDetails(reporter, project.suite)
|
|
240
|
+
progress?.writeSummaryGap()
|
|
241
|
+
|
|
242
|
+
if (hasWarningFindings(project.suite.findings)) {
|
|
243
|
+
let controllersSuite = createSkippedDoctorSuite('controllers', 'Blocked by project warnings.')
|
|
244
|
+
suites.push(controllersSuite)
|
|
245
|
+
progress?.skip(controllersSuite.name, controllersSuite.reason)
|
|
246
|
+
writeDoctorSuiteDetails(reporter, controllersSuite)
|
|
247
|
+
progress?.writeSummaryGap()
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
appRoot: environment.projectRoot,
|
|
251
|
+
appliedFixes,
|
|
252
|
+
findings,
|
|
253
|
+
remainingFindings: findings,
|
|
254
|
+
routesFile,
|
|
255
|
+
suites,
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let controllers = await runDoctorSuite(progress, 'controllers', async () => {
|
|
260
|
+
let controllerResult = await checkControllerConventions(
|
|
261
|
+
project.routeManifest!.appRoot,
|
|
262
|
+
project.routeManifest!.tree,
|
|
263
|
+
)
|
|
264
|
+
let remainingFindings = controllerResult.suite.findings
|
|
265
|
+
let suiteAppliedFixes: DoctorAppliedFix[] = []
|
|
266
|
+
|
|
267
|
+
if (options.fix && controllerResult.fixPlans.length > 0) {
|
|
268
|
+
suiteAppliedFixes = await applyDoctorFixPlans(
|
|
269
|
+
project.routeManifest!.appRoot,
|
|
270
|
+
controllerResult.fixPlans,
|
|
271
|
+
)
|
|
272
|
+
let finalControllerResult = await checkControllerConventions(
|
|
273
|
+
project.routeManifest!.appRoot,
|
|
274
|
+
project.routeManifest!.tree,
|
|
275
|
+
)
|
|
276
|
+
remainingFindings = finalControllerResult.suite.findings
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let suite = createDoctorSuite('controllers', remainingFindings)
|
|
280
|
+
if (suiteAppliedFixes.length > 0) {
|
|
281
|
+
suite.appliedFixes = suiteAppliedFixes
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { appliedFixes: suiteAppliedFixes, suite }
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
findings.push(...controllers.suite.findings)
|
|
288
|
+
appliedFixes.push(...(controllers.appliedFixes ?? []))
|
|
289
|
+
suites.push(controllers.suite)
|
|
290
|
+
writeDoctorSuiteDetails(reporter, controllers.suite)
|
|
291
|
+
progress?.writeSummaryGap()
|
|
292
|
+
|
|
293
|
+
let report: DoctorReport = {
|
|
294
|
+
appRoot: environment.projectRoot,
|
|
295
|
+
findings,
|
|
296
|
+
routesFile,
|
|
297
|
+
suites,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (options.fix) {
|
|
301
|
+
report.appliedFixes = appliedFixes
|
|
302
|
+
report.remainingFindings = findings
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return report
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function hasWarningFindings(findings: DoctorFinding[]): boolean {
|
|
309
|
+
return findings.some((finding) => finding.severity === 'warn')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function formatAppliedFix(appliedFix: DoctorAppliedFix): string {
|
|
313
|
+
if (appliedFix.kind === 'update-file') {
|
|
314
|
+
return `Updated ${appliedFix.path}`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return `Created ${appliedFix.path}`
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function runDoctorSuite<
|
|
321
|
+
result extends { appliedFixes?: DoctorAppliedFix[]; suite: DoctorSuiteResult },
|
|
322
|
+
>(
|
|
323
|
+
progress: StepProgressReporter<DoctorSuiteName> | null,
|
|
324
|
+
label: DoctorSuiteName,
|
|
325
|
+
callback: () => Promise<result>,
|
|
326
|
+
): Promise<result> {
|
|
327
|
+
progress?.start(label)
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
let result = await callback()
|
|
331
|
+
|
|
332
|
+
if (result.suite.status === 'ok') {
|
|
333
|
+
progress?.succeed(label)
|
|
334
|
+
} else if (result.suite.status === 'issues') {
|
|
335
|
+
progress?.fail(label)
|
|
336
|
+
} else {
|
|
337
|
+
progress?.skip(label, result.suite.reason)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result
|
|
341
|
+
} catch (error) {
|
|
342
|
+
progress?.fail(label)
|
|
343
|
+
throw error
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function createDoctorProgressReporter(
|
|
348
|
+
reporter: CommandReporter,
|
|
349
|
+
): StepProgressReporter<DoctorSuiteName> {
|
|
350
|
+
return createStepProgressReporter(reporter.status, DOCTOR_SUITE_LABELS)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function writeDoctorReport(reporter: CommandReporter, report: DoctorReport): void {
|
|
354
|
+
let warningCount = report.findings.filter((finding) => finding.severity === 'warn').length
|
|
355
|
+
let adviceCount = report.findings.length - warningCount
|
|
356
|
+
|
|
357
|
+
if ((report.appliedFixes?.length ?? 0) > 0) {
|
|
358
|
+
let fixCount = report.appliedFixes!.length
|
|
359
|
+
reporter.out.line(`Applied ${fixCount} ${fixCount === 1 ? 'fix' : 'fixes'}.`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (report.findings.length === 0) {
|
|
363
|
+
reporter.out.line('Doctor found no issues.')
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
reporter.out.line(`Summary: ${warningCount} warnings, ${adviceCount} advice.`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function writeDoctorSuiteDetails(reporter: CommandReporter | null, suite: DoctorSuiteResult): void {
|
|
370
|
+
if (reporter == null) {
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (suite.findings.length === 0 && (suite.appliedFixes?.length ?? 0) === 0) {
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
reporter.out.withIndent(() => {
|
|
379
|
+
for (let finding of suite.findings) {
|
|
380
|
+
reporter.out.bullet(
|
|
381
|
+
reporter.out.label(finding.severity.toUpperCase(), finding.message, {
|
|
382
|
+
tone: finding.severity === 'warn' ? 'warn' : undefined,
|
|
383
|
+
}),
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if ((suite.appliedFixes?.length ?? 0) > 0) {
|
|
388
|
+
reporter.out.section('Applied fixes:', () => {
|
|
389
|
+
reporter.out.bullets((suite.appliedFixes ?? []).map(formatAppliedFix))
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { getCompletionCommandHelpText } from './completion.ts'
|
|
4
|
+
import { renderCliError, toCliError, unknownHelpTopic } from '../errors.ts'
|
|
5
|
+
import { formatHelpText } from '../help-text.ts'
|
|
6
|
+
import { getDoctorCommandHelpText } from './doctor.ts'
|
|
7
|
+
import { getNewCommandHelpText } from './new.ts'
|
|
8
|
+
import { getRoutesCommandHelpText } from './routes.ts'
|
|
9
|
+
import {
|
|
10
|
+
getSkillsCommandHelpText,
|
|
11
|
+
getSkillsInstallCommandHelpText,
|
|
12
|
+
getSkillsListCommandHelpText,
|
|
13
|
+
} from './skills.ts'
|
|
14
|
+
import { getTestCommandHelpText } from './test.ts'
|
|
15
|
+
import { getVersionCommandHelpText } from './version.ts'
|
|
16
|
+
|
|
17
|
+
export async function runHelpCommand(argv: string[]): Promise<number> {
|
|
18
|
+
if (argv.includes('-h') || argv.includes('--help')) {
|
|
19
|
+
process.stdout.write(getHelpCommandHelpText())
|
|
20
|
+
return 0
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
process.stdout.write(getCommandHelpText(argv))
|
|
25
|
+
return 0
|
|
26
|
+
} catch (error) {
|
|
27
|
+
process.stderr.write(
|
|
28
|
+
renderCliError(toCliError(error), { helpText: getCliHelpText(process.stderr) }),
|
|
29
|
+
)
|
|
30
|
+
return 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getCliHelpText(target: NodeJS.WriteStream = process.stdout): string {
|
|
35
|
+
return formatHelpText(
|
|
36
|
+
{
|
|
37
|
+
commands: [
|
|
38
|
+
{ description: 'Print shell completion scripts for Remix', label: 'completion' },
|
|
39
|
+
{ description: 'Show help for Remix commands', label: 'help [command]' },
|
|
40
|
+
{ description: 'Create a new Remix project', label: 'new <name>' },
|
|
41
|
+
{ description: 'Check project health for the current project', label: 'doctor' },
|
|
42
|
+
{ description: 'Show the route tree for the current project', label: 'routes' },
|
|
43
|
+
{ description: 'Manage Remix skills for the current project', label: 'skills' },
|
|
44
|
+
{ description: 'Run tests for the current project', label: 'test [glob]' },
|
|
45
|
+
{ description: 'Show the current Remix version', label: 'version' },
|
|
46
|
+
],
|
|
47
|
+
examples: [
|
|
48
|
+
'remix completion bash',
|
|
49
|
+
'remix help',
|
|
50
|
+
'remix help completion',
|
|
51
|
+
'remix help doctor',
|
|
52
|
+
'remix help skills install',
|
|
53
|
+
'remix doctor',
|
|
54
|
+
'remix new my-remix-app',
|
|
55
|
+
'remix new my-remix-app --app-name "My Remix App"',
|
|
56
|
+
'remix routes',
|
|
57
|
+
'remix skills install',
|
|
58
|
+
'remix test',
|
|
59
|
+
'remix version',
|
|
60
|
+
],
|
|
61
|
+
options: [
|
|
62
|
+
{ description: 'Show help', label: '-h, --help' },
|
|
63
|
+
{ description: 'Disable ANSI color output', label: '--no-color' },
|
|
64
|
+
{ description: 'Show version', label: '-v, --version' },
|
|
65
|
+
],
|
|
66
|
+
usage: ['remix <command> [options]'],
|
|
67
|
+
},
|
|
68
|
+
target,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getHelpCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
|
|
73
|
+
return formatHelpText(
|
|
74
|
+
{
|
|
75
|
+
description: 'Show help for Remix commands.',
|
|
76
|
+
examples: [
|
|
77
|
+
'remix help',
|
|
78
|
+
'remix help completion',
|
|
79
|
+
'remix help doctor',
|
|
80
|
+
'remix help new',
|
|
81
|
+
'remix help routes',
|
|
82
|
+
'remix help skills install',
|
|
83
|
+
'remix help test',
|
|
84
|
+
'remix help version',
|
|
85
|
+
],
|
|
86
|
+
usage: ['remix help [command]'],
|
|
87
|
+
},
|
|
88
|
+
target,
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getCommandHelpText(argv: string[]): string {
|
|
93
|
+
if (argv.length === 0) {
|
|
94
|
+
return getCliHelpText()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let [command, ...rest] = argv
|
|
98
|
+
|
|
99
|
+
if (command === 'help') {
|
|
100
|
+
return rest.length === 0 ? getHelpCommandHelpText() : getNestedHelpText(command, rest)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (command === 'new' && rest.length === 0) {
|
|
104
|
+
return getNewCommandHelpText()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (command === 'completion' && rest.length === 0) {
|
|
108
|
+
return getCompletionCommandHelpText()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (command === 'doctor' && rest.length === 0) {
|
|
112
|
+
return getDoctorCommandHelpText()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (command === 'routes' && rest.length === 0) {
|
|
116
|
+
return getRoutesCommandHelpText()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (command === 'skills') {
|
|
120
|
+
return getSkillsHelpText(rest)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (command === 'test' && rest.length === 0) {
|
|
124
|
+
return getTestCommandHelpText()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (command === 'version' && rest.length === 0) {
|
|
128
|
+
return getVersionCommandHelpText()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw unknownHelpTopic(argv.join(' '))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getNestedHelpText(command: string, argv: string[]): string {
|
|
135
|
+
throw unknownHelpTopic(`${command} ${argv.join(' ')}`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getSkillsHelpText(argv: string[]): string {
|
|
139
|
+
if (argv.length === 0) {
|
|
140
|
+
return getSkillsCommandHelpText()
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let [subcommand, ...rest] = argv
|
|
144
|
+
if (rest.length > 0) {
|
|
145
|
+
throw unknownHelpTopic(`skills ${argv.join(' ')}`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (subcommand === 'install') {
|
|
149
|
+
return getSkillsInstallCommandHelpText()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (subcommand === 'list') {
|
|
153
|
+
return getSkillsListCommandHelpText()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
throw unknownHelpTopic(`skills ${argv.join(' ')}`)
|
|
157
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
BootstrapProjectOptions,
|
|
5
|
+
BootstrapProjectPhase,
|
|
6
|
+
BootstrapProgressReporter,
|
|
7
|
+
} from '../bootstrap-project.ts'
|
|
8
|
+
import { bootstrapProject } from '../bootstrap-project.ts'
|
|
9
|
+
import type { CliContext } from '../cli-context.ts'
|
|
10
|
+
import { renderCliError, missingTargetDirectory, toCliError } from '../errors.ts'
|
|
11
|
+
import { getDisplayPath } from '../display-path.ts'
|
|
12
|
+
import { formatHelpText } from '../help-text.ts'
|
|
13
|
+
import { parseArgs } from '../parse-args.ts'
|
|
14
|
+
import {
|
|
15
|
+
createCommandReporter,
|
|
16
|
+
createStepProgressReporter,
|
|
17
|
+
type CommandReporter,
|
|
18
|
+
} from '../reporter.ts'
|
|
19
|
+
|
|
20
|
+
const NEW_PROGRESS_LABELS = {
|
|
21
|
+
'finalize-package-json': 'Finalize package.json',
|
|
22
|
+
'generate-scaffold-files': 'Generate scaffold files',
|
|
23
|
+
'prepare-target-directory': 'Prepare target directory',
|
|
24
|
+
} satisfies Record<BootstrapProjectPhase, string>
|
|
25
|
+
|
|
26
|
+
export async function runNewCommand(argv: string[], context: CliContext): Promise<number> {
|
|
27
|
+
if (argv.length === 0 || argv.includes('-h') || argv.includes('--help')) {
|
|
28
|
+
process.stdout.write(getNewCommandHelpText())
|
|
29
|
+
return 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let reporter: CommandReporter | null = null
|
|
33
|
+
let progress: BootstrapProgressReporter | null = null
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
let options = parseNewCommandArgs(argv)
|
|
37
|
+
reporter = createCommandReporter({ remixVersion: context.remixVersion })
|
|
38
|
+
progress = createNewProgressReporter(reporter)
|
|
39
|
+
|
|
40
|
+
await reporter.status.commandHeader('new')
|
|
41
|
+
let result = await bootstrapProject(
|
|
42
|
+
{
|
|
43
|
+
...options,
|
|
44
|
+
cwd: context.cwd,
|
|
45
|
+
remixVersion: context.remixVersion,
|
|
46
|
+
},
|
|
47
|
+
progress,
|
|
48
|
+
)
|
|
49
|
+
progress.writeSummaryGap()
|
|
50
|
+
reporter.out.line(
|
|
51
|
+
`Created ${result.appDisplayName} at ${getDisplayPath(result.targetDir, context.cwd)}`,
|
|
52
|
+
)
|
|
53
|
+
reporter.finish()
|
|
54
|
+
return 0
|
|
55
|
+
} catch (error) {
|
|
56
|
+
progress?.writeSummaryGap()
|
|
57
|
+
process.stderr.write(
|
|
58
|
+
renderCliError(toCliError(error), { helpText: getNewCommandHelpText(process.stderr) }),
|
|
59
|
+
)
|
|
60
|
+
reporter?.finish()
|
|
61
|
+
return 1
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getNewCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
|
|
66
|
+
return formatHelpText(
|
|
67
|
+
{
|
|
68
|
+
description: 'Create a new Remix project in the target directory.',
|
|
69
|
+
examples: [
|
|
70
|
+
'remix new ./my-remix-app',
|
|
71
|
+
'remix new ./my-remix-app --app-name "My Remix App"',
|
|
72
|
+
'remix new ./my-remix-app --force',
|
|
73
|
+
],
|
|
74
|
+
usage: ['remix new <target-dir> [--app-name <name>] [--force]'],
|
|
75
|
+
},
|
|
76
|
+
target,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseNewCommandArgs(argv: string[]): BootstrapProjectOptions {
|
|
81
|
+
let parsed = parseArgs(
|
|
82
|
+
argv,
|
|
83
|
+
{
|
|
84
|
+
appName: { flag: '--app-name', type: 'string' },
|
|
85
|
+
force: { flag: '--force', type: 'boolean' },
|
|
86
|
+
},
|
|
87
|
+
{ maxPositionals: 1 },
|
|
88
|
+
)
|
|
89
|
+
let targetDir = parsed.positionals[0] ?? null
|
|
90
|
+
|
|
91
|
+
if (targetDir == null) {
|
|
92
|
+
throw missingTargetDirectory()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
appName: parsed.options.appName ?? null,
|
|
97
|
+
force: parsed.options.force,
|
|
98
|
+
targetDir,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function createNewProgressReporter(reporter: CommandReporter): BootstrapProgressReporter {
|
|
103
|
+
return createStepProgressReporter(reporter.status, NEW_PROGRESS_LABELS)
|
|
104
|
+
}
|