@remix-run/cli 0.0.0 → 0.2.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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -3
  3. package/bootstrap/.agents/skills/remix/SKILL.md +501 -0
  4. package/bootstrap/.agents/skills/remix/references/animate-elements.md +195 -0
  5. package/bootstrap/.agents/skills/remix/references/assets-and-browser-modules.md +122 -0
  6. package/bootstrap/.agents/skills/remix/references/auth-and-sessions.md +420 -0
  7. package/bootstrap/.agents/skills/remix/references/component-model.md +282 -0
  8. package/bootstrap/.agents/skills/remix/references/create-mixins.md +158 -0
  9. package/bootstrap/.agents/skills/remix/references/data-and-validation.md +363 -0
  10. package/bootstrap/.agents/skills/remix/references/hydration-frames-navigation.md +297 -0
  11. package/bootstrap/.agents/skills/remix/references/middleware-and-server.md +243 -0
  12. package/bootstrap/.agents/skills/remix/references/mixins-styling-events.md +213 -0
  13. package/bootstrap/.agents/skills/remix/references/routing-and-controllers.md +324 -0
  14. package/bootstrap/.agents/skills/remix/references/testing-patterns.md +156 -0
  15. package/bootstrap/AGENTS.md +39 -0
  16. package/bootstrap/README.md +27 -0
  17. package/bootstrap/app/assets/entry.ts +19 -0
  18. package/bootstrap/app/assets.ts +18 -0
  19. package/bootstrap/app/controllers/auth.tsx +21 -0
  20. package/bootstrap/app/controllers/home.tsx +11 -0
  21. package/bootstrap/app/router.ts +16 -0
  22. package/bootstrap/app/routes.ts +7 -0
  23. package/bootstrap/app/ui/document.tsx +26 -0
  24. package/bootstrap/app/ui/layout.tsx +22 -0
  25. package/bootstrap/app/ui/prompt-button.tsx +162 -0
  26. package/bootstrap/app/ui/scaffold-home-page.tsx +526 -0
  27. package/bootstrap/app/utils/render.tsx +26 -0
  28. package/bootstrap/package.json +22 -0
  29. package/bootstrap/server.ts +37 -0
  30. package/bootstrap/tsconfig.json +18 -0
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/lib/bootstrap-project.d.ts +17 -0
  35. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  36. package/dist/lib/bootstrap-project.js +137 -0
  37. package/dist/lib/cli-context.d.ts +11 -0
  38. package/dist/lib/cli-context.d.ts.map +1 -0
  39. package/dist/lib/cli-context.js +19 -0
  40. package/dist/lib/cli.d.ts +6 -0
  41. package/dist/lib/cli.d.ts.map +1 -0
  42. package/dist/lib/cli.js +84 -0
  43. package/dist/lib/commands/completion.d.ts +3 -0
  44. package/dist/lib/commands/completion.d.ts.map +1 -0
  45. package/dist/lib/commands/completion.js +51 -0
  46. package/dist/lib/commands/doctor.d.ts +4 -0
  47. package/dist/lib/commands/doctor.d.ts.map +1 -0
  48. package/dist/lib/commands/doctor.js +293 -0
  49. package/dist/lib/commands/help.d.ts +4 -0
  50. package/dist/lib/commands/help.d.ts.map +1 -0
  51. package/dist/lib/commands/help.js +100 -0
  52. package/dist/lib/commands/new.d.ts +4 -0
  53. package/dist/lib/commands/new.d.ts.map +1 -0
  54. package/dist/lib/commands/new.js +70 -0
  55. package/dist/lib/commands/routes.d.ts +4 -0
  56. package/dist/lib/commands/routes.d.ts.map +1 -0
  57. package/dist/lib/commands/routes.js +161 -0
  58. package/dist/lib/commands/test.d.ts +4 -0
  59. package/dist/lib/commands/test.d.ts.map +1 -0
  60. package/dist/lib/commands/test.js +23 -0
  61. package/dist/lib/commands/version.d.ts +4 -0
  62. package/dist/lib/commands/version.d.ts.map +1 -0
  63. package/dist/lib/commands/version.js +29 -0
  64. package/dist/lib/completion.d.ts +12 -0
  65. package/dist/lib/completion.d.ts.map +1 -0
  66. package/dist/lib/completion.js +310 -0
  67. package/dist/lib/contained-path.d.ts +2 -0
  68. package/dist/lib/contained-path.d.ts.map +1 -0
  69. package/dist/lib/contained-path.js +10 -0
  70. package/dist/lib/controller-files.d.ts +14 -0
  71. package/dist/lib/controller-files.d.ts.map +1 -0
  72. package/dist/lib/controller-files.js +53 -0
  73. package/dist/lib/controller-ownership.d.ts +41 -0
  74. package/dist/lib/controller-ownership.d.ts.map +1 -0
  75. package/dist/lib/controller-ownership.js +224 -0
  76. package/dist/lib/display-path.d.ts +2 -0
  77. package/dist/lib/display-path.d.ts.map +1 -0
  78. package/dist/lib/display-path.js +12 -0
  79. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  80. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  81. package/dist/lib/doctor/controller-findings.js +132 -0
  82. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  83. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  84. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  85. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  86. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  87. package/dist/lib/doctor/controller-placeholders.js +183 -0
  88. package/dist/lib/doctor/controllers.d.ts +8 -0
  89. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  90. package/dist/lib/doctor/controllers.js +13 -0
  91. package/dist/lib/doctor/environment.d.ts +17 -0
  92. package/dist/lib/doctor/environment.d.ts.map +1 -0
  93. package/dist/lib/doctor/environment.js +226 -0
  94. package/dist/lib/doctor/fixes.d.ts +3 -0
  95. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  96. package/dist/lib/doctor/fixes.js +65 -0
  97. package/dist/lib/doctor/project.d.ts +10 -0
  98. package/dist/lib/doctor/project.d.ts.map +1 -0
  99. package/dist/lib/doctor/project.js +371 -0
  100. package/dist/lib/doctor/types.d.ts +48 -0
  101. package/dist/lib/doctor/types.d.ts.map +1 -0
  102. package/dist/lib/doctor/types.js +15 -0
  103. package/dist/lib/errors.d.ts +167 -0
  104. package/dist/lib/errors.d.ts.map +1 -0
  105. package/dist/lib/errors.js +307 -0
  106. package/dist/lib/help-text.d.ts +13 -0
  107. package/dist/lib/help-text.d.ts.map +1 -0
  108. package/dist/lib/help-text.js +43 -0
  109. package/dist/lib/load-route-map-worker.d.ts +2 -0
  110. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  111. package/dist/lib/load-route-map-worker.js +92 -0
  112. package/dist/lib/parse-args.d.ts +22 -0
  113. package/dist/lib/parse-args.d.ts.map +1 -0
  114. package/dist/lib/parse-args.js +35 -0
  115. package/dist/lib/remix-version.d.ts +2 -0
  116. package/dist/lib/remix-version.d.ts.map +1 -0
  117. package/dist/lib/remix-version.js +58 -0
  118. package/dist/lib/reporter.d.ts +56 -0
  119. package/dist/lib/reporter.d.ts.map +1 -0
  120. package/dist/lib/reporter.js +357 -0
  121. package/dist/lib/route-map.d.ts +38 -0
  122. package/dist/lib/route-map.d.ts.map +1 -0
  123. package/dist/lib/route-map.js +195 -0
  124. package/dist/lib/terminal.d.ts +17 -0
  125. package/dist/lib/terminal.d.ts.map +1 -0
  126. package/dist/lib/terminal.js +66 -0
  127. package/package.json +47 -5
  128. package/src/index.ts +1 -0
  129. package/src/lib/bootstrap-project.ts +222 -0
  130. package/src/lib/cli-context.ts +38 -0
  131. package/src/lib/cli.ts +116 -0
  132. package/src/lib/commands/completion.ts +74 -0
  133. package/src/lib/commands/doctor.ts +393 -0
  134. package/src/lib/commands/help.ts +123 -0
  135. package/src/lib/commands/new.ts +104 -0
  136. package/src/lib/commands/routes.ts +251 -0
  137. package/src/lib/commands/test.ts +31 -0
  138. package/src/lib/commands/version.ts +39 -0
  139. package/src/lib/completion.ts +418 -0
  140. package/src/lib/contained-path.ts +13 -0
  141. package/src/lib/controller-files.ts +79 -0
  142. package/src/lib/controller-ownership.ts +397 -0
  143. package/src/lib/display-path.ts +16 -0
  144. package/src/lib/doctor/controller-findings.ts +157 -0
  145. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  146. package/src/lib/doctor/controller-placeholders.ts +242 -0
  147. package/src/lib/doctor/controllers.ts +23 -0
  148. package/src/lib/doctor/environment.ts +289 -0
  149. package/src/lib/doctor/fixes.ts +84 -0
  150. package/src/lib/doctor/project.ts +435 -0
  151. package/src/lib/doctor/types.ts +95 -0
  152. package/src/lib/errors.ts +367 -0
  153. package/src/lib/help-text.ts +88 -0
  154. package/src/lib/load-route-map-worker.ts +110 -0
  155. package/src/lib/parse-args.ts +82 -0
  156. package/src/lib/remix-version.ts +80 -0
  157. package/src/lib/reporter.ts +535 -0
  158. package/src/lib/route-map.ts +303 -0
  159. package/src/lib/terminal.ts +86 -0
