@effect-app/cli 1.23.1 → 1.23.2

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/src/index.ts CHANGED
@@ -1,283 +1,675 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- /* eslint-disable @typescript-eslint/no-unsafe-argument */
3
- import cp from "child_process"
4
- import fs from "fs"
5
- import w from "node-watch"
6
- import path from "path"
7
- import readline from "readline/promises"
8
- import { sync } from "./sync.js"
9
-
10
- function askQuestion(query: string) {
11
- const rl = readline.createInterface({
12
- input: process.stdin,
13
- output: process.stdout
14
- })
15
-
16
- return rl.question(query)
17
- }
18
-
19
- const _cmd = process.argv[2]
20
- const supportedCommands = [
21
- "watch",
22
- "index",
23
- "index-multi",
24
- "packagejson",
25
- "packagejson-target",
26
- "packagejson-packages",
27
- "link",
28
- "unlink",
29
- "sync",
30
- "ncu:effect",
31
- "ncu:effect-app"
32
- ] as const
33
- if (
34
- !supportedCommands.includes(_cmd as any)
35
- ) {
36
- console.log("unknown command: ", _cmd, "supported commands: ", supportedCommands.join(", "))
37
- process.exit(1)
38
- }
39
-
40
- const cmd = _cmd as typeof supportedCommands[number]
41
-
42
- const debug = process.argv.includes("--debug")
43
-
44
- function touch(path: string) {
45
- const time = new Date()
46
- try {
47
- fs.utimesSync(path, time, time)
48
- } catch (err) {
49
- fs.closeSync(fs.openSync(path, "w"))
50
- }
51
- }
52
-
53
- function* monitorIndexes_(path: string) {
54
- yield monitorChildIndexes(path)
55
- const indexFile = path + "/index.ts"
56
- if (fs.existsSync(indexFile)) {
57
- yield monitorRootIndexes(path, indexFile)
58
- }
59
- }
60
-
61
- function monitorIndexes(path: string) {
62
- return [...monitorIndexes_(path)]
63
- }
64
-
65
- function monitorChildIndexes(path: string) {
66
- return w.default(path, { recursive: true }, (evt, path) => {
67
- const pathParts = path.split("/")
68
- const isController = pathParts[pathParts.length - 1]?.toLowerCase().includes(".controllers.")
69
- if (!isController) return
70
-
71
- let i = 1
72
- const r = pathParts.toReversed()
73
- while (i < r.length) {
74
- const files = ["controllers.ts", "routes.ts"]
75
- .map((f) => [...pathParts.slice(0, pathParts.length - i), f].join("/"))
76
- .filter((f) => fs.existsSync(f))
77
- if (files.length) {
1
+ /* eslint-disable no-constant-binary-expression */
2
+ /* eslint-disable no-empty-pattern */
3
+ // import necessary modules from the libraries
4
+ import { Args, Command, Options, Prompt } from "@effect/cli"
5
+ import { Command as NodeCommand, FileSystem, Path } from "@effect/platform"
6
+ import { NodeContext, NodeRuntime } from "@effect/platform-node"
7
+ import { Effect, identity, Stream } from "effect"
8
+ import { ExtractExportMappingsService } from "./extract.js"
9
+ import { packages } from "./shared.js"
10
+
11
+ Effect
12
+ .fn("effa-cli")(function*() {
13
+ const fs = yield* FileSystem.FileSystem
14
+ const path = yield* Path.Path
15
+ const extractExportMappings = yield* ExtractExportMappingsService
16
+
17
+ /**
18
+ * Executes a shell command using Node.js Command API with inherited stdio streams.
19
+ * The command is run through the system shell (/bin/sh) for proper command parsing.
20
+ *
21
+ * @param cmd - The shell command to execute
22
+ * @param cwd - Optional working directory to execute the command in
23
+ * @returns An Effect that succeeds with the exit code or fails with a PlatformError
24
+ */
25
+ const runNodeCommand = (cmd: string, cwd?: string) =>
26
+ NodeCommand
27
+ .make("sh", "-c", cmd)
28
+ .pipe(
29
+ NodeCommand.stdout("inherit"),
30
+ NodeCommand.stderr("inherit"),
31
+ cwd ? NodeCommand.workingDirectory(cwd) : identity,
32
+ NodeCommand.exitCode
33
+ )
34
+
35
+ /**
36
+ * Executes a bash script file using Node.js Command API with inherited stdio streams.
37
+ * The script file is executed directly through the shell (/bin/sh).
38
+ *
39
+ * @param file - The path to the bash script file to execute
40
+ * @param cwd - Optional working directory to execute the script in
41
+ * @returns An Effect that succeeds with the output or fails with a PlatformError
42
+ */
43
+ // const runBashFile = (file: string, cwd?: string) =>
44
+ // NodeCommand
45
+ // .make("sh", file)
46
+ // .pipe(
47
+ // NodeCommand.stdout("inherit"),
48
+ // NodeCommand.stderr("inherit"),
49
+ // cwd ? NodeCommand.workingDirectory(cwd) : identity,
50
+ // NodeCommand.string
51
+ // )
52
+
53
+ /**
54
+ * Creates a file if it doesn't exist or updates the access and modification times of an existing file.
55
+ * This is the effectful equivalent of the Unix `touch` command.
56
+ *
57
+ * @param path - The path to the file to touch
58
+ * @returns An Effect that succeeds with void or fails with a FileSystem error
59
+ */
60
+ const touch = Effect.fn("touch")(function*(path: string) {
61
+ const time = new Date()
62
+
63
+ yield* fs.utimes(path, time, time).pipe(
64
+ Effect.catchTag("SystemError", (err) =>
65
+ err.reason === "NotFound"
66
+ ? fs.writeFileString(path, "")
67
+ : Effect.fail(err))
68
+ )
69
+ })
70
+
71
+ /**
72
+ * Updates effect-app packages to their latest versions using npm-check-updates.
73
+ * Runs both at workspace root and recursively in all workspace packages.
74
+ */
75
+ const updateEffectAppPackages = Effect.fn("effa-cli.ue.updateEffectAppPackages")(function*() {
76
+ const filters = ["effect-app", "@effect-app/*"]
77
+ for (const filter of filters) {
78
+ yield* runNodeCommand(`pnpm exec ncu -u --filter "${filter}"`)
79
+ yield* runNodeCommand(`pnpm -r exec ncu -u --filter "${filter}"`)
80
+ }
81
+ })()
82
+
83
+ /**
84
+ * Updates Effect ecosystem packages to their latest versions using npm-check-updates.
85
+ * Covers core Effect packages, Effect ecosystem packages, and Effect Atom packages.
86
+ * Runs both at workspace root and recursively in all workspace packages.
87
+ */
88
+ const updateEffectPackages = Effect.fn("effa-cli.ue.updateEffectPackages")(function*() {
89
+ const effectFilters = ["effect", "@effect/*", "@effect-atom/*"]
90
+ for (const filter of effectFilters) {
91
+ yield* runNodeCommand(`pnpm exec ncu -u --filter "${filter}"`)
92
+ yield* runNodeCommand(`pnpm -r exec ncu -u --filter "${filter}"`)
93
+ }
94
+ })()
95
+
96
+ /**
97
+ * Links local effect-app packages by adding file resolutions to package.json.
98
+ * Updates the package.json with file: protocol paths pointing to the local effect-app-libs directory,
99
+ * then runs pnpm install to apply the changes.
100
+ *
101
+ * @param effectAppLibsPath - Path to the local effect-app-libs directory
102
+ * @returns An Effect that succeeds when linking is complete
103
+ */
104
+ const linkPackages = Effect.fnUntraced(function*(effectAppLibsPath: string) {
105
+ yield* Effect.log("Linking local effect-app packages...")
106
+
107
+ const packageJsonPath = "./package.json"
108
+ const packageJsonContent = yield* fs.readFileString(packageJsonPath)
109
+ const pj = JSON.parse(packageJsonContent)
110
+
111
+ const resolutions = {
112
+ ...pj.resolutions,
113
+ "@effect-app/eslint-codegen-model": "file:" + effectAppLibsPath + "/packages/eslint-codegen-model",
114
+ "effect-app": "file:" + effectAppLibsPath + "/packages/effect-app",
115
+ "@effect-app/infra": "file:" + effectAppLibsPath + "/packages/infra",
116
+ "@effect-app/vue": "file:" + effectAppLibsPath + "/packages/vue",
117
+ "@effect-app/vue-components": "file:" + effectAppLibsPath + "/packages/vue-components",
118
+ ...packages.reduce((acc, p) => ({ ...acc, [p]: `file:${effectAppLibsPath}/node_modules/${p}` }), {})
119
+ }
120
+
121
+ pj.resolutions = resolutions
122
+
123
+ yield* fs.writeFileString(packageJsonPath, JSON.stringify(pj, null, 2))
124
+ yield* Effect.log("Updated package.json with local file resolutions")
125
+
126
+ yield* runNodeCommand("pnpm i")
127
+
128
+ yield* Effect.log("Successfully linked local packages")
129
+ })
130
+
131
+ /**
132
+ * Unlinks local effect-app packages by removing file resolutions from package.json.
133
+ * Filters out all effect-app related file: protocol resolutions from package.json,
134
+ * then runs pnpm install to restore registry packages.
135
+ *
136
+ * @returns An Effect that succeeds when unlinking is complete
137
+ */
138
+ const unlinkPackages = Effect.fnUntraced(function*() {
139
+ yield* Effect.log("Unlinking local effect-app packages...")
140
+
141
+ const packageJsonPath = "./package.json"
142
+ const packageJsonContent = yield* fs.readFileString(packageJsonPath)
143
+ const pj = JSON.parse(packageJsonContent)
144
+
145
+ const filteredResolutions = Object.entries(pj.resolutions as Record<string, string>).reduce(
146
+ (acc, [k, v]) => {
147
+ if (k.startsWith("@effect-app/") || k === "effect-app" || packages.includes(k)) return acc
148
+ acc[k] = v
149
+ return acc
150
+ },
151
+ {} as Record<string, string>
152
+ )
153
+
154
+ pj.resolutions = filteredResolutions
155
+
156
+ yield* fs.writeFileString(packageJsonPath, JSON.stringify(pj, null, 2))
157
+ yield* Effect.log("Removed effect-app file resolutions from package.json")
158
+
159
+ yield* runNodeCommand("pnpm i")
160
+ yield* Effect.log("Successfully unlinked local packages")
161
+ })()
162
+
163
+ /**
164
+ * Monitors controller files for changes and runs eslint on related controllers.ts/routes.ts files.
165
+ * Watches for .controllers. files and triggers eslint fixes on parent directory's controller files.
166
+ *
167
+ * @param watchPath - The path to watch for controller changes
168
+ * @param debug - Whether to enable debug logging
169
+ * @returns An Effect that sets up controller file monitoring
170
+ */
171
+ const monitorChildIndexes = Effect.fn("effa-cli.index-multi.monitorChildIndexes")(
172
+ function*(watchPath: string, debug: boolean) {
173
+ const fileSystem = yield* FileSystem.FileSystem
174
+
78
175
  if (debug) {
79
- console.log("change!", evt, path, files)
176
+ yield* Effect.logInfo(`Starting controller monitoring for: ${watchPath}`)
80
177
  }
81
- cp.execSync(`cd api && pnpm eslint --fix ${files.map((_) => `"../${_}"`).join(" ")}`)
82
- break
178
+
179
+ const watchStream = fileSystem.watch(watchPath, { recursive: true })
180
+
181
+ yield* watchStream.pipe(
182
+ Stream.runForEach(
183
+ Effect.fn("effa-cli.monitorChildIndexes.handleEvent")(function*(event) {
184
+ const pathParts = event.path.split("/")
185
+ const fileName = pathParts[pathParts.length - 1]
186
+ const isController = fileName?.toLowerCase().includes(".controllers.")
187
+
188
+ if (!isController) return
189
+
190
+ let i = 1
191
+ const reversedParts = pathParts.toReversed()
192
+
193
+ while (i < reversedParts.length) {
194
+ const candidateFiles = ["controllers.ts", "routes.ts"]
195
+ .map((f) => [...pathParts.slice(0, pathParts.length - i), f].join("/"))
196
+
197
+ const existingFiles: string[] = []
198
+ for (const file of candidateFiles) {
199
+ const exists = yield* fileSystem.exists(file)
200
+ if (exists) existingFiles.push(file)
201
+ }
202
+
203
+ if (existingFiles.length > 0) {
204
+ if (debug) {
205
+ yield* Effect.logInfo(
206
+ `Controller change detected: ${event.path}, fixing files: ${existingFiles.join(", ")}`
207
+ )
208
+ }
209
+
210
+ const eslintArgs = existingFiles.map((f) => `"../${f}"`).join(" ")
211
+ yield* runNodeCommand(`cd api && pnpm eslint --fix ${eslintArgs}`)
212
+ break
213
+ }
214
+ i++
215
+ }
216
+ })
217
+ )
218
+ )
83
219
  }
84
- i++
85
- }
86
- })
87
- }
88
-
89
- function monitorRootIndexes(path: string, indexFile: string) {
90
- return w.default(path, (_, path) => {
91
- if (path.endsWith(indexFile)) return
92
- // const dirName = pathParts[pathParts.length - 2]!
93
- // console.log("change!", evt, path, dirName, indexFile)
94
- cp.execSync(`pnpm eslint --fix "${indexFile}"`)
95
- })
96
- }
97
-
98
- // TODO: cache, don't do things when it already existed before, so only file is updated, not created.
99
-
100
- const startDir = process.cwd()
101
-
102
- function packagejson(p: string, levels = 0) {
103
- const curDir = process.cwd()
104
- let r = ""
105
- // TODO: no chdir!
106
- try {
107
- process.chdir(path.resolve(startDir, p))
108
- r = cp.execSync(`sh ${p === "." ? "../.." : startDir}/scripts/extract.sh`, { encoding: "utf-8" })
109
- } finally {
110
- process.chdir(curDir)
111
- }
112
-
113
- const s = r.split("\n").sort((a, b) => a < b ? -1 : 1).join("\n")
114
- const items = JSON.parse(`{${s.substring(0, s.length - 1)} }`) as Record<string, unknown>
115
-
116
- const pkg = JSON.parse(fs.readFileSync(p + "/package.json", "utf-8"))
117
- const t = levels
118
- ? Object
119
- .keys(items)
120
- .filter((_) => _.split("/").length <= (levels + 1 /* `./` */))
121
- .reduce((prev, cur) => {
122
- prev[cur] = items[cur]
123
- return prev
124
- }, {} as Record<string, unknown>)
125
- : items
126
-
127
- const exps = {
128
- ...(fs.existsSync(p + "/src/index.ts")
129
- ? {
130
- ".": {
131
- "types": "./dist/index.d.ts",
132
- "default": "./dist/index.js"
220
+ )
221
+
222
+ /**
223
+ * Monitors a directory for changes and runs eslint on the specified index file.
224
+ * Triggers eslint fixes when any file in the directory changes (except the index file itself).
225
+ *
226
+ * @param watchPath - The path to watch for changes
227
+ * @param indexFile - The index file to run eslint on when changes occur
228
+ * @param debug - Whether to enable debug logging
229
+ * @returns An Effect that sets up root index monitoring
230
+ */
231
+ const monitorRootIndexes = Effect.fn("effa-cli.index-multi.monitorRootIndexes")(
232
+ function*(watchPath: string, indexFile: string, debug: boolean) {
233
+ const fileSystem = yield* FileSystem.FileSystem
234
+
235
+ if (debug) {
236
+ yield* Effect.logInfo(`Starting root index monitoring for: ${watchPath} -> ${indexFile}`)
133
237
  }
238
+
239
+ const watchStream = fileSystem.watch(watchPath)
240
+
241
+ yield* watchStream.pipe(
242
+ Stream.runForEach(
243
+ Effect.fn("effa-cli.index-multi.monitorRootIndexes.handleEvent")(function*(event) {
244
+ if (event.path.endsWith(indexFile)) return
245
+
246
+ if (debug) {
247
+ yield* Effect.logInfo(`Root change detected: ${event.path}, fixing: ${indexFile}`)
248
+ }
249
+
250
+ yield* runNodeCommand(`pnpm eslint --fix "${indexFile}"`)
251
+ })
252
+ )
253
+ )
134
254
  }
135
- : undefined),
136
- ...Object
137
- .keys(t)
138
- .reduce((prev, cur) => {
139
- if (cur !== "./index" && !cur.includes("/internal/")) prev[cur] = t[cur]
140
- return prev
141
- }, {} as Record<string, unknown>)
142
- // ...pkg.name === "effect-app" ? {
143
- // "./types/awesome": { "types": "./types/awesome.d.ts" }
144
- // } : {},
145
- }
146
- pkg.exports = exps
147
- fs.writeFileSync(p + "/package.json", JSON.stringify(pkg, null, 2))
148
- }
149
-
150
- function monitorPackagejson(path: string, levels = 0) {
151
- packagejson(path, levels)
152
- w.default(path + "/src", { recursive: true }, (_, __) => {
153
- packagejson(path, levels)
154
- })
155
- }
156
-
157
- function updateEffectAppPackages() {
158
- const filters = ["effect-app", "@effect-app/*"]
159
- for (const filter of filters) {
160
- cp.execSync(`pnpm exec ncu -u --filter "${filter}"`, { stdio: "inherit" })
161
- cp.execSync(`pnpm -r exec ncu -u --filter "${filter}"`, { stdio: "inherit" })
162
- }
163
- }
164
-
165
- function updateEffectPackages() {
166
- const effectFilters = ["effect", "@effect/*", "@effect-atom/*"]
167
- for (const filter of effectFilters) {
168
- cp.execSync(`pnpm exec ncu -u --filter "${filter}"`, { stdio: "inherit" })
169
- cp.execSync(`pnpm -r exec ncu -u --filter "${filter}"`, { stdio: "inherit" })
170
- }
171
-
172
- updateEffectAppPackages()
173
- }
174
-
175
- ;(async () => {
176
- let cmds = process.argv.slice(3).filter((_) => _ !== "--debug")
177
- switch (cmd) {
178
- case "link":
179
- await import("./link.js")
180
- break
181
- case "unlink":
182
- await import("./unlink.js")
183
- break
184
- case "watch": {
255
+ )
256
+
257
+ /**
258
+ * Sets up comprehensive index monitoring for a given path.
259
+ * Combines both child controller monitoring and root index monitoring.
260
+ *
261
+ * @param watchPath - The path to monitor
262
+ * @param debug - Whether to enable debug logging
263
+ * @returns An Effect that sets up all index monitoring for the path
264
+ */
265
+ const monitorIndexes = Effect.fn("effa-cli.index-multi.monitorIndexes")(
266
+ function*(watchPath: string, debug: boolean) {
267
+ const fileSystem = yield* FileSystem.FileSystem
268
+
269
+ if (debug) {
270
+ yield* Effect.logInfo(`Setting up index monitoring for path: ${watchPath}`)
271
+ }
272
+
273
+ const indexFile = watchPath + "/index.ts"
274
+
275
+ const monitors = [monitorChildIndexes(watchPath, debug)]
276
+
277
+ if (yield* fileSystem.exists(indexFile)) {
278
+ monitors.push(monitorRootIndexes(watchPath, indexFile, debug))
279
+ } else {
280
+ yield* Effect.logInfo(`Index file ${indexFile} does not exist`)
281
+ }
282
+
283
+ if (debug) {
284
+ yield* Effect.logInfo(`Starting ${monitors.length} monitor(s) for ${watchPath}`)
285
+ }
286
+
287
+ yield* Effect.all(monitors, { concurrency: monitors.length })
288
+ }
289
+ )
290
+
291
+ /**
292
+ * Watches directories for file changes and updates tsconfig.json and vite.config.ts accordingly.
293
+ * Monitors API resources and models directories for changes using Effect's native file watching.
294
+ *
295
+ * @returns An Effect that sets up file watching streams
296
+ */
297
+ const watcher = Effect.fn("watch")(function*(debug: boolean) {
298
+ yield* Effect.log("Watch API resources and models for changes")
299
+
185
300
  const dirs = ["../api/src/resources", "../api/src/models"]
186
301
  const viteConfigFile = "./vite.config.ts"
187
- const viteConfigExists = fs.existsSync(viteConfigFile)
188
- dirs.forEach((d) => {
189
- if (fs.existsSync(d)) {
190
- const files: string[] = []
191
- w.default(d, { recursive: true }, (t, f) => {
192
- // console.log("change!", d)
193
- touch("./tsconfig.json")
194
- if (viteConfigExists && t === "update" && !files.includes(f)) {
195
- // TODO: only on new files
196
- touch(viteConfigFile)
197
- files.push(f)
198
- }
199
- })
200
- }
201
- })
302
+ const fileSystem = yield* FileSystem.FileSystem
202
303
 
203
- break
204
- }
304
+ const viteConfigExists = yield* fileSystem.exists(viteConfigFile)
205
305
 
206
- case "index-multi": {
207
- ;[
208
- "./api/src"
209
- ]
210
- .filter(
211
- (_) => fs.existsSync(_)
212
- )
213
- .forEach(monitorIndexes)
214
- break
215
- }
216
-
217
- case "index": {
218
- monitorIndexes("./src")
219
- break
220
- }
221
-
222
- case "packagejson": {
223
- monitorPackagejson(".")
224
- break
225
- }
226
-
227
- case "packagejson-target": {
228
- const target = process.argv[3]!
229
- target.split(",").forEach((_) => monitorPackagejson(_, 1))
230
- cmds = process.argv.slice(4)
231
- break
232
- }
233
-
234
- case "packagejson-packages": {
235
- fs
236
- .readdirSync(startDir + "/packages")
237
- .map((_) => startDir + "/packages/" + _)
238
- .filter((_) =>
239
- fs.existsSync(_ + "/package.json")
240
- && fs.existsSync(_ + "/src")
241
- && !_.endsWith("eslint-codegen-model")
242
- && !_.endsWith("vue-components")
243
- )
244
- .forEach((_) => monitorPackagejson(_))
245
- break
246
- }
306
+ if (debug) {
307
+ yield* Effect.logInfo("watcher debug mode is enabled")
308
+ }
247
309
 
248
- case "sync": {
249
- console.log("Sync all snippets?")
310
+ // validate directories and filter out non-existing ones
311
+ const existingDirs: string[] = []
312
+ for (const dir of dirs) {
313
+ const dirExists = yield* fileSystem.exists(dir)
314
+ if (dirExists) {
315
+ existingDirs.push(dir)
316
+ } else {
317
+ yield* Effect.logWarning(`Directory ${dir} does not exist - skipping`)
318
+ }
319
+ }
250
320
 
251
- await askQuestion("Are you sure you want to sync snippets")
252
- await sync()
253
- return process.exit(0)
254
- }
321
+ if (existingDirs.length === 0) {
322
+ return yield* Effect.logWarning("No directories to watch - exiting")
323
+ }
255
324
 
256
- case "ncu:effect": {
257
- console.log("Updating effect & effect-app dependencies...")
325
+ // start watching all existing directories concurrently
326
+ const watchStreams = existingDirs.map((dir) =>
327
+ Effect.gen(function*() {
328
+ if (debug) {
329
+ yield* Effect.logInfo(`Starting to watch directory: ${dir}`)
330
+ }
258
331
 
259
- updateEffectPackages()
332
+ const files: string[] = []
333
+ const watchStream = fileSystem.watch(dir, { recursive: true })
334
+
335
+ yield* watchStream.pipe(
336
+ Stream.runForEach(
337
+ Effect.fn("effa-cli.watch.handleEvent")(function*(event) {
338
+ if (debug) {
339
+ yield* Effect.logInfo(`File ${event._tag.toLowerCase()}: ${event.path}`)
340
+ }
341
+
342
+ // touch tsconfig.json on any file change
343
+ yield* touch("./tsconfig.json")
344
+ if (debug) {
345
+ yield* Effect.logInfo("Updated tsconfig.json")
346
+ }
347
+
348
+ // touch vite config only on file updates (not creates/deletes)
349
+ if (
350
+ viteConfigExists
351
+ && event._tag === "Update"
352
+ && !files.includes(event.path)
353
+ ) {
354
+ yield* touch(viteConfigFile)
355
+ if (debug) {
356
+ yield* Effect.logInfo("Updated vite.config.ts")
357
+ }
358
+ files.push(event.path)
359
+ }
360
+ })
361
+ )
362
+ )
363
+ })
364
+ )
365
+
366
+ // run all watch streams concurrently
367
+ yield* Effect.all(watchStreams, { concurrency: existingDirs.length })
368
+ })
369
+
370
+ /**
371
+ * Updates a package.json file with generated exports mappings for TypeScript modules.
372
+ * Scans TypeScript source files and creates export entries that map module paths
373
+ * to their compiled JavaScript and TypeScript declaration files.
374
+ *
375
+ * @param startDir - The starting directory path for resolving relative paths
376
+ * @param p - The package directory path to process
377
+ * @param levels - Optional depth limit for export filtering (0 = no limit)
378
+ * @returns An Effect that succeeds when the package.json is updated
379
+ */
380
+ const packagejsonUpdater = Effect.fn("effa-cli.packagejsonUpdater")(
381
+ function*(startDir: string, p: string, levels = 0) {
382
+ yield* Effect.log(`Generating exports for ${p}`)
383
+
384
+ const exportMappings = yield* extractExportMappings(path.resolve(startDir, p))
385
+
386
+ // if exportMappings is empty skip export generation
387
+ if (exportMappings === "") {
388
+ yield* Effect.log(`No src directory found for ${p}, skipping export generation`)
389
+ return
390
+ }
260
391
 
261
- cp.execSync("pnpm i", { stdio: "inherit" })
392
+ const sortedExportEntries = JSON.parse(
393
+ `{ ${exportMappings} }`
394
+ ) as Record<
395
+ string,
396
+ unknown
397
+ >
398
+
399
+ const filteredExportEntries = levels
400
+ ? Object
401
+ .keys(sortedExportEntries)
402
+ // filter exports by directory depth - only include paths up to specified levels deep
403
+ .filter((_) => _.split("/").length <= (levels + 1 /* `./` */))
404
+ .reduce(
405
+ (prev, cur) => ({ ...prev, [cur]: sortedExportEntries[cur] }),
406
+ {} as Record<string, unknown>
407
+ )
408
+ : sortedExportEntries
409
+
410
+ const packageExports = {
411
+ ...((yield* fs.exists(p + "/src/index.ts"))
412
+ && {
413
+ ".": {
414
+ "types": "./dist/index.d.ts",
415
+ "default": "./dist/index.js"
416
+ }
417
+ }),
418
+ ...Object
419
+ .keys(filteredExportEntries)
420
+ .reduce(
421
+ (prev, cur) => ({
422
+ ...prev,
423
+ // exclude index files and internal modules from package exports:
424
+ // - skip "./index" to avoid conflicts with the main "." export
425
+ // - skip "/internal/" paths to keep internal modules private
426
+ ...cur !== "./index" && !cur.includes("/internal/") && { [cur]: filteredExportEntries[cur] }
427
+ }),
428
+ {} as Record<string, unknown>
429
+ )
430
+ }
262
431
 
263
- break
264
- }
432
+ const pkgJson = JSON.parse(yield* fs.readFileString(p + "/package.json", "utf-8"))
433
+ pkgJson.exports = packageExports
265
434
 
266
- case "ncu:effect-app": {
267
- console.log("Updating effect-app dependencies...")
435
+ yield* Effect.log(`Writing updated package.json for ${p}`)
268
436
 
269
- updateEffectAppPackages()
437
+ return yield* fs.writeFileString(
438
+ p + "/package.json",
439
+ JSON.stringify(pkgJson, null, 2)
440
+ )
441
+ }
442
+ )
270
443
 
271
- cp.execSync("pnpm i", { stdio: "inherit" })
444
+ /*
445
+ * CLI
446
+ */
272
447
 
273
- break
274
- }
275
- }
448
+ const EffectAppLibsPath = Args
449
+ .directory({
450
+ exists: "yes",
451
+ name: "effect-app-libs-path"
452
+ })
453
+ .pipe(
454
+ Args.withDefault("../../effect-app/libs"),
455
+ Args.withDescription("Path to the effect-app-libs directory")
456
+ )
457
+
458
+ const link = Command
459
+ .make(
460
+ "link",
461
+ { effectAppLibsPath: EffectAppLibsPath },
462
+ Effect.fn("effa-cli.link")(function*({ effectAppLibsPath }) {
463
+ return yield* linkPackages(effectAppLibsPath)
464
+ })
465
+ )
466
+ .pipe(Command.withDescription("Link local effect-app packages using file resolutions"))
467
+
468
+ const unlink = Command
469
+ .make(
470
+ "unlink",
471
+ {},
472
+ Effect.fn("effa-cli.unlink")(function*({}) {
473
+ return yield* unlinkPackages
474
+ })
475
+ )
476
+ .pipe(Command.withDescription("Remove effect-app file resolutions and restore npm registry packages"))
477
+
478
+ const ue = Command
479
+ .make(
480
+ "ue",
481
+ {},
482
+ Effect.fn("effa-cli.ue")(function*({}) {
483
+ yield* Effect.log("Update effect-app and/or effect packages")
484
+
485
+ const prompted = yield* Prompt.select({
486
+ choices: [
487
+ {
488
+ title: "effect-app",
489
+ description: "Update only effect-app packages",
490
+ value: "effect-app"
491
+ },
492
+ {
493
+ title: "effect",
494
+ description: "Update only effect packages",
495
+ value: "effect"
496
+ },
497
+ {
498
+ title: "both",
499
+ description: "Update both effect-app and effect packages",
500
+ value: "both"
501
+ }
502
+ ],
503
+ message: "Select an option"
504
+ })
276
505
 
277
- if (cmds.length) {
278
- const p = cp.spawn(cmds[0]!, cmds.slice(1), { stdio: "inherit" })
279
- p.on("close", (code) => process.exit(code ?? 0))
280
- p.on("exit", (code) => process.exit(code ?? 0))
281
- p.on("disconnect", () => process.exit(1))
282
- }
283
- })()
506
+ switch (prompted) {
507
+ case "effect-app":
508
+ return yield* updateEffectAppPackages.pipe(
509
+ Effect.andThen(runNodeCommand("pnpm i"))
510
+ )
511
+
512
+ case "effect":
513
+ return yield* updateEffectPackages.pipe(
514
+ Effect.andThen(runNodeCommand("pnpm i"))
515
+ )
516
+ case "both":
517
+ return yield* updateEffectPackages.pipe(
518
+ Effect.andThen(updateEffectAppPackages),
519
+ Effect.andThen(runNodeCommand("pnpm i"))
520
+ )
521
+ }
522
+ })
523
+ )
524
+ .pipe(Command.withDescription("Update effect-app and/or effect packages"))
525
+
526
+ const DebugOption = Options.boolean("debug").pipe(
527
+ Options.withAlias("d"),
528
+ Options.withDescription("Enable debug logging")
529
+ )
530
+
531
+ const watch = Command
532
+ .make(
533
+ "watch",
534
+ { debug: DebugOption },
535
+ Effect.fn("effa-cli.watch")(function*({ debug }) {
536
+ return yield* watcher(debug)
537
+ })
538
+ )
539
+ .pipe(
540
+ Command.withDescription(
541
+ "Watch API resources and models for changes and update tsconfig.json and vite.config.ts accordingly"
542
+ )
543
+ )
544
+
545
+ const indexMulti = Command
546
+ .make(
547
+ "index-multi",
548
+ { debug: DebugOption },
549
+ Effect.fn("effa-cli.index-multi")(function*({ debug }) {
550
+ yield* Effect.log("Starting multi-index monitoring")
551
+
552
+ const dirs = ["./api/src"]
553
+ const fileSystem = yield* FileSystem.FileSystem
554
+
555
+ const existingDirs: string[] = []
556
+ for (const dir of dirs) {
557
+ const dirExists = yield* fileSystem.exists(dir)
558
+ if (dirExists) {
559
+ existingDirs.push(dir)
560
+ } else {
561
+ yield* Effect.logWarning(`Directory ${dir} does not exist - skipping`)
562
+ }
563
+ }
564
+
565
+ if (existingDirs.length === 0) {
566
+ return yield* Effect.logWarning("No directories to monitor - exiting")
567
+ }
568
+
569
+ const monitors = existingDirs.map((dir) => monitorIndexes(dir, debug))
570
+ yield* Effect.all(monitors, { concurrency: monitors.length })
571
+ })
572
+ )
573
+ .pipe(
574
+ Command.withDescription(
575
+ "Monitor multiple directories for index and controller file changes"
576
+ )
577
+ )
578
+
579
+ const packagejson = Command
580
+ .make(
581
+ "packagejson",
582
+ {},
583
+ Effect.fn("effa-cli.packagejson")(function*({}) {
584
+ // https://nodejs.org/api/path.html#pathresolvepaths
585
+ const startDir = path.resolve()
586
+
587
+ return yield* packagejsonUpdater(startDir, ".")
588
+ })
589
+ )
590
+ .pipe(
591
+ Command.withDescription("Generate and update root-level package.json exports mappings for TypeScript modules")
592
+ )
593
+
594
+ const packagejsonPackages = Command
595
+ .make(
596
+ "packagejson-packages",
597
+ {},
598
+ Effect.fn("effa-cli.packagejson-packages")(function*({}) {
599
+ // https://nodejs.org/api/path.html#pathresolvepaths
600
+ const startDir = path.resolve()
601
+
602
+ const packagesDir = path.join(startDir, "packages")
603
+
604
+ const packagesExists = yield* fs.exists(packagesDir)
605
+ if (!packagesExists) {
606
+ return yield* Effect.logWarning("No packages directory found")
607
+ }
608
+
609
+ // get all package directories
610
+ const packageDirs = yield* fs.readDirectory(packagesDir)
611
+
612
+ const validPackages: string[] = []
613
+
614
+ // filter packages that have package.json and src directory
615
+ for (const packageName of packageDirs) {
616
+ const packagePath = path.join(packagesDir, packageName)
617
+ const packageJsonExists = yield* fs.exists(path.join(packagePath, "package.json"))
618
+ const srcExists = yield* fs.exists(path.join(packagePath, "src"))
619
+
620
+ const shouldExclude = false
621
+ || packageName.endsWith("eslint-codegen-model")
622
+ || packageName.endsWith("vue-components")
623
+
624
+ if (packageJsonExists && srcExists && !shouldExclude) {
625
+ validPackages.push(packagePath)
626
+ }
627
+ }
628
+
629
+ if (validPackages.length === 0) {
630
+ return yield* Effect.logWarning("No valid packages found to update")
631
+ }
632
+
633
+ yield* Effect.log(`Found ${validPackages.length} packages to update`)
634
+
635
+ // update each package sequentially
636
+ yield* Effect.all(
637
+ validPackages.map((packagePath) =>
638
+ Effect.gen(function*() {
639
+ const relativePackagePath = path.relative(startDir, packagePath)
640
+ yield* Effect.log(`Updating ${relativePackagePath}`)
641
+ return yield* packagejsonUpdater(startDir, relativePackagePath)
642
+ })
643
+ )
644
+ )
645
+
646
+ yield* Effect.log("All packages updated successfully")
647
+ })
648
+ )
649
+ .pipe(Command.withDescription("Generate and update package.json exports mappings for all packages in monorepo"))
650
+
651
+ // configure CLI
652
+ const cli = Command.run(
653
+ Command
654
+ .make("effa")
655
+ .pipe(Command.withSubcommands([
656
+ ue,
657
+ link,
658
+ unlink,
659
+ watch,
660
+ indexMulti,
661
+ packagejson,
662
+ packagejsonPackages
663
+ ])),
664
+ {
665
+ name: "Effect-App CLI by jfet97 ❤️",
666
+ version: "v1.0.0"
667
+ }
668
+ )
669
+
670
+ return yield* cli(process.argv)
671
+ })()
672
+ .pipe(
673
+ Effect.provide(NodeContext.layer),
674
+ NodeRuntime.runMain
675
+ )