@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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -3
  3. package/bootstrap/AGENTS.md +35 -0
  4. package/bootstrap/README.md +27 -0
  5. package/bootstrap/app/controllers/auth.tsx +21 -0
  6. package/bootstrap/app/controllers/home.tsx +26 -0
  7. package/bootstrap/app/router.ts +10 -0
  8. package/bootstrap/app/routes.ts +6 -0
  9. package/bootstrap/app/ui/document.tsx +21 -0
  10. package/bootstrap/app/ui/layout.tsx +22 -0
  11. package/bootstrap/app/utils/render.tsx +7 -0
  12. package/bootstrap/package.json +22 -0
  13. package/bootstrap/server.ts +37 -0
  14. package/bootstrap/tsconfig.json +19 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/lib/bootstrap-project.d.ts +17 -0
  19. package/dist/lib/bootstrap-project.d.ts.map +1 -0
  20. package/dist/lib/bootstrap-project.js +137 -0
  21. package/dist/lib/cli-context.d.ts +11 -0
  22. package/dist/lib/cli-context.d.ts.map +1 -0
  23. package/dist/lib/cli-context.js +19 -0
  24. package/dist/lib/cli.d.ts +6 -0
  25. package/dist/lib/cli.d.ts.map +1 -0
  26. package/dist/lib/cli.js +87 -0
  27. package/dist/lib/commands/completion.d.ts +3 -0
  28. package/dist/lib/commands/completion.d.ts.map +1 -0
  29. package/dist/lib/commands/completion.js +51 -0
  30. package/dist/lib/commands/doctor.d.ts +4 -0
  31. package/dist/lib/commands/doctor.d.ts.map +1 -0
  32. package/dist/lib/commands/doctor.js +293 -0
  33. package/dist/lib/commands/help.d.ts +4 -0
  34. package/dist/lib/commands/help.d.ts.map +1 -0
  35. package/dist/lib/commands/help.js +124 -0
  36. package/dist/lib/commands/new.d.ts +4 -0
  37. package/dist/lib/commands/new.d.ts.map +1 -0
  38. package/dist/lib/commands/new.js +70 -0
  39. package/dist/lib/commands/routes.d.ts +4 -0
  40. package/dist/lib/commands/routes.d.ts.map +1 -0
  41. package/dist/lib/commands/routes.js +161 -0
  42. package/dist/lib/commands/skills.d.ts +6 -0
  43. package/dist/lib/commands/skills.d.ts.map +1 -0
  44. package/dist/lib/commands/skills.js +222 -0
  45. package/dist/lib/commands/test.d.ts +4 -0
  46. package/dist/lib/commands/test.d.ts.map +1 -0
  47. package/dist/lib/commands/test.js +19 -0
  48. package/dist/lib/commands/version.d.ts +4 -0
  49. package/dist/lib/commands/version.d.ts.map +1 -0
  50. package/dist/lib/commands/version.js +29 -0
  51. package/dist/lib/completion.d.ts +12 -0
  52. package/dist/lib/completion.d.ts.map +1 -0
  53. package/dist/lib/completion.js +408 -0
  54. package/dist/lib/contained-path.d.ts +2 -0
  55. package/dist/lib/contained-path.d.ts.map +1 -0
  56. package/dist/lib/contained-path.js +10 -0
  57. package/dist/lib/controller-files.d.ts +14 -0
  58. package/dist/lib/controller-files.d.ts.map +1 -0
  59. package/dist/lib/controller-files.js +53 -0
  60. package/dist/lib/controller-ownership.d.ts +41 -0
  61. package/dist/lib/controller-ownership.d.ts.map +1 -0
  62. package/dist/lib/controller-ownership.js +224 -0
  63. package/dist/lib/display-path.d.ts +2 -0
  64. package/dist/lib/display-path.d.ts.map +1 -0
  65. package/dist/lib/display-path.js +12 -0
  66. package/dist/lib/doctor/controller-findings.d.ts +4 -0
  67. package/dist/lib/doctor/controller-findings.d.ts.map +1 -0
  68. package/dist/lib/doctor/controller-findings.js +132 -0
  69. package/dist/lib/doctor/controller-fix-plans.d.ts +4 -0
  70. package/dist/lib/doctor/controller-fix-plans.d.ts.map +1 -0
  71. package/dist/lib/doctor/controller-fix-plans.js +130 -0
  72. package/dist/lib/doctor/controller-placeholders.d.ts +4 -0
  73. package/dist/lib/doctor/controller-placeholders.d.ts.map +1 -0
  74. package/dist/lib/doctor/controller-placeholders.js +183 -0
  75. package/dist/lib/doctor/controllers.d.ts +8 -0
  76. package/dist/lib/doctor/controllers.d.ts.map +1 -0
  77. package/dist/lib/doctor/controllers.js +13 -0
  78. package/dist/lib/doctor/environment.d.ts +17 -0
  79. package/dist/lib/doctor/environment.d.ts.map +1 -0
  80. package/dist/lib/doctor/environment.js +226 -0
  81. package/dist/lib/doctor/fixes.d.ts +3 -0
  82. package/dist/lib/doctor/fixes.d.ts.map +1 -0
  83. package/dist/lib/doctor/fixes.js +65 -0
  84. package/dist/lib/doctor/project.d.ts +10 -0
  85. package/dist/lib/doctor/project.d.ts.map +1 -0
  86. package/dist/lib/doctor/project.js +371 -0
  87. package/dist/lib/doctor/types.d.ts +48 -0
  88. package/dist/lib/doctor/types.d.ts.map +1 -0
  89. package/dist/lib/doctor/types.js +15 -0
  90. package/dist/lib/errors.d.ts +173 -0
  91. package/dist/lib/errors.d.ts.map +1 -0
  92. package/dist/lib/errors.js +318 -0
  93. package/dist/lib/help-text.d.ts +13 -0
  94. package/dist/lib/help-text.d.ts.map +1 -0
  95. package/dist/lib/help-text.js +43 -0
  96. package/dist/lib/load-route-map-worker.d.ts +2 -0
  97. package/dist/lib/load-route-map-worker.d.ts.map +1 -0
  98. package/dist/lib/load-route-map-worker.js +92 -0
  99. package/dist/lib/parse-args.d.ts +22 -0
  100. package/dist/lib/parse-args.d.ts.map +1 -0
  101. package/dist/lib/parse-args.js +35 -0
  102. package/dist/lib/remix-version.d.ts +2 -0
  103. package/dist/lib/remix-version.d.ts.map +1 -0
  104. package/dist/lib/remix-version.js +58 -0
  105. package/dist/lib/reporter.d.ts +56 -0
  106. package/dist/lib/reporter.d.ts.map +1 -0
  107. package/dist/lib/reporter.js +357 -0
  108. package/dist/lib/route-map.d.ts +38 -0
  109. package/dist/lib/route-map.d.ts.map +1 -0
  110. package/dist/lib/route-map.js +195 -0
  111. package/dist/lib/skills-cache.d.ts +19 -0
  112. package/dist/lib/skills-cache.d.ts.map +1 -0
  113. package/dist/lib/skills-cache.js +89 -0
  114. package/dist/lib/skills.d.ts +30 -0
  115. package/dist/lib/skills.d.ts.map +1 -0
  116. package/dist/lib/skills.js +441 -0
  117. package/dist/lib/terminal.d.ts +17 -0
  118. package/dist/lib/terminal.d.ts.map +1 -0
  119. package/dist/lib/terminal.js +66 -0
  120. package/package.json +48 -5
  121. package/src/index.ts +1 -0
  122. package/src/lib/bootstrap-project.ts +222 -0
  123. package/src/lib/cli-context.ts +38 -0
  124. package/src/lib/cli.ts +120 -0
  125. package/src/lib/commands/completion.ts +74 -0
  126. package/src/lib/commands/doctor.ts +393 -0
  127. package/src/lib/commands/help.ts +157 -0
  128. package/src/lib/commands/new.ts +104 -0
  129. package/src/lib/commands/routes.ts +251 -0
  130. package/src/lib/commands/skills.ts +306 -0
  131. package/src/lib/commands/test.ts +25 -0
  132. package/src/lib/commands/version.ts +39 -0
  133. package/src/lib/completion.ts +561 -0
  134. package/src/lib/contained-path.ts +13 -0
  135. package/src/lib/controller-files.ts +79 -0
  136. package/src/lib/controller-ownership.ts +397 -0
  137. package/src/lib/display-path.ts +16 -0
  138. package/src/lib/doctor/controller-findings.ts +157 -0
  139. package/src/lib/doctor/controller-fix-plans.ts +184 -0
  140. package/src/lib/doctor/controller-placeholders.ts +242 -0
  141. package/src/lib/doctor/controllers.ts +23 -0
  142. package/src/lib/doctor/environment.ts +289 -0
  143. package/src/lib/doctor/fixes.ts +84 -0
  144. package/src/lib/doctor/project.ts +435 -0
  145. package/src/lib/doctor/types.ts +95 -0
  146. package/src/lib/errors.ts +379 -0
  147. package/src/lib/help-text.ts +88 -0
  148. package/src/lib/load-route-map-worker.ts +110 -0
  149. package/src/lib/parse-args.ts +82 -0
  150. package/src/lib/remix-version.ts +80 -0
  151. package/src/lib/reporter.ts +535 -0
  152. package/src/lib/route-map.ts +303 -0
  153. package/src/lib/skills-cache.ts +140 -0
  154. package/src/lib/skills.ts +706 -0
  155. package/src/lib/terminal.ts +86 -0
