@react-native-reusables/cli 0.6.3 → 0.7.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/.changeset/config.json +16 -0
- package/.github/actions/setup/action.yml +21 -0
- package/.github/workflows/check.yml +54 -0
- package/.github/workflows/release.yml +0 -0
- package/.github/workflows/snapshot.yml +24 -0
- package/.prettierrc +8 -0
- package/.vscode/extensions.json +6 -0
- package/.vscode/settings.json +46 -0
- package/LICENSE +1 -1
- package/README.md +97 -0
- package/eslint.config.mjs +118 -0
- package/package.json +64 -8
- package/patches/@changesets__get-github-info@0.6.0.patch +48 -0
- package/scripts/copy-package-json.ts +32 -0
- package/src/bin.ts +18 -0
- package/src/cli.ts +66 -0
- package/src/contexts/cli-options.ts +29 -0
- package/src/project-manifest.ts +369 -0
- package/src/services/commands/add.ts +127 -0
- package/src/services/commands/doctor.ts +287 -0
- package/src/services/commands/init.ts +94 -0
- package/src/services/git.ts +39 -0
- package/src/services/package-manager.ts +48 -0
- package/src/services/project-config.ts +295 -0
- package/src/services/required-files-checker.ts +375 -0
- package/src/services/spinner.ts +15 -0
- package/src/services/template.ts +222 -0
- package/src/utils/retry-with.ts +9 -0
- package/src/utils/run-command.ts +10 -0
- package/test/Dummy.test.ts +7 -0
- package/tsconfig.base.json +53 -0
- package/tsconfig.json +14 -0
- package/tsconfig.scripts.json +17 -0
- package/tsconfig.src.json +10 -0
- package/tsconfig.test.json +10 -0
- package/tsup.config.ts +9 -0
- package/vitest.config.ts +12 -0
- package/bin.cjs +0 -59056
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { CliOptions } from "@cli/contexts/cli-options.js"
|
|
2
|
+
import { type CustomFileCheck, type FileCheck, type MissingInclude, PROJECT_MANIFEST } from "@cli/project-manifest.js"
|
|
3
|
+
import { ProjectConfig } from "@cli/services/project-config.js"
|
|
4
|
+
import { RequiredFilesChecker } from "@cli/services/required-files-checker.js"
|
|
5
|
+
import { Spinner } from "@cli/services/spinner.js"
|
|
6
|
+
import { runCommand } from "@cli/utils/run-command.js"
|
|
7
|
+
import { Prompt } from "@effect/cli"
|
|
8
|
+
import { FileSystem, Path } from "@effect/platform"
|
|
9
|
+
import { Data, Effect, Layer, Schema } from "effect"
|
|
10
|
+
import logSymbols from "log-symbols"
|
|
11
|
+
|
|
12
|
+
const packageJsonSchema = Schema.Struct({
|
|
13
|
+
dependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
14
|
+
devDependencies: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String }))
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
class PackageJsonError extends Data.TaggedError("PackageJsonError")<{
|
|
18
|
+
cause?: unknown
|
|
19
|
+
message?: string
|
|
20
|
+
}> {}
|
|
21
|
+
|
|
22
|
+
type DoctorOptions = {
|
|
23
|
+
cwd: string
|
|
24
|
+
summary: boolean
|
|
25
|
+
yes: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class Doctor extends Effect.Service<Doctor>()("Doctor", {
|
|
29
|
+
dependencies: [RequiredFilesChecker.Default, Spinner.Default],
|
|
30
|
+
effect: Effect.gen(function* () {
|
|
31
|
+
const options = yield* CliOptions
|
|
32
|
+
const fs = yield* FileSystem.FileSystem
|
|
33
|
+
const path = yield* Path.Path
|
|
34
|
+
const requiredFileChecker = yield* RequiredFilesChecker
|
|
35
|
+
const spinner = yield* Spinner
|
|
36
|
+
const projectConfig = yield* ProjectConfig
|
|
37
|
+
|
|
38
|
+
const stylingLibrary = yield* projectConfig.getStylingLibrary()
|
|
39
|
+
|
|
40
|
+
if (stylingLibrary !== "unknown"){
|
|
41
|
+
console.log(
|
|
42
|
+
`\x1b[2m${logSymbols.info} Styling Library: ${stylingLibrary === "uniwind" ? "Uniwind" : "Nativewind"}\x1b[0m`
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const checkRequiredDependencies = ({
|
|
48
|
+
dependencies,
|
|
49
|
+
devDependencies
|
|
50
|
+
}: {
|
|
51
|
+
dependencies: Array<string>
|
|
52
|
+
devDependencies: Array<string>
|
|
53
|
+
}) =>
|
|
54
|
+
Effect.gen(function* () {
|
|
55
|
+
const packageJsonExists = yield* fs.exists(path.join(options.cwd, "package.json"))
|
|
56
|
+
if (!packageJsonExists) {
|
|
57
|
+
return yield* Effect.fail(new PackageJsonError({ message: "A package.json was not found and is required." }))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const packageJson = yield* fs.readFileString(path.join(options.cwd, "package.json")).pipe(
|
|
61
|
+
Effect.flatMap(Schema.decodeUnknown(Schema.parseJson())),
|
|
62
|
+
Effect.flatMap(Schema.decodeUnknown(packageJsonSchema)),
|
|
63
|
+
Effect.catchTags({
|
|
64
|
+
ParseError: () => Effect.fail(new PackageJsonError({ message: "Failed to parse package.json" }))
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const uninstalledDependencies: Array<string> = []
|
|
69
|
+
const uninstalledDevDependencies: Array<string> = []
|
|
70
|
+
|
|
71
|
+
for (const dependency of dependencies) {
|
|
72
|
+
if (
|
|
73
|
+
!packageJson.dependencies?.[dependency.split("@")[0]] &&
|
|
74
|
+
!packageJson.devDependencies?.[dependency.split("@")[0]]
|
|
75
|
+
) {
|
|
76
|
+
uninstalledDependencies.push(dependency)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
yield* Effect.logDebug(
|
|
80
|
+
`${logSymbols.success} ${dependency}@${packageJson.dependencies?.[dependency.split("@")[0]]} is installed`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const devDependency of devDependencies) {
|
|
85
|
+
if (
|
|
86
|
+
!packageJson.devDependencies?.[devDependency.split("@")[0]] &&
|
|
87
|
+
!packageJson.dependencies?.[devDependency.split("@")[0]]
|
|
88
|
+
) {
|
|
89
|
+
uninstalledDevDependencies.push(devDependency)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
yield* Effect.logDebug(
|
|
93
|
+
`${logSymbols.success} ${devDependency}@${packageJson.devDependencies?.[devDependency]} is installed`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { uninstalledDependencies, uninstalledDevDependencies }
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const registry = stylingLibrary === "uniwind" ? "uniwind" : "nativewind"
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
run: (options: DoctorOptions) =>
|
|
104
|
+
Effect.gen(function* () {
|
|
105
|
+
yield* Effect.logDebug(`Doctor options: ${JSON.stringify(options, null, 2)}`)
|
|
106
|
+
const { uninstalledDependencies, uninstalledDevDependencies } = yield* checkRequiredDependencies({
|
|
107
|
+
dependencies: PROJECT_MANIFEST.dependencies[registry],
|
|
108
|
+
devDependencies: PROJECT_MANIFEST.devDependencies
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const { customFileResults, deprecatedFileResults, fileResults } = yield* requiredFileChecker.run({
|
|
112
|
+
customFileChecks: PROJECT_MANIFEST.customFileChecks,
|
|
113
|
+
deprecatedFromLib: PROJECT_MANIFEST.deprecatedFromLib,
|
|
114
|
+
deprecatedFromUi: PROJECT_MANIFEST.deprecatedFromUi,
|
|
115
|
+
fileChecks: PROJECT_MANIFEST.fileChecks,
|
|
116
|
+
stylingLibrary: registry
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const result = {
|
|
120
|
+
missingFiles: [...fileResults.missingFiles, ...customFileResults.missingFiles],
|
|
121
|
+
uninstalledDependencies,
|
|
122
|
+
uninstalledDevDependencies,
|
|
123
|
+
missingIncludes: [...fileResults.missingIncludes, ...customFileResults.missingIncludes],
|
|
124
|
+
deprecatedFileResults
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let total = Object.values(result).reduce((sum, cat) => sum + cat.length, 0)
|
|
128
|
+
if (!options.summary) {
|
|
129
|
+
const dependenciesToInstall: Array<string> = []
|
|
130
|
+
for (const dep of result.uninstalledDependencies) {
|
|
131
|
+
const confirmsInstall = options.yes
|
|
132
|
+
? true
|
|
133
|
+
: yield* Prompt.confirm({
|
|
134
|
+
message: `The ${dep} dependency is missing. Do you want to install it?`,
|
|
135
|
+
initial: true
|
|
136
|
+
})
|
|
137
|
+
if (confirmsInstall) {
|
|
138
|
+
if (uninstalledDependencies.includes("expo")) {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
total--
|
|
142
|
+
yield* Effect.logDebug(`Adding ${dep} to dependencies to install`)
|
|
143
|
+
dependenciesToInstall.push(dep)
|
|
144
|
+
result.uninstalledDependencies = result.uninstalledDependencies.filter((d) => d !== dep)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (dependenciesToInstall.length > 0) {
|
|
149
|
+
yield* Effect.logDebug(`Installing ${dependenciesToInstall.join(", ")}`)
|
|
150
|
+
if (process.env.INTERNAL_ENV !== "development") {
|
|
151
|
+
spinner.start("Installing dependencies")
|
|
152
|
+
yield* runCommand("npx", ["expo", "install", ...dependenciesToInstall], {
|
|
153
|
+
cwd: options.cwd
|
|
154
|
+
})
|
|
155
|
+
spinner.stop()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const devDependenciesToInstall: Array<string> = []
|
|
160
|
+
for (const dep of result.uninstalledDevDependencies) {
|
|
161
|
+
const confirmsInstall = options.yes
|
|
162
|
+
? true
|
|
163
|
+
: yield* Prompt.confirm({
|
|
164
|
+
message: `The ${dep} dependency is missing. Do you want to install it?`,
|
|
165
|
+
initial: true
|
|
166
|
+
})
|
|
167
|
+
if (confirmsInstall) {
|
|
168
|
+
if (uninstalledDependencies.includes("expo")) {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
total--
|
|
172
|
+
yield* Effect.logDebug(`Adding ${dep} to devDependencies to install`)
|
|
173
|
+
devDependenciesToInstall.push(dep)
|
|
174
|
+
result.uninstalledDevDependencies = result.uninstalledDevDependencies.filter((d) => d !== dep)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (devDependenciesToInstall.length > 0) {
|
|
179
|
+
yield* Effect.logDebug(`Installing ${devDependenciesToInstall.join(", ")}`)
|
|
180
|
+
if (process.env.INTERNAL_ENV !== "development") {
|
|
181
|
+
spinner.start("Installing dev dependencies")
|
|
182
|
+
yield* runCommand("npx", ["expo", "install", ...devDependenciesToInstall], {
|
|
183
|
+
cwd: options.cwd
|
|
184
|
+
})
|
|
185
|
+
spinner.stop()
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (total === 0) {
|
|
191
|
+
console.log(`\x1b[2m${logSymbols.success} All checks passed.\x1b[0m\n`)
|
|
192
|
+
return yield* Effect.succeed(true)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const analysis = analyzeResult(result)
|
|
196
|
+
if (options.summary) {
|
|
197
|
+
console.log(
|
|
198
|
+
`\x1b[2m${logSymbols.warning} ${total} Potential issue${
|
|
199
|
+
total > 1 ? "s" : ""
|
|
200
|
+
} found. For more info, run: 'npx @react-native-reusables/cli doctor${
|
|
201
|
+
options.cwd !== "." ? ` -c ${options.cwd}` : ""
|
|
202
|
+
}'\x1b[0m\n`
|
|
203
|
+
)
|
|
204
|
+
} else {
|
|
205
|
+
yield* Effect.log("\n\n🩺 Diagnosis")
|
|
206
|
+
for (const item of analysis) {
|
|
207
|
+
console.group(`\n${item.title}`)
|
|
208
|
+
item.logs.forEach((line) => console.log(line))
|
|
209
|
+
console.groupEnd()
|
|
210
|
+
}
|
|
211
|
+
console.log(`\n`)
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}) {}
|
|
217
|
+
|
|
218
|
+
function make(options: DoctorOptions) {
|
|
219
|
+
const optionsLayer = Layer.succeed(CliOptions, { ...options, stylingLibrary: undefined })
|
|
220
|
+
return Effect.gen(function* () {
|
|
221
|
+
const doctor = yield* Doctor
|
|
222
|
+
return yield* doctor.run(options)
|
|
223
|
+
}).pipe(Effect.provide(Doctor.Default), Effect.provide(ProjectConfig.Default), Effect.provide(optionsLayer))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export { Doctor, make }
|
|
227
|
+
|
|
228
|
+
interface Result {
|
|
229
|
+
missingFiles: Array<FileCheck | CustomFileCheck>
|
|
230
|
+
missingIncludes: Array<MissingInclude>
|
|
231
|
+
uninstalledDependencies: Array<string>
|
|
232
|
+
uninstalledDevDependencies: Array<string>
|
|
233
|
+
deprecatedFileResults: Array<Omit<FileCheck, "docs" | "stylingLibraries">>
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function analyzeResult(result: Result) {
|
|
237
|
+
const categories: Array<{ title: string; logs: Array<string>; count: number }> = []
|
|
238
|
+
|
|
239
|
+
if (result.missingFiles.length > 0) {
|
|
240
|
+
categories.push({
|
|
241
|
+
title: `${logSymbols.error} Missing Files (${result.missingFiles.length})`,
|
|
242
|
+
count: result.missingFiles.length,
|
|
243
|
+
logs: result.missingFiles.flatMap((f) => [`• ${f.name}`, ` 📘 Docs: ${f.docs}`])
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (result.missingIncludes.length > 0) {
|
|
248
|
+
categories.push({
|
|
249
|
+
title: `${logSymbols.error} Potentially Misconfigured Files (${result.missingIncludes.length})`,
|
|
250
|
+
count: result.missingIncludes.length,
|
|
251
|
+
logs: result.missingIncludes.flatMap((inc) => [
|
|
252
|
+
`• ${inc.fileName}`,
|
|
253
|
+
` ↪ ${inc.message}`,
|
|
254
|
+
` 📘 Docs: ${inc.docs}`
|
|
255
|
+
])
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (result.uninstalledDependencies.length > 0) {
|
|
260
|
+
categories.push({
|
|
261
|
+
title: `${logSymbols.error} Missing Dependencies (${result.uninstalledDependencies.length})`,
|
|
262
|
+
count: result.uninstalledDependencies.length,
|
|
263
|
+
logs: ["• Install with:", ` ↪ npx expo install ${result.uninstalledDependencies.join(" ")}`]
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (result.uninstalledDevDependencies.length > 0) {
|
|
268
|
+
categories.push({
|
|
269
|
+
title: `${logSymbols.error} Missing Dev Dependencies (${result.uninstalledDevDependencies.length})`,
|
|
270
|
+
count: result.uninstalledDevDependencies.length,
|
|
271
|
+
logs: ["• Install with:", ` ↪ npx expo install -- -D ${result.uninstalledDevDependencies.join(" ")}`]
|
|
272
|
+
})
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (result.deprecatedFileResults.length > 0) {
|
|
276
|
+
categories.push({
|
|
277
|
+
title: `${logSymbols.warning} Deprecated (${result.deprecatedFileResults.length})`,
|
|
278
|
+
count: result.deprecatedFileResults.length,
|
|
279
|
+
logs: result.deprecatedFileResults.flatMap((deprecatedFile) => [
|
|
280
|
+
`• ${deprecatedFile.name}`,
|
|
281
|
+
...deprecatedFile.includes.map((item) => ` ↪ ${item.message}\n 📘 Docs: ${item.docs}`)
|
|
282
|
+
])
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return categories
|
|
287
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CliOptions } from "@cli/contexts/cli-options.js"
|
|
2
|
+
import { PROJECT_MANIFEST } from "@cli/project-manifest.js"
|
|
3
|
+
import { Doctor } from "@cli/services/commands/doctor.js"
|
|
4
|
+
import { ProjectConfig } from "@cli/services/project-config.js"
|
|
5
|
+
import { Template } from "@cli/services/template.js"
|
|
6
|
+
import { Prompt } from "@effect/cli"
|
|
7
|
+
import { FileSystem, Path } from "@effect/platform"
|
|
8
|
+
import { Effect, Layer } from "effect"
|
|
9
|
+
import logSymbols from "log-symbols"
|
|
10
|
+
|
|
11
|
+
type InitOptions = {
|
|
12
|
+
cwd: string
|
|
13
|
+
template: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
class Init extends Effect.Service<Init>()("Init", {
|
|
17
|
+
dependencies: [Template.Default],
|
|
18
|
+
effect: Effect.gen(function* () {
|
|
19
|
+
const fs = yield* FileSystem.FileSystem
|
|
20
|
+
const path = yield* Path.Path
|
|
21
|
+
const doctor = yield* Doctor
|
|
22
|
+
const template = yield* Template
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
run: (options: InitOptions) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
yield* Effect.logDebug(`Init options: ${JSON.stringify(options, null, 2)}`)
|
|
28
|
+
|
|
29
|
+
const packageJsonExists = yield* fs.exists(path.join(options.cwd, "package.json"))
|
|
30
|
+
|
|
31
|
+
yield* Effect.logDebug(`Does package.json exist: ${packageJsonExists ? "yes" : "no"}`)
|
|
32
|
+
|
|
33
|
+
if (packageJsonExists) {
|
|
34
|
+
yield* Effect.logWarning(`${logSymbols.warning} A project already exists in this directory.`)
|
|
35
|
+
const choice = yield* Prompt.select({
|
|
36
|
+
message: "How would you like to proceed?",
|
|
37
|
+
choices: [
|
|
38
|
+
{ title: "Initialize a new project here anyway", value: "init-new" },
|
|
39
|
+
{ title: "Inspect project configuration", value: "doctor" },
|
|
40
|
+
{ title: "Cancel and exit", value: "cancel" }
|
|
41
|
+
]
|
|
42
|
+
})
|
|
43
|
+
yield* Effect.logDebug(`Init choice: ${choice}`)
|
|
44
|
+
if (choice === "cancel") {
|
|
45
|
+
return yield* Effect.succeed(true)
|
|
46
|
+
}
|
|
47
|
+
if (choice === "doctor") {
|
|
48
|
+
console.log("")
|
|
49
|
+
return yield* doctor.run({ ...options, summary: false, yes: false })
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const projectName = yield* Prompt.text({
|
|
54
|
+
message: "What is the name of your project? (e.g. my-app)",
|
|
55
|
+
default: "my-app"
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const templateFromFlag = PROJECT_MANIFEST.templates.find((t) => t.subPath === options.template)
|
|
59
|
+
|
|
60
|
+
const selectedTemplate = templateFromFlag
|
|
61
|
+
? templateFromFlag
|
|
62
|
+
: yield* Prompt.select({
|
|
63
|
+
message: "Select a template",
|
|
64
|
+
choices: PROJECT_MANIFEST.templates.map((template) => ({
|
|
65
|
+
title: template.name,
|
|
66
|
+
value: template
|
|
67
|
+
}))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
yield* template.clone({
|
|
71
|
+
cwd: options.cwd,
|
|
72
|
+
name: projectName,
|
|
73
|
+
repo: selectedTemplate
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
}) {}
|
|
79
|
+
|
|
80
|
+
function make(options: InitOptions) {
|
|
81
|
+
const optionsLayer = Layer.succeed(CliOptions, { ...options, yes: true, stylingLibrary: undefined })
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
const init = yield* Init
|
|
84
|
+
|
|
85
|
+
return yield* init.run(options)
|
|
86
|
+
}).pipe(
|
|
87
|
+
Effect.provide(Init.Default),
|
|
88
|
+
Effect.provide(Doctor.Default),
|
|
89
|
+
Effect.provide(ProjectConfig.Default),
|
|
90
|
+
Effect.provide(optionsLayer)
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { make }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Data, Effect } from "effect"
|
|
2
|
+
import { Command } from "@effect/platform"
|
|
3
|
+
import { Prompt } from "@effect/cli"
|
|
4
|
+
import logSymbols from "log-symbols"
|
|
5
|
+
|
|
6
|
+
export class GitError extends Data.TaggedError("GitError")<{
|
|
7
|
+
cause?: unknown
|
|
8
|
+
message?: string
|
|
9
|
+
}> {}
|
|
10
|
+
|
|
11
|
+
const COMMANDS = {
|
|
12
|
+
status: Command.make("git", "status", "--porcelain")
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export class Git extends Effect.Service<Git>()("Git", {
|
|
16
|
+
succeed: {
|
|
17
|
+
promptIfDirty: () =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const gitStatus = yield* COMMANDS.status.pipe(
|
|
20
|
+
Command.string,
|
|
21
|
+
Effect.catchAll(() => Effect.succeed("")) // Not a git repository
|
|
22
|
+
)
|
|
23
|
+
const isDirty = gitStatus.trim().length > 0
|
|
24
|
+
if (!isDirty) {
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
const result = yield* Prompt.confirm({
|
|
28
|
+
message: `${logSymbols.warning} The Git repository is dirty (uncommitted changes). Continue anyway?`,
|
|
29
|
+
initial: true
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
if (!result) {
|
|
33
|
+
return yield* Effect.fail(new GitError({ message: "Aborted due to uncommitted changes." }))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
}) {}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { detect } from "package-manager-detector"
|
|
3
|
+
|
|
4
|
+
const PACKAGE_MANAGERS = ["npm", "bun", "pnpm", "yarn@berry", "yarn"] as const
|
|
5
|
+
|
|
6
|
+
const BINARY_RUNNERS = {
|
|
7
|
+
npm: ["npx"],
|
|
8
|
+
bun: ["bunx", "--bun"],
|
|
9
|
+
pnpm: ["pnpm", "dlx"],
|
|
10
|
+
yarn: ["npx"],
|
|
11
|
+
"yarn@berry": ["npx"]
|
|
12
|
+
} as const
|
|
13
|
+
|
|
14
|
+
const detectPackageManager = (cwd: string) =>
|
|
15
|
+
Effect.tryPromise({
|
|
16
|
+
try: () => {
|
|
17
|
+
return detect({
|
|
18
|
+
cwd,
|
|
19
|
+
strategies: ["install-metadata", "lockfile", "packageManager-field", "devEngines-field"]
|
|
20
|
+
})
|
|
21
|
+
},
|
|
22
|
+
catch: () => new Error("Failed to get package manager")
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const getPackageManager = (cwd: string) =>
|
|
26
|
+
Effect.gen(function* () {
|
|
27
|
+
const pm = yield* detectPackageManager(cwd)
|
|
28
|
+
if (!pm) {
|
|
29
|
+
return "npm"
|
|
30
|
+
}
|
|
31
|
+
const name = PACKAGE_MANAGERS.find((name) => pm.agent.startsWith(name) || pm.name.startsWith(name)) ?? "npm"
|
|
32
|
+
return name
|
|
33
|
+
})
|
|
34
|
+
const getBinaryRunner = (cwd: string) =>
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const pm = yield* getPackageManager(cwd)
|
|
37
|
+
return BINARY_RUNNERS[pm]
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
class PackageManager extends Effect.Service<PackageManager>()("PackageManager", {
|
|
41
|
+
succeed: {
|
|
42
|
+
detectPackageManager,
|
|
43
|
+
getPackageManager,
|
|
44
|
+
getBinaryRunner
|
|
45
|
+
}
|
|
46
|
+
}) {}
|
|
47
|
+
|
|
48
|
+
export { PackageManager }
|