@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
@@ -0,0 +1,289 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+ import * as process from 'node:process'
4
+ import { createRequire } from 'node:module'
5
+ import * as semver from 'semver'
6
+
7
+ import { MINIMUM_SUPPORTED_NODE_VERSION } from '../bootstrap-project.ts'
8
+ import {
9
+ createDoctorSuite,
10
+ type DoctorFinding,
11
+ type DoctorFixPlan,
12
+ type DoctorSuiteResult,
13
+ } from './types.ts'
14
+
15
+ interface DoctorPackageJson {
16
+ dependencies?: Record<string, string>
17
+ devDependencies?: Record<string, string>
18
+ engines?: Record<string, string>
19
+ name?: string
20
+ }
21
+
22
+ export interface EnvironmentDoctorResult {
23
+ packageJson?: DoctorPackageJson
24
+ packageJsonPath?: string
25
+ projectRoot?: string
26
+ suite: DoctorSuiteResult
27
+ }
28
+
29
+ export async function checkEnvironment(
30
+ cwd: string = process.cwd(),
31
+ ): Promise<EnvironmentDoctorResult> {
32
+ let projectRoot = await findNearestPackageRoot(cwd)
33
+ if (projectRoot == null) {
34
+ return {
35
+ suite: createDoctorSuite('environment', [
36
+ {
37
+ code: 'project-root-not-found',
38
+ message: 'Could not find package.json. Run this command inside a Remix project.',
39
+ severity: 'warn',
40
+ suite: 'environment',
41
+ },
42
+ ]),
43
+ }
44
+ }
45
+
46
+ let packageJsonPath = path.join(projectRoot, 'package.json')
47
+ let packageJson = await readPackageJson(packageJsonPath)
48
+ if ('suite' in packageJson) {
49
+ return {
50
+ projectRoot,
51
+ suite: packageJson.suite,
52
+ }
53
+ }
54
+
55
+ let findings: DoctorFinding[] = []
56
+ let nodeRequirement = packageJson.engines?.node
57
+
58
+ if (nodeRequirement == null) {
59
+ findings.push({
60
+ actualPath: 'package.json',
61
+ code: 'node-engine-missing',
62
+ fixable: true,
63
+ message:
64
+ 'package.json does not declare engines.node. Add one to document the supported Node.js version.',
65
+ severity: 'advice',
66
+ suite: 'environment',
67
+ })
68
+ } else {
69
+ let nodeSupport = satisfiesNodeRange(process.version, nodeRequirement)
70
+
71
+ if (nodeSupport == null) {
72
+ findings.push({
73
+ actualPath: 'package.json',
74
+ code: 'node-engine-unparseable',
75
+ fixable: true,
76
+ message: `Could not evaluate engines.node "${nodeRequirement}" automatically.`,
77
+ severity: 'advice',
78
+ suite: 'environment',
79
+ })
80
+ } else if (!nodeSupport) {
81
+ findings.push({
82
+ actualPath: 'package.json',
83
+ code: 'node-version-unsupported',
84
+ fixable: true,
85
+ message: `Project requires Node.js ${nodeRequirement}, but the current runtime is ${process.version}.`,
86
+ severity: 'warn',
87
+ suite: 'environment',
88
+ })
89
+ }
90
+ }
91
+
92
+ if (!hasRemixDependency(packageJson)) {
93
+ findings.push({
94
+ actualPath: 'package.json',
95
+ code: 'remix-dependency-missing',
96
+ fixable: true,
97
+ message: 'package.json does not declare a remix dependency.',
98
+ severity: 'warn',
99
+ suite: 'environment',
100
+ })
101
+ }
102
+
103
+ if (!(await canResolveRemix(projectRoot))) {
104
+ findings.push({
105
+ actualPath: 'package.json',
106
+ code: 'remix-install-missing',
107
+ message: 'Could not resolve remix from this project. Install dependencies and try again.',
108
+ severity: 'warn',
109
+ suite: 'environment',
110
+ })
111
+ }
112
+
113
+ return {
114
+ packageJson,
115
+ packageJsonPath,
116
+ projectRoot,
117
+ suite: createDoctorSuite('environment', findings),
118
+ }
119
+ }
120
+
121
+ export function getEnvironmentFixPlans(
122
+ result: EnvironmentDoctorResult,
123
+ remixVersion: string | undefined,
124
+ ): DoctorFixPlan[] {
125
+ if (result.projectRoot == null || result.packageJson == null || result.packageJsonPath == null) {
126
+ return []
127
+ }
128
+
129
+ let packageJson = structuredClone(result.packageJson)
130
+ let code = getEnvironmentFixCode(result.suite.findings)
131
+ let changed = false
132
+
133
+ if (
134
+ result.suite.findings.some(
135
+ (finding) =>
136
+ finding.code === 'node-engine-missing' ||
137
+ finding.code === 'node-engine-unparseable' ||
138
+ finding.code === 'node-version-unsupported',
139
+ )
140
+ ) {
141
+ packageJson.engines = {
142
+ ...packageJson.engines,
143
+ node: `>=${MINIMUM_SUPPORTED_NODE_VERSION}`,
144
+ }
145
+ changed = true
146
+ }
147
+
148
+ if (result.suite.findings.some((finding) => finding.code === 'remix-dependency-missing')) {
149
+ packageJson.dependencies = {
150
+ ...packageJson.dependencies,
151
+ remix: remixVersion ?? 'latest',
152
+ }
153
+ changed = true
154
+ }
155
+
156
+ if (!changed || code == null) {
157
+ return []
158
+ }
159
+
160
+ return [
161
+ {
162
+ code,
163
+ contents: `${JSON.stringify(packageJson, null, 2)}\n`,
164
+ kind: 'update-file',
165
+ path: 'package.json',
166
+ suite: 'environment',
167
+ },
168
+ ]
169
+ }
170
+
171
+ async function findNearestPackageRoot(startDir: string): Promise<string | null> {
172
+ let currentDir = path.resolve(startDir)
173
+
174
+ while (true) {
175
+ if (await pathExists(path.join(currentDir, 'package.json'))) {
176
+ return currentDir
177
+ }
178
+
179
+ let parentDir = path.dirname(currentDir)
180
+ if (parentDir === currentDir) {
181
+ return null
182
+ }
183
+
184
+ currentDir = parentDir
185
+ }
186
+ }
187
+
188
+ async function readPackageJson(
189
+ packageJsonPath: string,
190
+ ): Promise<DoctorPackageJson | { suite: DoctorSuiteResult }> {
191
+ let source: string
192
+
193
+ try {
194
+ source = await fs.readFile(packageJsonPath, 'utf8')
195
+ } catch (error) {
196
+ let nodeError = error as NodeJS.ErrnoException
197
+ let detail =
198
+ nodeError.code == null
199
+ ? 'Could not read package.json.'
200
+ : `Could not read package.json (${nodeError.code}).`
201
+
202
+ return {
203
+ suite: createDoctorSuite('environment', [
204
+ {
205
+ actualPath: 'package.json',
206
+ code: 'package-json-read-failed',
207
+ message: detail,
208
+ severity: 'warn',
209
+ suite: 'environment',
210
+ },
211
+ ]),
212
+ }
213
+ }
214
+
215
+ try {
216
+ return JSON.parse(source) as DoctorPackageJson
217
+ } catch {
218
+ return {
219
+ suite: createDoctorSuite('environment', [
220
+ {
221
+ actualPath: 'package.json',
222
+ code: 'package-json-invalid',
223
+ message: 'package.json is not valid JSON.',
224
+ severity: 'warn',
225
+ suite: 'environment',
226
+ },
227
+ ]),
228
+ }
229
+ }
230
+ }
231
+
232
+ function hasRemixDependency(packageJson: DoctorPackageJson): boolean {
233
+ return packageJson.dependencies?.remix != null || packageJson.devDependencies?.remix != null
234
+ }
235
+
236
+ function getEnvironmentFixCode(findings: DoctorFinding[]): DoctorFixPlan['code'] | null {
237
+ let fixableFinding = findings.find(
238
+ (finding) =>
239
+ finding.code === 'node-engine-missing' ||
240
+ finding.code === 'node-engine-unparseable' ||
241
+ finding.code === 'node-version-unsupported' ||
242
+ finding.code === 'remix-dependency-missing',
243
+ )
244
+
245
+ return fixableFinding?.code ?? null
246
+ }
247
+
248
+ async function canResolveRemix(projectRoot: string): Promise<boolean> {
249
+ if (!(await pathExists(path.join(projectRoot, 'node_modules', 'remix', 'package.json')))) {
250
+ return false
251
+ }
252
+
253
+ try {
254
+ let require = createRequire(path.join(projectRoot, 'package.json'))
255
+ require.resolve('remix/package.json')
256
+ return true
257
+ } catch {
258
+ return false
259
+ }
260
+ }
261
+
262
+ function satisfiesNodeRange(version: string, range: string): boolean | null {
263
+ let parsedVersion = semver.valid(version)
264
+ let trimmedRange = range.trim()
265
+ if (parsedVersion == null || trimmedRange === '') {
266
+ return null
267
+ }
268
+
269
+ let parsedRange = semver.validRange(trimmedRange)
270
+ if (parsedRange == null) {
271
+ return null
272
+ }
273
+
274
+ return semver.satisfies(parsedVersion, parsedRange)
275
+ }
276
+
277
+ async function pathExists(filePath: string): Promise<boolean> {
278
+ try {
279
+ await fs.access(filePath)
280
+ return true
281
+ } catch (error) {
282
+ let nodeError = error as NodeJS.ErrnoException
283
+ if (nodeError.code === 'ENOENT') {
284
+ return false
285
+ }
286
+
287
+ throw error
288
+ }
289
+ }
@@ -0,0 +1,84 @@
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+
4
+ import { resolveContainedPath } from '../contained-path.ts'
5
+ import type { DoctorAppliedFix, DoctorFixPlan } from './types.ts'
6
+
7
+ export async function applyDoctorFixPlans(
8
+ appRoot: string,
9
+ fixPlans: DoctorFixPlan[],
10
+ ): Promise<DoctorAppliedFix[]> {
11
+ let appliedFixes: DoctorAppliedFix[] = []
12
+
13
+ for (let fixPlan of dedupeDoctorFixPlans(fixPlans)) {
14
+ let absolutePath = resolveDoctorFixPath(appRoot, fixPlan.path)
15
+
16
+ if (fixPlan.kind === 'create-directory') {
17
+ await fs.mkdir(absolutePath, { recursive: true })
18
+ appliedFixes.push(toAppliedDoctorFix(fixPlan))
19
+ continue
20
+ }
21
+
22
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true })
23
+
24
+ if (fixPlan.kind === 'update-file') {
25
+ await fs.writeFile(absolutePath, fixPlan.contents ?? '', { encoding: 'utf8' })
26
+ appliedFixes.push(toAppliedDoctorFix(fixPlan))
27
+ continue
28
+ }
29
+
30
+ try {
31
+ await fs.writeFile(absolutePath, fixPlan.contents ?? '', { encoding: 'utf8', flag: 'wx' })
32
+ } catch (error) {
33
+ let nodeError = error as NodeJS.ErrnoException
34
+ if (nodeError.code !== 'EEXIST') {
35
+ throw error
36
+ }
37
+
38
+ continue
39
+ }
40
+
41
+ appliedFixes.push(toAppliedDoctorFix(fixPlan))
42
+ }
43
+
44
+ return appliedFixes
45
+ }
46
+
47
+ function dedupeDoctorFixPlans(fixPlans: DoctorFixPlan[]): DoctorFixPlan[] {
48
+ let seen = new Set<string>()
49
+ let uniqueFixPlans: DoctorFixPlan[] = []
50
+
51
+ for (let fixPlan of fixPlans) {
52
+ let key = `${fixPlan.kind}:${fixPlan.path}`
53
+ if (seen.has(key)) {
54
+ continue
55
+ }
56
+
57
+ seen.add(key)
58
+ uniqueFixPlans.push(fixPlan)
59
+ }
60
+
61
+ return uniqueFixPlans
62
+ }
63
+
64
+ function toAppliedDoctorFix(fixPlan: DoctorFixPlan): DoctorAppliedFix {
65
+ return {
66
+ code: fixPlan.code,
67
+ kind: fixPlan.kind,
68
+ path: fixPlan.path,
69
+ routeName: fixPlan.routeName,
70
+ suite: fixPlan.suite,
71
+ }
72
+ }
73
+
74
+ function resolveDoctorFixPath(appRoot: string, fixPath: string): string {
75
+ try {
76
+ return resolveContainedPath(appRoot, fixPath)
77
+ } catch (error) {
78
+ if (error instanceof Error && error.message.includes('escapes the allowed root')) {
79
+ throw new Error(`Doctor fix path resolves outside the app root: ${fixPath}`)
80
+ }
81
+
82
+ throw error
83
+ }
84
+ }