@@ -0,0 +1,561 @@
1
+ export interface CompletionResult {
2
+ mode: 'files' | 'none' | 'values'
3
+ values?: string[]
4
+ }
5
+
6
+ const COMPLETION_SHELLS = ['bash', 'zsh'] as const
7
+ const HELP_COMMANDS = [
8
+ 'completion',
9
+ 'doctor',
10
+ 'help',
11
+ 'new',
12
+ 'routes',
13
+ 'skills',
14
+ 'test',
15
+ 'version',
16
+ ] as const
17
+ const ROOT_COMMANDS = [
18
+ 'completion',
19
+ 'doctor',
20
+ 'help',
21
+ 'new',
22
+ 'routes',
23
+ 'skills',
24
+ 'test',
25
+ 'version',
26
+ ] as const
27
+ const SKILLS_COMMANDS = ['install', 'list'] as const
28
+
29
+ export type CompletionShell = (typeof COMPLETION_SHELLS)[number]
30
+
31
+ export function isCompletionShell(value: string): value is CompletionShell {
32
+ return COMPLETION_SHELLS.includes(value as CompletionShell)
33
+ }
34
+
35
+ export function getCompletionResult(words: string[], currentIndex: number): CompletionResult {
36
+ let resolvedIndex = Math.max(0, currentIndex)
37
+ let currentWord = words[resolvedIndex] ?? ''
38
+ let tokens = getTokensBeforeCursor(words, resolvedIndex)
39
+
40
+ return completeTopLevel(tokens, currentWord)
41
+ }
42
+
43
+ export function renderCompletionResult(result: CompletionResult): string {
44
+ let lines = [`mode:${result.mode}`]
45
+
46
+ if (result.mode === 'values') {
47
+ lines.push(...(result.values ?? []))
48
+ }
49
+
50
+ return `${lines.join('\n')}\n`
51
+ }
52
+
53
+ export function getCompletionScript(): string {
54
+ return `###-begin-remix-completion-###
55
+ #
56
+ # Remix command completion script
57
+ #
58
+ # Installation:
59
+ # remix completion bash >> ~/.bashrc
60
+ # remix completion zsh >> ~/.zshrc
61
+ #
62
+
63
+ if type complete &>/dev/null; then
64
+ _remix_completion() {
65
+ local words cword current output mode si
66
+ local lines
67
+
68
+ if type _get_comp_words_by_ref &>/dev/null; then
69
+ _get_comp_words_by_ref -w words -i cword
70
+ else
71
+ cword="$COMP_CWORD"
72
+ words=("\${COMP_WORDS[@]}")
73
+ fi
74
+
75
+ current="\${words[cword]}"
76
+ output=$(remix completion -- "$cword" "\${words[@]}" 2>/dev/null) || return 0
77
+ si="$IFS"
78
+ IFS=$'\\n' lines=($output)
79
+ IFS="$si"
80
+ mode="\${lines[0]}"
81
+
82
+ if [[ "$mode" == "mode:values" ]]; then
83
+ COMPREPLY=("\${lines[@]:1}")
84
+ return 0
85
+ fi
86
+
87
+ if [[ "$mode" == "mode:files" ]]; then
88
+ COMPREPLY=($(compgen -f -- "$current"))
89
+ return 0
90
+ fi
91
+
92
+ COMPREPLY=()
93
+ }
94
+
95
+ complete -o default -F _remix_completion remix
96
+ elif type compdef &>/dev/null; then
97
+ _remix_completion() {
98
+ local output mode
99
+ local -a lines values
100
+
101
+ output=$(remix completion -- "$((CURRENT - 1))" "\${words[@]}" 2>/dev/null) || return 0
102
+ lines=("\${(@f)output}")
103
+ mode="\${lines[1]}"
104
+
105
+ if [[ "$mode" == "mode:values" ]]; then
106
+ values=("\${(@)lines[2,-1]}")
107
+ if (( \${#values[@]} > 0 )); then
108
+ compadd -- "\${values[@]}"
109
+ fi
110
+ return 0
111
+ fi
112
+
113
+ if [[ "$mode" == "mode:files" ]]; then
114
+ if autoload -U +X _files 2>/dev/null; then
115
+ _files
116
+ else
117
+ compadd -f
118
+ fi
119
+ return 0
120
+ fi
121
+
122
+ return 0
123
+ }
124
+
125
+ compdef _remix_completion remix
126
+ fi
127
+ ###-end-remix-completion-###
128
+ `
129
+ }
130
+
131
+ function completeTopLevel(tokens: string[], currentWord: string): CompletionResult {
132
+ let usedFlags = new Set<string>()
133
+ let index = 0
134
+
135
+ while (index < tokens.length) {
136
+ let token = tokens[index]
137
+
138
+ if (token === '--no-color') {
139
+ usedFlags.add('--no-color')
140
+ index++
141
+ continue
142
+ }
143
+
144
+ if (token === '-h' || token === '--help') {
145
+ usedFlags.add('-h')
146
+ return completeValues([], currentWord)
147
+ }
148
+
149
+ if (token === '-v' || token === '--version') {
150
+ usedFlags.add('-v')
151
+ return completeValues([], currentWord)
152
+ }
153
+
154
+ return completeCommand(token, tokens.slice(index + 1), currentWord, usedFlags)
155
+ }
156
+
157
+ return completeValues(getTopLevelSuggestions(currentWord, usedFlags), currentWord)
158
+ }
159
+
160
+ function completeCommand(
161
+ command: string,
162
+ tokens: string[],
163
+ currentWord: string,
164
+ usedGlobalFlags: Set<string>,
165
+ ): CompletionResult {
166
+ if (command === 'help') {
167
+ return completeHelp(tokens, currentWord, usedGlobalFlags)
168
+ }
169
+
170
+ if (command === 'new') {
171
+ return completeNew(tokens, currentWord, usedGlobalFlags)
172
+ }
173
+
174
+ if (command === 'doctor') {
175
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [
176
+ '--fix',
177
+ '--json',
178
+ '--strict',
179
+ ])
180
+ }
181
+
182
+ if (command === 'routes') {
183
+ return completeRoutes(tokens, currentWord, usedGlobalFlags)
184
+ }
185
+
186
+ if (command === 'skills') {
187
+ return completeSkills(tokens, currentWord, usedGlobalFlags)
188
+ }
189
+
190
+ if (command === 'version') {
191
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [])
192
+ }
193
+
194
+ if (command === 'test') {
195
+ return completeSimpleFlags(tokens, currentWord, usedGlobalFlags, [
196
+ '--coverage',
197
+ '--watch',
198
+ '--help',
199
+ ])
200
+ }
201
+
202
+ if (command === 'completion') {
203
+ return completeCompletionCommand(tokens, currentWord, usedGlobalFlags)
204
+ }
205
+
206
+ return completeValues([], currentWord)
207
+ }
208
+
209
+ function completeHelp(
210
+ tokens: string[],
211
+ currentWord: string,
212
+ usedGlobalFlags: Set<string>,
213
+ ): CompletionResult {
214
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
215
+ if (filteredTokens == null) {
216
+ return completeValues([], currentWord)
217
+ }
218
+
219
+ if (filteredTokens.length === 0) {
220
+ return completeValues(withHelpFlags([...HELP_COMMANDS], usedGlobalFlags), currentWord)
221
+ }
222
+
223
+ let [topic, ...rest] = filteredTokens
224
+ if (rest.length === 0) {
225
+ if (topic === 'skills') {
226
+ return completeValues(withHelpFlags([...SKILLS_COMMANDS], usedGlobalFlags), currentWord)
227
+ }
228
+
229
+ return completeValues([], currentWord)
230
+ }
231
+
232
+ return completeValues([], currentWord)
233
+ }
234
+
235
+ function completeNew(
236
+ tokens: string[],
237
+ currentWord: string,
238
+ usedGlobalFlags: Set<string>,
239
+ ): CompletionResult {
240
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
241
+ if (filteredTokens == null) {
242
+ return completeValues([], currentWord)
243
+ }
244
+
245
+ let hasAppName = false
246
+ let hasForce = false
247
+ let hasTargetDir = false
248
+ let expectsAppName = false
249
+
250
+ for (let token of filteredTokens) {
251
+ if (expectsAppName) {
252
+ expectsAppName = false
253
+ continue
254
+ }
255
+
256
+ if (token === '--app-name') {
257
+ hasAppName = true
258
+ expectsAppName = true
259
+ continue
260
+ }
261
+
262
+ if (token === '--force') {
263
+ hasForce = true
264
+ continue
265
+ }
266
+
267
+ if (token.startsWith('-')) {
268
+ return completeValues([], currentWord)
269
+ }
270
+
271
+ if (!hasTargetDir) {
272
+ hasTargetDir = true
273
+ continue
274
+ }
275
+
276
+ return completeValues([], currentWord)
277
+ }
278
+
279
+ if (expectsAppName) {
280
+ return { mode: 'none' }
281
+ }
282
+
283
+ let flags = withHelpFlags(
284
+ [...(!hasAppName ? ['--app-name'] : []), ...(!hasForce ? ['--force'] : [])],
285
+ usedGlobalFlags,
286
+ )
287
+
288
+ if (!hasTargetDir && !currentWord.startsWith('-')) {
289
+ return { mode: 'files' }
290
+ }
291
+
292
+ return completeValues(flags, currentWord)
293
+ }
294
+
295
+ function completeRoutes(
296
+ tokens: string[],
297
+ currentWord: string,
298
+ usedGlobalFlags: Set<string>,
299
+ ): CompletionResult {
300
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
301
+ if (filteredTokens == null) {
302
+ return completeValues([], currentWord)
303
+ }
304
+
305
+ let hasJson = false
306
+ let hasNoHeaders = false
307
+ let hasTable = false
308
+ let hasVerbose = false
309
+
310
+ for (let token of filteredTokens) {
311
+ if (token === '--json') {
312
+ hasJson = true
313
+ continue
314
+ }
315
+
316
+ if (token === '--table') {
317
+ hasTable = true
318
+ continue
319
+ }
320
+
321
+ if (token === '--no-headers') {
322
+ hasNoHeaders = true
323
+ continue
324
+ }
325
+
326
+ if (token === '--verbose') {
327
+ hasVerbose = true
328
+ continue
329
+ }
330
+
331
+ return completeValues([], currentWord)
332
+ }
333
+
334
+ let flags = withHelpFlags(
335
+ [
336
+ ...(!hasJson && !hasTable && !hasVerbose ? ['--json'] : []),
337
+ ...(!hasTable && !hasJson ? ['--table'] : []),
338
+ ...(!hasNoHeaders && hasTable ? ['--no-headers'] : []),
339
+ ...(!hasVerbose && !hasJson ? ['--verbose'] : []),
340
+ ],
341
+ usedGlobalFlags,
342
+ )
343
+
344
+ return completeValues(flags, currentWord)
345
+ }
346
+
347
+ function completeSkills(
348
+ tokens: string[],
349
+ currentWord: string,
350
+ usedGlobalFlags: Set<string>,
351
+ ): CompletionResult {
352
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
353
+ if (filteredTokens == null) {
354
+ return completeValues([], currentWord)
355
+ }
356
+
357
+ if (filteredTokens.length === 0) {
358
+ return completeValues(withHelpFlags([...SKILLS_COMMANDS], usedGlobalFlags), currentWord)
359
+ }
360
+
361
+ let [subcommand, ...rest] = filteredTokens
362
+ if (subcommand === 'install') {
363
+ return completeSkillsInstall(rest, currentWord, usedGlobalFlags)
364
+ }
365
+
366
+ if (subcommand === 'list') {
367
+ return completeSkillsList(rest, currentWord, usedGlobalFlags)
368
+ }
369
+
370
+ return completeValues([], currentWord)
371
+ }
372
+
373
+ function completeSkillsInstall(
374
+ tokens: string[],
375
+ currentWord: string,
376
+ usedGlobalFlags: Set<string>,
377
+ ): CompletionResult {
378
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
379
+ if (filteredTokens == null) {
380
+ return completeValues([], currentWord)
381
+ }
382
+
383
+ let hasDir = false
384
+ let expectsDir = false
385
+
386
+ for (let token of filteredTokens) {
387
+ if (expectsDir) {
388
+ expectsDir = false
389
+ continue
390
+ }
391
+
392
+ if (token === '--dir') {
393
+ hasDir = true
394
+ expectsDir = true
395
+ continue
396
+ }
397
+
398
+ return completeValues([], currentWord)
399
+ }
400
+
401
+ if (expectsDir) {
402
+ return { mode: 'files' }
403
+ }
404
+
405
+ return completeValues(withHelpFlags(!hasDir ? ['--dir'] : [], usedGlobalFlags), currentWord)
406
+ }
407
+
408
+ function completeSkillsList(
409
+ tokens: string[],
410
+ currentWord: string,
411
+ usedGlobalFlags: Set<string>,
412
+ ): CompletionResult {
413
+ return completeSkillsDirectoryCommand(tokens, currentWord, usedGlobalFlags)
414
+ }
415
+
416
+ function completeSkillsDirectoryCommand(
417
+ tokens: string[],
418
+ currentWord: string,
419
+ usedGlobalFlags: Set<string>,
420
+ ): CompletionResult {
421
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
422
+ if (filteredTokens == null) {
423
+ return completeValues([], currentWord)
424
+ }
425
+
426
+ let hasDir = false
427
+ let hasJson = false
428
+ let expectsDir = false
429
+
430
+ for (let token of filteredTokens) {
431
+ if (expectsDir) {
432
+ expectsDir = false
433
+ continue
434
+ }
435
+
436
+ if (token === '--dir') {
437
+ hasDir = true
438
+ expectsDir = true
439
+ continue
440
+ }
441
+
442
+ if (token === '--json') {
443
+ hasJson = true
444
+ continue
445
+ }
446
+
447
+ return completeValues([], currentWord)
448
+ }
449
+
450
+ if (expectsDir) {
451
+ return { mode: 'files' }
452
+ }
453
+
454
+ return completeValues(
455
+ withHelpFlags(
456
+ [...(!hasDir ? ['--dir'] : []), ...(!hasJson ? ['--json'] : [])],
457
+ usedGlobalFlags,
458
+ ),
459
+ currentWord,
460
+ )
461
+ }
462
+
463
+ function completeCompletionCommand(
464
+ tokens: string[],
465
+ currentWord: string,
466
+ usedGlobalFlags: Set<string>,
467
+ ): CompletionResult {
468
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
469
+ if (filteredTokens == null) {
470
+ return completeValues([], currentWord)
471
+ }
472
+
473
+ if (filteredTokens.length === 0) {
474
+ return completeValues(withHelpFlags([...COMPLETION_SHELLS], usedGlobalFlags), currentWord)
475
+ }
476
+
477
+ return completeValues([], currentWord)
478
+ }
479
+
480
+ function completeSimpleFlags(
481
+ tokens: string[],
482
+ currentWord: string,
483
+ usedGlobalFlags: Set<string>,
484
+ commandFlags: string[],
485
+ ): CompletionResult {
486
+ let filteredTokens = filterGlobalCommandTokens(tokens, usedGlobalFlags)
487
+ if (filteredTokens == null) {
488
+ return completeValues([], currentWord)
489
+ }
490
+
491
+ let usedFlags = new Set<string>()
492
+ for (let token of filteredTokens) {
493
+ if (!commandFlags.includes(token)) {
494
+ return completeValues([], currentWord)
495
+ }
496
+
497
+ usedFlags.add(token)
498
+ }
499
+
500
+ let availableFlags = commandFlags.filter((flag) => !usedFlags.has(flag))
501
+ return completeValues(withHelpFlags(availableFlags, usedGlobalFlags), currentWord)
502
+ }
503
+
504
+ function getTopLevelSuggestions(currentWord: string, usedFlags: Set<string>): string[] {
505
+ let flags = [
506
+ ...(!usedFlags.has('-h') ? ['-h', '--help'] : []),
507
+ ...(!usedFlags.has('--no-color') ? ['--no-color'] : []),
508
+ ...(!usedFlags.has('-v') ? ['-v', '--version'] : []),
509
+ ]
510
+
511
+ if (currentWord.startsWith('-')) {
512
+ return flags
513
+ }
514
+
515
+ return [...ROOT_COMMANDS, ...flags]
516
+ }
517
+
518
+ function withHelpFlags(values: string[], usedGlobalFlags: Set<string>): string[] {
519
+ return [
520
+ ...values,
521
+ ...(!usedGlobalFlags.has('-h') ? ['-h', '--help'] : []),
522
+ ...(!usedGlobalFlags.has('--no-color') ? ['--no-color'] : []),
523
+ ]
524
+ }
525
+
526
+ function filterGlobalCommandTokens(
527
+ tokens: string[],
528
+ usedGlobalFlags: Set<string>,
529
+ ): string[] | null {
530
+ let filtered: string[] = []
531
+
532
+ for (let token of tokens) {
533
+ if (token === '--no-color') {
534
+ usedGlobalFlags.add('--no-color')
535
+ continue
536
+ }
537
+
538
+ if (token === '-h' || token === '--help') {
539
+ usedGlobalFlags.add('-h')
540
+ return null
541
+ }
542
+
543
+ filtered.push(token)
544
+ }
545
+
546
+ return filtered
547
+ }
548
+
549
+ function completeValues(values: string[], currentWord: string): CompletionResult {
550
+ return {
551
+ mode: 'values',
552
+ values: values.filter((value) => value.startsWith(currentWord)),
553
+ }
554
+ }
555
+
556
+ function getTokensBeforeCursor(words: string[], currentIndex: number): string[] {
557
+ let startIndex = words[0] === 'remix' ? 1 : 0
558
+ let endIndex = Math.max(startIndex, Math.min(currentIndex, words.length))
559
+
560
+ return words.slice(startIndex, endIndex)
561
+ }
@@ -0,0 +1,13 @@
1
+ import * as path from 'node:path'
2
+
3
+ export function resolveContainedPath(rootDir: string, relativePath: string): string {
4
+ let resolvedRootDir = path.resolve(rootDir)
5
+ let resolvedPath = path.resolve(resolvedRootDir, relativePath)
6
+ let pathFromRoot = path.relative(resolvedRootDir, resolvedPath)
7
+
8
+ if (pathFromRoot === '' || (!pathFromRoot.startsWith('..') && !path.isAbsolute(pathFromRoot))) {
9
+ return resolvedPath
10
+ }
11
+
12
+ throw new Error(`Resolved path escapes the allowed root: ${relativePath}`)
13
+ }
@@ -0,0 +1,79 @@
1
+ import * as path from 'node:path'
2
+
3
+ const OWNER_FILE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'] as const
4
+ export type OwnerFileExtension = (typeof OWNER_FILE_EXTENSIONS)[number]
5
+
6
+ export function getActionOwnerCandidates(segments: string[]): string[] {
7
+ return OWNER_FILE_EXTENSIONS.map((extension) =>
8
+ normalizeRelativePath(path.join('app', 'controllers', `${path.join(...segments)}${extension}`)),
9
+ )
10
+ }
11
+
12
+ export function getControllerOwnerCandidates(segments: string[]): string[] {
13
+ return OWNER_FILE_EXTENSIONS.map((extension) =>
14
+ normalizeRelativePath(path.join('app', 'controllers', ...segments, `controller${extension}`)),
15
+ )
16
+ }
17
+
18
+ export function getRouteSubtreePath(segments: string[]): string {
19
+ return normalizeRelativePath(path.join('app', 'controllers', ...segments))
20
+ }
21
+
22
+ export function getPreferredOwnerDisplayPath(candidates: string[]): string {
23
+ let tsxCandidate = candidates.find((candidate) => candidate.endsWith('.tsx'))
24
+ return tsxCandidate ?? candidates[0] ?? ''
25
+ }
26
+
27
+ export function getOwnerCandidateForExtension(
28
+ candidates: string[],
29
+ extension: OwnerFileExtension,
30
+ ): string | null {
31
+ return candidates.find((candidate) => candidate.endsWith(extension)) ?? null
32
+ }
33
+
34
+ export function getOwnerFileExtension(filePath: string): OwnerFileExtension | null {
35
+ return OWNER_FILE_EXTENSIONS.find((extension) => filePath.endsWith(extension)) ?? null
36
+ }
37
+
38
+ export function toDiskSegment(segment: string): string {
39
+ let diskSegment = segment
40
+ .replace(/[\\/]+/g, '-')
41
+ .replace(/\.+/g, '-')
42
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
43
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
44
+ .toLowerCase()
45
+ .replace(/[^a-z0-9]+/g, '-')
46
+ .replace(/-+/g, '-')
47
+ .replace(/^-+|-+$/g, '')
48
+
49
+ return diskSegment.length === 0 ? 'route' : diskSegment
50
+ }
51
+
52
+ export function isControllerEntryFileName(fileName: string): boolean {
53
+ return OWNER_FILE_EXTENSIONS.some((extension) => fileName === `controller${extension}`)
54
+ }
55
+
56
+ export function getOwnerModuleBaseName(fileName: string): string | null {
57
+ let extension = OWNER_FILE_EXTENSIONS.find((ownerExtension) => fileName.endsWith(ownerExtension))
58
+
59
+ if (extension == null) {
60
+ return null
61
+ }
62
+
63
+ return fileName.slice(0, -extension.length)
64
+ }
65
+
66
+ export function isActionFileName(fileName: string): boolean {
67
+ let baseName = getOwnerModuleBaseName(fileName)
68
+
69
+ return (
70
+ baseName != null &&
71
+ baseName !== 'controller' &&
72
+ !baseName.endsWith('.test') &&
73
+ !baseName.endsWith('.spec')
74
+ )
75
+ }
76
+
77
+ function normalizeRelativePath(filePath: string): string {
78
+ return filePath.split(path.sep).join('/')
79
+ }