package/src/lib/cli.ts ADDED
@@ -0,0 +1,116 @@
1
+ import * as process from 'node:process'
2
+
3
+ import { getCliHelpText, runHelpCommand } from './commands/help.ts'
4
+ import { resolveCliContext, type CliContext } from './cli-context.ts'
5
+ import { renderCliError, unknownCommand } from './errors.ts'
6
+ import { configureColors, restoreTerminalFormatting } from './terminal.ts'
7
+
8
+ export interface RunRemixOptions {
9
+ cwd?: string
10
+ remixVersion?: string
11
+ }
12
+
13
+ export async function runRemix(
14
+ argv: string[] = process.argv.slice(2),
15
+ options: RunRemixOptions = {},
16
+ ): Promise<number> {
17
+ let context = await resolveCliContext(options)
18
+
19
+ try {
20
+ while (argv[0] === '--') {
21
+ argv = argv.slice(1)
22
+ }
23
+
24
+ let globalOptions = extractGlobalOptions(argv)
25
+ argv = globalOptions.argv
26
+ configureColors({ disabled: globalOptions.noColor })
27
+
28
+ if (argv.length === 0) {
29
+ process.stdout.write(getCliHelpText())
30
+ return 0
31
+ }
32
+
33
+ let [command, ...rest] = argv
34
+
35
+ if (command === '-h' || command === '--help') {
36
+ process.stdout.write(getCliHelpText())
37
+ return 0
38
+ }
39
+
40
+ return await runCommand(command, rest, context)
41
+ } finally {
42
+ restoreTerminalFormatting()
43
+ }
44
+ }
45
+
46
+ async function runCommand(command: string, argv: string[], context: CliContext): Promise<number> {
47
+ if (command === 'help') {
48
+ return runHelpCommand(argv)
49
+ }
50
+
51
+ if (command === '-v' || command === '--version') {
52
+ let { runVersionCommand } = await import('./commands/version.ts')
53
+ return runVersionCommand([], context)
54
+ }
55
+
56
+ if (command === 'new') {
57
+ let { runNewCommand } = await import('./commands/new.ts')
58
+ return runNewCommand(argv, context)
59
+ }
60
+
61
+ if (command === 'completion') {
62
+ let { runCompletionCommand } = await import('./commands/completion.ts')
63
+ return runCompletionCommand(argv)
64
+ }
65
+
66
+ if (command === 'doctor') {
67
+ let { runDoctorCommand } = await import('./commands/doctor.ts')
68
+ return runDoctorCommand(argv, context)
69
+ }
70
+
71
+ if (command === 'routes') {
72
+ let { runRoutesCommand } = await import('./commands/routes.ts')
73
+ return runRoutesCommand(argv, context)
74
+ }
75
+
76
+ if (command === 'test') {
77
+ let { runTestCommand } = await import('./commands/test.ts')
78
+ return runTestCommand(argv, context)
79
+ }
80
+
81
+ if (command === 'version') {
82
+ let { runVersionCommand } = await import('./commands/version.ts')
83
+ return runVersionCommand(argv, context)
84
+ }
85
+
86
+ process.stderr.write(
87
+ renderCliError(unknownCommand(command), { helpText: getCliHelpText(process.stderr) }),
88
+ )
89
+ return 1
90
+ }
91
+
92
+ function extractGlobalOptions(argv: string[]): { argv: string[]; noColor: boolean } {
93
+ let filteredArgv: string[] = []
94
+ let noColor = false
95
+
96
+ for (let index = 0; index < argv.length; index++) {
97
+ let arg = argv[index]!
98
+
99
+ if (arg === '--') {
100
+ filteredArgv.push(...argv.slice(index))
101
+ break
102
+ }
103
+
104
+ if (arg === '--no-color') {
105
+ noColor = true
106
+ continue
107
+ }
108
+
109
+ filteredArgv.push(arg)
110
+ }
111
+
112
+ return {
113
+ argv: filteredArgv,
114
+ noColor,
115
+ }
116
+ }
@@ -0,0 +1,74 @@
1
+ import * as process from 'node:process'
2
+
3
+ import {
4
+ getCompletionResult,
5
+ getCompletionScript,
6
+ isCompletionShell,
7
+ renderCompletionResult,
8
+ } from '../completion.ts'
9
+ import {
10
+ invalidCompletionRequest,
11
+ renderCliError,
12
+ toCliError,
13
+ unknownCompletionShell,
14
+ } from '../errors.ts'
15
+ import { formatHelpText } from '../help-text.ts'
16
+ import { parseArgs } from '../parse-args.ts'
17
+
18
+ export async function runCompletionCommand(argv: string[]): Promise<number> {
19
+ if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
20
+ process.stdout.write(getCompletionCommandHelpText())
21
+ return 0
22
+ }
23
+
24
+ if (argv[0] === '--') {
25
+ return runCompletionPlumbing(argv.slice(1))
26
+ }
27
+
28
+ let [shell, ...rest] = argv
29
+
30
+ try {
31
+ parseArgs(rest, {}, { maxPositionals: 0 })
32
+
33
+ if (!isCompletionShell(shell)) {
34
+ throw unknownCompletionShell(shell)
35
+ }
36
+
37
+ process.stdout.write(getCompletionScript())
38
+ return 0
39
+ } catch (error) {
40
+ process.stderr.write(
41
+ renderCliError(toCliError(error), {
42
+ helpText: getCompletionCommandHelpText(process.stderr),
43
+ }),
44
+ )
45
+ return 1
46
+ }
47
+ }
48
+
49
+ export function getCompletionCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
50
+ return formatHelpText(
51
+ {
52
+ description: 'Print a shell completion script for Remix.',
53
+ examples: ['remix completion bash >> ~/.bashrc', 'remix completion zsh >> ~/.zshrc'],
54
+ usage: ['remix completion <bash|zsh>'],
55
+ },
56
+ target,
57
+ )
58
+ }
59
+
60
+ function runCompletionPlumbing(argv: string[]): number {
61
+ try {
62
+ let [currentIndexArg, ...words] = argv
63
+ if (currentIndexArg == null || !/^\d+$/.test(currentIndexArg)) {
64
+ throw invalidCompletionRequest()
65
+ }
66
+
67
+ let currentIndex = Number(currentIndexArg)
68
+ process.stdout.write(renderCompletionResult(getCompletionResult(words, currentIndex)))
69
+ return 0
70
+ } catch (error) {
71
+ process.stderr.write(renderCliError(toCliError(error)))
72
+ return 1
73
+ }
74
+ }
@@ -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,123 @@
1
+ import * as process from 'node:process'
2
+
3
+ import { renderCliError, toCliError, unknownHelpTopic } from '../errors.ts'
4
+ import { formatHelpText } from '../help-text.ts'
5
+
6
+ export async function runHelpCommand(argv: string[]): Promise<number> {
7
+ if (argv.includes('-h') || argv.includes('--help')) {
8
+ process.stdout.write(getHelpCommandHelpText())
9
+ return 0
10
+ }
11
+
12
+ try {
13
+ process.stdout.write(await getCommandHelpText(argv))
14
+ return 0
15
+ } catch (error) {
16
+ process.stderr.write(
17
+ renderCliError(toCliError(error), { helpText: getCliHelpText(process.stderr) }),
18
+ )
19
+ return 1
20
+ }
21
+ }
22
+
23
+ export function getCliHelpText(target: NodeJS.WriteStream = process.stdout): string {
24
+ return formatHelpText(
25
+ {
26
+ commands: [
27
+ { description: 'Print shell completion scripts for Remix', label: 'completion' },
28
+ { description: 'Show help for Remix commands', label: 'help [command]' },
29
+ { description: 'Create a new Remix project', label: 'new <name>' },
30
+ { description: 'Check project health for the current project', label: 'doctor' },
31
+ { description: 'Show the route tree for the current project', label: 'routes' },
32
+ { description: 'Run tests for the current project', label: 'test [glob]' },
33
+ { description: 'Show the current Remix version', label: 'version' },
34
+ ],
35
+ examples: [
36
+ 'remix completion bash',
37
+ 'remix help',
38
+ 'remix help completion',
39
+ 'remix help doctor',
40
+ 'remix doctor',
41
+ 'remix new my-remix-app',
42
+ 'remix new my-remix-app --app-name "My Remix App"',
43
+ 'remix routes',
44
+ 'remix test',
45
+ 'remix version',
46
+ ],
47
+ options: [
48
+ { description: 'Show help', label: '-h, --help' },
49
+ { description: 'Disable ANSI color output', label: '--no-color' },
50
+ { description: 'Show version', label: '-v, --version' },
51
+ ],
52
+ usage: ['remix <command> [options]'],
53
+ },
54
+ target,
55
+ )
56
+ }
57
+
58
+ export function getHelpCommandHelpText(target: NodeJS.WriteStream = process.stdout): string {
59
+ return formatHelpText(
60
+ {
61
+ description: 'Show help for Remix commands.',
62
+ examples: [
63
+ 'remix help',
64
+ 'remix help completion',
65
+ 'remix help doctor',
66
+ 'remix help new',
67
+ 'remix help routes',
68
+ 'remix help test',
69
+ 'remix help version',
70
+ ],
71
+ usage: ['remix help [command]'],
72
+ },
73
+ target,
74
+ )
75
+ }
76
+
77
+ async function getCommandHelpText(argv: string[]): Promise<string> {
78
+ if (argv.length === 0) {
79
+ return getCliHelpText()
80
+ }
81
+
82
+ let [command, ...rest] = argv
83
+
84
+ if (command === 'help') {
85
+ return rest.length === 0 ? getHelpCommandHelpText() : getNestedHelpText(command, rest)
86
+ }
87
+
88
+ if (command === 'new' && rest.length === 0) {
89
+ let { getNewCommandHelpText } = await import('./new.ts')
90
+ return getNewCommandHelpText()
91
+ }
92
+
93
+ if (command === 'completion' && rest.length === 0) {
94
+ let { getCompletionCommandHelpText } = await import('./completion.ts')
95
+ return getCompletionCommandHelpText()
96
+ }
97
+
98
+ if (command === 'doctor' && rest.length === 0) {
99
+ let { getDoctorCommandHelpText } = await import('./doctor.ts')
100
+ return getDoctorCommandHelpText()
101
+ }
102
+
103
+ if (command === 'routes' && rest.length === 0) {
104
+ let { getRoutesCommandHelpText } = await import('./routes.ts')
105
+ return getRoutesCommandHelpText()
106
+ }
107
+
108
+ if (command === 'test' && rest.length === 0) {
109
+ let { getTestCommandHelpText } = await import('./test.ts')
110
+ return await getTestCommandHelpText()
111
+ }
112
+
113
+ if (command === 'version' && rest.length === 0) {
114
+ let { getVersionCommandHelpText } = await import('./version.ts')
115
+ return getVersionCommandHelpText()
116
+ }
117
+
118
+ throw unknownHelpTopic(argv.join(' '))
119
+ }
120
+
121
+ function getNestedHelpText(command: string, argv: string[]): string {
122
+ throw unknownHelpTopic(`${command} ${argv.join(' ')}`)
123
+ }