@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,379 @@
|
|
|
1
|
+
export interface CliErrorDefinition {
|
|
2
|
+
code: string
|
|
3
|
+
docsUrl?: string
|
|
4
|
+
fix?: string
|
|
5
|
+
title: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type CliErrorContext = Record<string, boolean | number | string | null | undefined>
|
|
9
|
+
|
|
10
|
+
export interface CliErrorOptions {
|
|
11
|
+
code: string
|
|
12
|
+
context?: CliErrorContext
|
|
13
|
+
docsUrl?: string
|
|
14
|
+
fix?: string
|
|
15
|
+
message: string
|
|
16
|
+
showHelp?: boolean
|
|
17
|
+
title: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CreateErrorOptions {
|
|
21
|
+
context?: CliErrorContext
|
|
22
|
+
message: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const CLI_ERROR_DEFINITIONS = {
|
|
26
|
+
appNameUnavailable: {
|
|
27
|
+
code: 'RMX_APP_NAME_UNAVAILABLE',
|
|
28
|
+
title: 'Could not determine an app name',
|
|
29
|
+
fix: 'Pass --app-name or choose a target directory name that can become an app name.',
|
|
30
|
+
},
|
|
31
|
+
remixVersionUnavailable: {
|
|
32
|
+
code: 'RMX_REMIX_VERSION_UNAVAILABLE',
|
|
33
|
+
title: 'Could not determine the Remix version',
|
|
34
|
+
},
|
|
35
|
+
invalidCompletionRequest: {
|
|
36
|
+
code: 'RMX_INVALID_COMPLETION_REQUEST',
|
|
37
|
+
title: 'Invalid completion request',
|
|
38
|
+
},
|
|
39
|
+
fetchUnavailable: {
|
|
40
|
+
code: 'RMX_FETCH_UNAVAILABLE',
|
|
41
|
+
title: 'This runtime does not provide fetch()',
|
|
42
|
+
fix: 'Run the Remix CLI in a runtime that provides fetch().',
|
|
43
|
+
},
|
|
44
|
+
internalError: {
|
|
45
|
+
code: 'RMX_INTERNAL_ERROR',
|
|
46
|
+
title: 'Unexpected Remix CLI error',
|
|
47
|
+
},
|
|
48
|
+
invalidFlagCombination: {
|
|
49
|
+
code: 'RMX_INVALID_FLAG_COMBINATION',
|
|
50
|
+
title: 'Invalid flag combination',
|
|
51
|
+
fix: 'Remove one of the conflicting flags and try again.',
|
|
52
|
+
},
|
|
53
|
+
invalidPackageName: {
|
|
54
|
+
code: 'RMX_INVALID_PACKAGE_NAME',
|
|
55
|
+
title: 'Could not derive a valid package name',
|
|
56
|
+
fix: 'Choose an app name that can be normalized into a valid npm package name.',
|
|
57
|
+
},
|
|
58
|
+
missingOptionValue: {
|
|
59
|
+
code: 'RMX_MISSING_OPTION_VALUE',
|
|
60
|
+
title: 'Missing option value',
|
|
61
|
+
fix: 'Pass a value immediately after the option.',
|
|
62
|
+
},
|
|
63
|
+
missingTargetDirectory: {
|
|
64
|
+
code: 'RMX_MISSING_TARGET_DIRECTORY',
|
|
65
|
+
title: 'Missing target directory',
|
|
66
|
+
fix: 'Pass a target directory, for example `remix new my-remix-app`.',
|
|
67
|
+
},
|
|
68
|
+
projectRootNotFound: {
|
|
69
|
+
code: 'RMX_PROJECT_ROOT_NOT_FOUND',
|
|
70
|
+
title: 'Could not find a project root',
|
|
71
|
+
fix: 'Run this command inside a Remix project.',
|
|
72
|
+
},
|
|
73
|
+
remoteSkillDataMissing: {
|
|
74
|
+
code: 'RMX_REMOTE_SKILL_DATA_MISSING',
|
|
75
|
+
title: 'Could not find remote Remix skill data',
|
|
76
|
+
},
|
|
77
|
+
routeMapLoaderFailed: {
|
|
78
|
+
code: 'RMX_ROUTE_MAP_LOADER_FAILED',
|
|
79
|
+
title: 'Route-map loader failed',
|
|
80
|
+
fix: 'Check app/routes.ts and make sure it exports a named `routes` value with a valid route map.',
|
|
81
|
+
},
|
|
82
|
+
routeMapLoaderInvalidJson: {
|
|
83
|
+
code: 'RMX_ROUTE_MAP_LOADER_INVALID_JSON',
|
|
84
|
+
title: 'Route-map loader returned invalid JSON',
|
|
85
|
+
fix: 'Check app/routes.ts and make sure route-map loading does not write extra output.',
|
|
86
|
+
},
|
|
87
|
+
routeMapLoaderSignal: {
|
|
88
|
+
code: 'RMX_ROUTE_MAP_LOADER_SIGNAL',
|
|
89
|
+
title: 'Route-map loader exited from a signal',
|
|
90
|
+
fix: 'Check app/routes.ts for side effects or runtime issues and try again.',
|
|
91
|
+
},
|
|
92
|
+
routeOwnerPlanUnresolved: {
|
|
93
|
+
code: 'RMX_ROUTE_OWNER_PLAN_UNRESOLVED',
|
|
94
|
+
title: 'Could not resolve a route owner plan',
|
|
95
|
+
},
|
|
96
|
+
routesFileNotFound: {
|
|
97
|
+
code: 'RMX_ROUTES_FILE_NOT_FOUND',
|
|
98
|
+
title: 'Could not find app/routes.ts',
|
|
99
|
+
fix: 'Run this command inside a Remix app that has app/routes.ts.',
|
|
100
|
+
},
|
|
101
|
+
targetDirectoryNotEmpty: {
|
|
102
|
+
code: 'RMX_TARGET_DIRECTORY_NOT_EMPTY',
|
|
103
|
+
title: 'Target directory is not empty',
|
|
104
|
+
fix: 'Re-run with --force to continue.',
|
|
105
|
+
},
|
|
106
|
+
targetPathNotDirectory: {
|
|
107
|
+
code: 'RMX_TARGET_PATH_NOT_DIRECTORY',
|
|
108
|
+
title: 'Target path is not a directory',
|
|
109
|
+
fix: 'Choose a directory path for the new app target.',
|
|
110
|
+
},
|
|
111
|
+
unexpectedExtraArgument: {
|
|
112
|
+
code: 'RMX_UNEXPECTED_ARGUMENT',
|
|
113
|
+
title: 'Unexpected extra argument',
|
|
114
|
+
fix: 'Remove the extra argument or run the command with --help.',
|
|
115
|
+
},
|
|
116
|
+
unknownArgument: {
|
|
117
|
+
code: 'RMX_UNKNOWN_ARGUMENT',
|
|
118
|
+
title: 'Unknown argument',
|
|
119
|
+
fix: 'Run the command with --help to see supported arguments.',
|
|
120
|
+
},
|
|
121
|
+
unknownCommand: {
|
|
122
|
+
code: 'RMX_UNKNOWN_COMMAND',
|
|
123
|
+
title: 'Unknown command',
|
|
124
|
+
fix: 'Run `remix help` to see available commands.',
|
|
125
|
+
},
|
|
126
|
+
unknownCompletionShell: {
|
|
127
|
+
code: 'RMX_UNKNOWN_COMPLETION_SHELL',
|
|
128
|
+
title: 'Unknown completion shell',
|
|
129
|
+
fix: 'Run `remix completion bash` or `remix completion zsh`.',
|
|
130
|
+
},
|
|
131
|
+
unknownHelpTopic: {
|
|
132
|
+
code: 'RMX_UNKNOWN_HELP_TOPIC',
|
|
133
|
+
title: 'Unknown help topic',
|
|
134
|
+
fix: 'Run `remix help` to see available commands and help topics.',
|
|
135
|
+
},
|
|
136
|
+
unknownSkillsCommand: {
|
|
137
|
+
code: 'RMX_UNKNOWN_SKILLS_COMMAND',
|
|
138
|
+
title: 'Unknown skills command',
|
|
139
|
+
fix: 'Run `remix skills --help` to see available skills commands.',
|
|
140
|
+
},
|
|
141
|
+
} satisfies Record<string, CliErrorDefinition>
|
|
142
|
+
|
|
143
|
+
export class CliError extends Error {
|
|
144
|
+
code: string
|
|
145
|
+
context?: CliErrorContext
|
|
146
|
+
docsUrl?: string
|
|
147
|
+
fix?: string
|
|
148
|
+
showHelp: boolean
|
|
149
|
+
title: string
|
|
150
|
+
|
|
151
|
+
constructor(options: CliErrorOptions) {
|
|
152
|
+
super(options.message)
|
|
153
|
+
this.code = options.code
|
|
154
|
+
this.context = options.context
|
|
155
|
+
this.docsUrl = options.docsUrl
|
|
156
|
+
this.fix = options.fix
|
|
157
|
+
this.name = 'CliError'
|
|
158
|
+
this.showHelp = options.showHelp ?? false
|
|
159
|
+
this.title = options.title
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class UsageError extends CliError {
|
|
164
|
+
constructor(options: CliErrorOptions) {
|
|
165
|
+
super({ ...options, showHelp: options.showHelp ?? true })
|
|
166
|
+
this.name = 'UsageError'
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function appNameUnavailable(targetDir?: string): UsageError {
|
|
171
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.appNameUnavailable, {
|
|
172
|
+
context: targetDir == null ? undefined : { targetDir },
|
|
173
|
+
message: 'Could not determine an app name from the target directory.',
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function remixVersionUnavailable(): CliError {
|
|
178
|
+
return createCliError(CLI_ERROR_DEFINITIONS.remixVersionUnavailable, {
|
|
179
|
+
message: 'Could not determine the current Remix version.',
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function invalidCompletionRequest(): CliError {
|
|
184
|
+
return createCliError(CLI_ERROR_DEFINITIONS.invalidCompletionRequest, {
|
|
185
|
+
message: 'Invalid completion request.',
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function fetchUnavailable(): CliError {
|
|
190
|
+
return createCliError(CLI_ERROR_DEFINITIONS.fetchUnavailable, {
|
|
191
|
+
message: 'This runtime does not provide fetch().',
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function invalidFlagCombination(details: string): UsageError {
|
|
196
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.invalidFlagCombination, {
|
|
197
|
+
context: { details },
|
|
198
|
+
message: details,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function invalidPackageName(input: string): UsageError {
|
|
203
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.invalidPackageName, {
|
|
204
|
+
context: { input },
|
|
205
|
+
message: `Could not derive a valid package name from "${input}".`,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function missingOptionValue(option: string): UsageError {
|
|
210
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.missingOptionValue, {
|
|
211
|
+
context: { option },
|
|
212
|
+
message: `${option} requires a value.`,
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function missingTargetDirectory(): UsageError {
|
|
217
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.missingTargetDirectory, {
|
|
218
|
+
message: 'A target directory is required.',
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function projectRootNotFound(startDir?: string): CliError {
|
|
223
|
+
return createCliError(CLI_ERROR_DEFINITIONS.projectRootNotFound, {
|
|
224
|
+
context: startDir == null ? undefined : { startDir },
|
|
225
|
+
message: 'Could not find a project root. Run this command inside a Remix project.',
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function remoteSkillDataMissing(name: string): CliError {
|
|
230
|
+
return createCliError(CLI_ERROR_DEFINITIONS.remoteSkillDataMissing, {
|
|
231
|
+
context: { name },
|
|
232
|
+
message: `Could not find remote data for Remix skill: ${name}`,
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function routeMapLoaderFailed(details: string): CliError {
|
|
237
|
+
return createCliError(CLI_ERROR_DEFINITIONS.routeMapLoaderFailed, {
|
|
238
|
+
context: { details },
|
|
239
|
+
message: details,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function routeMapLoaderInvalidJson(): CliError {
|
|
244
|
+
return createCliError(CLI_ERROR_DEFINITIONS.routeMapLoaderInvalidJson, {
|
|
245
|
+
message: 'Route-map loader returned invalid JSON.',
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function routeMapLoaderSignal(signal: NodeJS.Signals): CliError {
|
|
250
|
+
return createCliError(CLI_ERROR_DEFINITIONS.routeMapLoaderSignal, {
|
|
251
|
+
context: { signal },
|
|
252
|
+
message: `Route-map loader exited from signal ${signal}.`,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function routeOwnerPlanUnresolved(routeName: string): CliError {
|
|
257
|
+
return createCliError(CLI_ERROR_DEFINITIONS.routeOwnerPlanUnresolved, {
|
|
258
|
+
context: { routeName },
|
|
259
|
+
message: `Could not resolve owner plan for "${routeName}".`,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function routesFileNotFound(startDir?: string): CliError {
|
|
264
|
+
return createCliError(CLI_ERROR_DEFINITIONS.routesFileNotFound, {
|
|
265
|
+
context: startDir == null ? undefined : { startDir },
|
|
266
|
+
message: 'Could not find app/routes.ts. Run this command inside a Remix app.',
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function targetDirectoryNotEmpty(targetDir: string): UsageError {
|
|
271
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.targetDirectoryNotEmpty, {
|
|
272
|
+
context: { targetDir },
|
|
273
|
+
message: `Target directory is not empty: ${targetDir}. Re-run with --force to continue.`,
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function targetPathNotDirectory(targetDir: string): UsageError {
|
|
278
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.targetPathNotDirectory, {
|
|
279
|
+
context: { targetDir },
|
|
280
|
+
message: `Target path is not a directory: ${targetDir}`,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function toCliError(error: unknown): CliError {
|
|
285
|
+
if (error instanceof CliError) {
|
|
286
|
+
return error
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (error instanceof Error) {
|
|
290
|
+
return createCliError(CLI_ERROR_DEFINITIONS.internalError, { message: error.message })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return createCliError(CLI_ERROR_DEFINITIONS.internalError, { message: String(error) })
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function unknownArgument(argument: string): UsageError {
|
|
297
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.unknownArgument, {
|
|
298
|
+
context: { argument },
|
|
299
|
+
message: `Unknown argument: ${argument}`,
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function unknownCommand(command: string): UsageError {
|
|
304
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.unknownCommand, {
|
|
305
|
+
context: { command },
|
|
306
|
+
message: `Unknown command: ${command}`,
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function unknownCompletionShell(shell: string): UsageError {
|
|
311
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.unknownCompletionShell, {
|
|
312
|
+
context: { shell },
|
|
313
|
+
message: `Unknown completion shell: ${shell}`,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function unknownHelpTopic(topic: string): UsageError {
|
|
318
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.unknownHelpTopic, {
|
|
319
|
+
context: { topic },
|
|
320
|
+
message: `Unknown help topic: ${topic}`,
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function unknownSkillsCommand(command: string): UsageError {
|
|
325
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.unknownSkillsCommand, {
|
|
326
|
+
context: { command },
|
|
327
|
+
message: `Unknown skills command: ${command}`,
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function unexpectedExtraArgument(argument: string): UsageError {
|
|
332
|
+
return createUsageError(CLI_ERROR_DEFINITIONS.unexpectedExtraArgument, {
|
|
333
|
+
context: { argument },
|
|
334
|
+
message: `Unexpected extra argument: ${argument}`,
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function renderCliError(error: unknown, options: { helpText?: string } = {}): string {
|
|
339
|
+
let cliError = toCliError(error)
|
|
340
|
+
let lines = [`Error [${cliError.code}] ${cliError.title}`, cliError.message]
|
|
341
|
+
|
|
342
|
+
if (cliError.fix != null && cliError.fix.length > 0) {
|
|
343
|
+
lines.push('', 'Try:', ` ${cliError.fix}`)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (cliError.docsUrl != null && cliError.docsUrl.length > 0) {
|
|
347
|
+
lines.push('', 'Documentation:', ` ${cliError.docsUrl}`)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let output = `${lines.join('\n')}\n`
|
|
351
|
+
|
|
352
|
+
if (cliError.showHelp && options.helpText != null) {
|
|
353
|
+
output += `\n${options.helpText}`
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return output
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function createCliError(definition: CliErrorDefinition, options: CreateErrorOptions): CliError {
|
|
360
|
+
return new CliError({
|
|
361
|
+
code: definition.code,
|
|
362
|
+
context: options.context,
|
|
363
|
+
docsUrl: definition.docsUrl,
|
|
364
|
+
fix: definition.fix,
|
|
365
|
+
message: options.message,
|
|
366
|
+
title: definition.title,
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function createUsageError(definition: CliErrorDefinition, options: CreateErrorOptions): UsageError {
|
|
371
|
+
return new UsageError({
|
|
372
|
+
code: definition.code,
|
|
373
|
+
context: options.context,
|
|
374
|
+
docsUrl: definition.docsUrl,
|
|
375
|
+
fix: definition.fix,
|
|
376
|
+
message: options.message,
|
|
377
|
+
title: definition.title,
|
|
378
|
+
})
|
|
379
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import * as process from 'node:process'
|
|
2
|
+
|
|
3
|
+
import { boldLightBlue, lightYellow } from './terminal.ts'
|
|
4
|
+
|
|
5
|
+
export interface HelpTextSectionRow {
|
|
6
|
+
description: string
|
|
7
|
+
label: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HelpTextOptions {
|
|
11
|
+
commands?: HelpTextSectionRow[]
|
|
12
|
+
description?: string
|
|
13
|
+
examples?: string[]
|
|
14
|
+
options?: HelpTextSectionRow[]
|
|
15
|
+
usage: string[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SECTION_INDENT = ' '
|
|
19
|
+
const ROW_GAP = 2
|
|
20
|
+
|
|
21
|
+
export function formatHelpText(
|
|
22
|
+
options: HelpTextOptions,
|
|
23
|
+
target: NodeJS.WriteStream = process.stdout,
|
|
24
|
+
): string {
|
|
25
|
+
let sections: string[] = [renderUsageSection(options.usage, target)]
|
|
26
|
+
|
|
27
|
+
if (options.description != null && options.description.length > 0) {
|
|
28
|
+
sections.push(options.description)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.commands != null && options.commands.length > 0) {
|
|
32
|
+
sections.push(renderRowSection('Commands', options.commands, target))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.options != null && options.options.length > 0) {
|
|
36
|
+
sections.push(renderRowSection('Options', options.options, target))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.examples != null && options.examples.length > 0) {
|
|
40
|
+
sections.push(renderExamplesSection(options.examples, target))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `${sections.join('\n\n')}\n`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderUsageSection(usage: string[], target: NodeJS.WriteStream): string {
|
|
47
|
+
return renderSection(
|
|
48
|
+
'Usage',
|
|
49
|
+
usage.map((line) => `${SECTION_INDENT}${highlightSyntax(line, target)}`),
|
|
50
|
+
target,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderExamplesSection(examples: string[], target: NodeJS.WriteStream): string {
|
|
55
|
+
return renderSection(
|
|
56
|
+
'Examples',
|
|
57
|
+
examples.map((line) => `${SECTION_INDENT}${highlightSyntax(line, target)}`),
|
|
58
|
+
target,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderRowSection(
|
|
63
|
+
title: string,
|
|
64
|
+
rows: HelpTextSectionRow[],
|
|
65
|
+
target: NodeJS.WriteStream,
|
|
66
|
+
): string {
|
|
67
|
+
let labelWidth = rows.reduce((width, row) => Math.max(width, row.label.length), 0) + ROW_GAP
|
|
68
|
+
let lines = rows.map((row) => {
|
|
69
|
+
let paddedLabel = row.label.padEnd(labelWidth)
|
|
70
|
+
return `${SECTION_INDENT}${highlightSyntax(paddedLabel, target)}${row.description}`
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return renderSection(title, lines, target)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function renderSection(title: string, lines: string[], target: NodeJS.WriteStream): string {
|
|
77
|
+
return `${formatHeading(title, target)}:\n${lines.join('\n')}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatHeading(title: string, target: NodeJS.WriteStream): string {
|
|
81
|
+
return boldLightBlue(title, target)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function highlightSyntax(text: string, target: NodeJS.WriteStream): string {
|
|
85
|
+
return text.replace(/(--[A-Za-z0-9-]+|-[A-Za-z]|\[[A-Za-z][A-Za-z0-9-]*\]|<[^>\n]+>)/g, (token) =>
|
|
86
|
+
lightYellow(token, target),
|
|
87
|
+
)
|
|
88
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as process from 'node:process'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
|
|
4
|
+
import type { RawRouteTreeNode } from './route-map.ts'
|
|
5
|
+
|
|
6
|
+
void run().catch((error: unknown) => {
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
process.stderr.write(`${error.message}\n`)
|
|
9
|
+
} else {
|
|
10
|
+
process.stderr.write(`${String(error)}\n`)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
setExitCode(1)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
async function run(): Promise<void> {
|
|
17
|
+
let routesFile = process.argv[2]
|
|
18
|
+
if (typeof routesFile !== 'string' || routesFile.length === 0) {
|
|
19
|
+
throw new Error('Missing app/routes.ts path.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let tree = await loadRawRouteTree(routesFile)
|
|
23
|
+
process.stdout.write(JSON.stringify(tree))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function loadRawRouteTree(routesFile: string): Promise<RawRouteTreeNode[]> {
|
|
27
|
+
let routeModule = await import(pathToFileURL(routesFile).href)
|
|
28
|
+
|
|
29
|
+
if (!('routes' in routeModule)) {
|
|
30
|
+
throw new Error(`Route module ${routesFile} must export a named "routes" value.`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return normalizeRouteGroup(Reflect.get(routeModule, 'routes'), [], new Map())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeRouteGroup(
|
|
37
|
+
value: unknown,
|
|
38
|
+
parentSegments: string[],
|
|
39
|
+
seen: Map<object, string>,
|
|
40
|
+
): RawRouteTreeNode[] {
|
|
41
|
+
if (!isPlainObject(value)) {
|
|
42
|
+
let location = parentSegments.length === 0 ? 'routes' : parentSegments.join('.')
|
|
43
|
+
throw new Error(`Invalid route map value at "${location}". Expected a nested route object.`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let existingPath = seen.get(value)
|
|
47
|
+
let currentPath = parentSegments.length === 0 ? 'routes' : parentSegments.join('.')
|
|
48
|
+
if (existingPath != null) {
|
|
49
|
+
throw new Error(`Detected a route map cycle at "${currentPath}" via "${existingPath}".`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
seen.set(value, currentPath)
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
return Object.entries(value).map(([key, entryValue]) => {
|
|
56
|
+
let nameSegments = [...parentSegments, key]
|
|
57
|
+
let name = nameSegments.join('.')
|
|
58
|
+
|
|
59
|
+
if (isRouteLeaf(entryValue)) {
|
|
60
|
+
return {
|
|
61
|
+
children: [],
|
|
62
|
+
key,
|
|
63
|
+
kind: 'route',
|
|
64
|
+
method: entryValue.method,
|
|
65
|
+
name,
|
|
66
|
+
pattern: entryValue.pattern.source,
|
|
67
|
+
} satisfies RawRouteTreeNode
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isPlainObject(entryValue)) {
|
|
71
|
+
return {
|
|
72
|
+
children: normalizeRouteGroup(entryValue, nameSegments, seen),
|
|
73
|
+
key,
|
|
74
|
+
kind: 'group',
|
|
75
|
+
name,
|
|
76
|
+
} satisfies RawRouteTreeNode
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Invalid route map value at "${name}". Expected a route or nested route object.`,
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
} finally {
|
|
84
|
+
seen.delete(value)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isRouteLeaf(value: unknown): value is { method: string; pattern: { source: string } } {
|
|
89
|
+
return (
|
|
90
|
+
typeof value === 'object' &&
|
|
91
|
+
value != null &&
|
|
92
|
+
typeof Reflect.get(value, 'method') === 'string' &&
|
|
93
|
+
typeof Reflect.get(value, 'pattern') === 'object' &&
|
|
94
|
+
Reflect.get(value, 'pattern') != null &&
|
|
95
|
+
typeof Reflect.get(Reflect.get(value, 'pattern'), 'source') === 'string'
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
100
|
+
if (typeof value !== 'object' || value == null) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let prototype = Object.getPrototypeOf(value)
|
|
105
|
+
return prototype === Object.prototype || prototype === null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setExitCode(exitCode: number) {
|
|
109
|
+
globalThis.process.exitCode = exitCode
|
|
110
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { missingOptionValue, unexpectedExtraArgument, unknownArgument } from './errors.ts'
|
|
2
|
+
|
|
3
|
+
export interface ParseArgsBooleanOptionSpec {
|
|
4
|
+
flag: string
|
|
5
|
+
type: 'boolean'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ParseArgsStringOptionSpec {
|
|
9
|
+
flag: string
|
|
10
|
+
type: 'string'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type ParseArgsOptionSpec = ParseArgsBooleanOptionSpec | ParseArgsStringOptionSpec
|
|
14
|
+
|
|
15
|
+
export type ParseArgsOptionDefinitions = Record<string, ParseArgsOptionSpec>
|
|
16
|
+
|
|
17
|
+
export interface ParseArgsOptions {
|
|
18
|
+
maxPositionals?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ParsedArgsValues<definitions extends ParseArgsOptionDefinitions> = {
|
|
22
|
+
[key in keyof definitions]: definitions[key]['type'] extends 'boolean'
|
|
23
|
+
? boolean
|
|
24
|
+
: string | undefined
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ParsedArgsResult<definitions extends ParseArgsOptionDefinitions> {
|
|
28
|
+
options: ParsedArgsValues<definitions>
|
|
29
|
+
positionals: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseArgs<const definitions extends ParseArgsOptionDefinitions>(
|
|
33
|
+
argv: string[],
|
|
34
|
+
definitions: definitions,
|
|
35
|
+
options: ParseArgsOptions = {},
|
|
36
|
+
): ParsedArgsResult<definitions> {
|
|
37
|
+
let values = {} as ParsedArgsValues<definitions>
|
|
38
|
+
let specsByFlag = new Map<string, { key: keyof definitions; spec: ParseArgsOptionSpec }>()
|
|
39
|
+
let positionals: string[] = []
|
|
40
|
+
|
|
41
|
+
for (let [key, spec] of Object.entries(definitions) as Array<
|
|
42
|
+
[keyof definitions, ParseArgsOptionSpec]
|
|
43
|
+
>) {
|
|
44
|
+
values[key] = (
|
|
45
|
+
spec.type === 'boolean' ? false : undefined
|
|
46
|
+
) as ParsedArgsValues<definitions>[typeof key]
|
|
47
|
+
specsByFlag.set(spec.flag, { key, spec })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (let index = 0; index < argv.length; index++) {
|
|
51
|
+
let arg = argv[index]!
|
|
52
|
+
let resolved = specsByFlag.get(arg)
|
|
53
|
+
|
|
54
|
+
if (resolved != null) {
|
|
55
|
+
if (resolved.spec.type === 'boolean') {
|
|
56
|
+
values[resolved.key] = true as ParsedArgsValues<definitions>[typeof resolved.key]
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let next = argv[index + 1]
|
|
61
|
+
if (next == null || next.startsWith('-')) {
|
|
62
|
+
throw missingOptionValue(arg)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
values[resolved.key] = next as ParsedArgsValues<definitions>[typeof resolved.key]
|
|
66
|
+
index += 1
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg.startsWith('-')) {
|
|
71
|
+
throw unknownArgument(arg)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (options.maxPositionals != null && positionals.length >= options.maxPositionals) {
|
|
75
|
+
throw unexpectedExtraArgument(arg)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
positionals.push(arg)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { options: values, positionals }
|
|
82
|
+
}
|