@effect-app/cli 1.23.0 → 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,241 +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
- ] as const
31
- if (
32
- !supportedCommands.includes(_cmd as any)
33
- ) {
34
- console.log("unknown command: ", _cmd, "supported commands: ", supportedCommands.join(", "))
35
- process.exit(1)
36
- }
37
-
38
- const cmd = _cmd as typeof supportedCommands[number]
39
-
40
- const debug = process.argv.includes("--debug")
41
-
42
- function touch(path: string) {
43
- const time = new Date()
44
- try {
45
- fs.utimesSync(path, time, time)
46
- } catch (err) {
47
- fs.closeSync(fs.openSync(path, "w"))
48
- }
49
- }
50
-
51
- function* monitorIndexes_(path: string) {
52
- yield monitorChildIndexes(path)
53
- const indexFile = path + "/index.ts"
54
- if (fs.existsSync(indexFile)) {
55
- yield monitorRootIndexes(path, indexFile)
56
- }
57
- }
58
-
59
- function monitorIndexes(path: string) {
60
- return [...monitorIndexes_(path)]
61
- }
62
-
63
- function monitorChildIndexes(path: string) {
64
- return w.default(path, { recursive: true }, (evt, path) => {
65
- const pathParts = path.split("/")
66
- const isController = pathParts[pathParts.length - 1]?.toLowerCase().includes(".controllers.")
67
- if (!isController) return
68
-
69
- let i = 1
70
- const r = pathParts.toReversed()
71
- while (i < r.length) {
72
- const files = ["controllers.ts", "routes.ts"]
73
- .map((f) => [...pathParts.slice(0, pathParts.length - i), f].join("/"))
74
- .filter((f) => fs.existsSync(f))
75
- 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
+
175
+ if (debug) {
176
+ yield* Effect.logInfo(`Starting controller monitoring for: ${watchPath}`)
177
+ }
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
+ )
219
+ }
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
+
76
235
  if (debug) {
77
- console.log("change!", evt, path, files)
236
+ yield* Effect.logInfo(`Starting root index monitoring for: ${watchPath} -> ${indexFile}`)
78
237
  }
79
- cp.execSync(`cd api && pnpm eslint --fix ${files.map((_) => `"../${_}"`).join(" ")}`)
80
- break
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
+ )
81
254
  }
82
- i++
83
- }
84
- })
85
- }
86
-
87
- function monitorRootIndexes(path: string, indexFile: string) {
88
- return w.default(path, (_, path) => {
89
- if (path.endsWith(indexFile)) return
90
- // const dirName = pathParts[pathParts.length - 2]!
91
- // console.log("change!", evt, path, dirName, indexFile)
92
- cp.execSync(`pnpm eslint --fix "${indexFile}"`)
93
- })
94
- }
95
-
96
- // TODO: cache, don't do things when it already existed before, so only file is updated, not created.
97
-
98
- const startDir = process.cwd()
99
-
100
- function packagejson(p: string, levels = 0) {
101
- const curDir = process.cwd()
102
- let r = ""
103
- // TODO: no chdir!
104
- try {
105
- process.chdir(path.resolve(startDir, p))
106
- r = cp.execSync(`sh ${p === "." ? "../.." : startDir}/scripts/extract.sh`, { encoding: "utf-8" })
107
- } finally {
108
- process.chdir(curDir)
109
- }
110
-
111
- const s = r.split("\n").sort((a, b) => a < b ? -1 : 1).join("\n")
112
- const items = JSON.parse(`{${s.substring(0, s.length - 1)} }`) as Record<string, unknown>
113
-
114
- const pkg = JSON.parse(fs.readFileSync(p + "/package.json", "utf-8"))
115
- const t = levels
116
- ? Object
117
- .keys(items)
118
- .filter((_) => _.split("/").length <= (levels + 1 /* `./` */))
119
- .reduce((prev, cur) => {
120
- prev[cur] = items[cur]
121
- return prev
122
- }, {} as Record<string, unknown>)
123
- : items
124
-
125
- const exps = {
126
- ...(fs.existsSync(p + "/src/index.ts")
127
- ? {
128
- ".": {
129
- "types": "./dist/index.d.ts",
130
- "default": "./dist/index.js"
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`)
131
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
+
300
+ const dirs = ["../api/src/resources", "../api/src/models"]
301
+ const viteConfigFile = "./vite.config.ts"
302
+ const fileSystem = yield* FileSystem.FileSystem
303
+
304
+ const viteConfigExists = yield* fileSystem.exists(viteConfigFile)
305
+
306
+ if (debug) {
307
+ yield* Effect.logInfo("watcher debug mode is enabled")
132
308
  }
133
- : undefined),
134
- ...Object
135
- .keys(t)
136
- .reduce((prev, cur) => {
137
- if (cur !== "./index" && !cur.includes("/internal/")) prev[cur] = t[cur]
138
- return prev
139
- }, {} as Record<string, unknown>)
140
- // ...pkg.name === "effect-app" ? {
141
- // "./types/awesome": { "types": "./types/awesome.d.ts" }
142
- // } : {},
143
- }
144
- pkg.exports = exps
145
- fs.writeFileSync(p + "/package.json", JSON.stringify(pkg, null, 2))
146
- }
147
-
148
- function monitorPackagejson(path: string, levels = 0) {
149
- packagejson(path, levels)
150
- w.default(path + "/src", { recursive: true }, (_, __) => {
151
- packagejson(path, levels)
152
- })
153
- }
154
-
155
- let cmds = process.argv.slice(3).filter((_) => _ !== "--debug")
156
- switch (cmd) {
157
- case "link":
158
- await import("./link.js")
159
- break
160
- case "unlink":
161
- await import("./unlink.js")
162
- break
163
- case "watch": {
164
- const dirs = ["../api/src/resources", "../api/src/models"]
165
- const viteConfigFile = "./vite.config.ts"
166
- const viteConfigExists = fs.existsSync(viteConfigFile)
167
- dirs.forEach((d) => {
168
- if (fs.existsSync(d)) {
169
- const files: string[] = []
170
- w.default(d, { recursive: true }, (t, f) => {
171
- // console.log("change!", d)
172
- touch("./tsconfig.json")
173
- if (viteConfigExists && t === "update" && !files.includes(f)) {
174
- // TODO: only on new files
175
- touch(viteConfigFile)
176
- files.push(f)
309
+
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
+ }
320
+
321
+ if (existingDirs.length === 0) {
322
+ return yield* Effect.logWarning("No directories to watch - exiting")
323
+ }
324
+
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}`)
177
330
  }
331
+
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
+ )
178
363
  })
179
- }
364
+ )
365
+
366
+ // run all watch streams concurrently
367
+ yield* Effect.all(watchStreams, { concurrency: existingDirs.length })
180
368
  })
181
369
 
182
- break
183
- }
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
+ }
391
+
392
+ const sortedExportEntries = JSON.parse(
393
+ `{ ${exportMappings} }`
394
+ ) as Record<
395
+ string,
396
+ unknown
397
+ >
184
398
 
185
- case "index-multi": {
186
- ;[
187
- "./api/src"
188
- ]
189
- .filter(
190
- (_) => fs.existsSync(_)
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
+ }
431
+
432
+ const pkgJson = JSON.parse(yield* fs.readFileString(p + "/package.json", "utf-8"))
433
+ pkgJson.exports = packageExports
434
+
435
+ yield* Effect.log(`Writing updated package.json for ${p}`)
436
+
437
+ return yield* fs.writeFileString(
438
+ p + "/package.json",
439
+ JSON.stringify(pkgJson, null, 2)
440
+ )
441
+ }
442
+ )
443
+
444
+ /*
445
+ * CLI
446
+ */
447
+
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")
191
456
  )
192
- .forEach(monitorIndexes)
193
- break
194
- }
195
-
196
- case "index": {
197
- monitorIndexes("./src")
198
- break
199
- }
200
-
201
- case "packagejson": {
202
- monitorPackagejson(".")
203
- break
204
- }
205
-
206
- case "packagejson-target": {
207
- const target = process.argv[3]!
208
- target.split(",").forEach((_) => monitorPackagejson(_, 1))
209
- cmds = process.argv.slice(4)
210
- break
211
- }
212
-
213
- case "packagejson-packages": {
214
- fs
215
- .readdirSync(startDir + "/packages")
216
- .map((_) => startDir + "/packages/" + _)
217
- .filter((_) =>
218
- fs.existsSync(_ + "/package.json")
219
- && fs.existsSync(_ + "/src")
220
- && !_.endsWith("eslint-codegen-model")
221
- && !_.endsWith("vue-components")
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
+ })
222
465
  )
223
- .forEach((_) => monitorPackagejson(_))
224
- break
225
- }
226
-
227
- case "sync": {
228
- console.log("Sync all snippets?")
229
-
230
- await askQuestion("Are you sure you want to sync snippets")
231
- await sync()
232
- process.exit(0)
233
- }
234
- }
235
-
236
- if (cmds.length) {
237
- const p = cp.spawn(cmds[0]!, cmds.slice(1), { stdio: "inherit" })
238
- p.on("close", (code) => process.exit(code ?? 0))
239
- p.on("exit", (code) => process.exit(code ?? 0))
240
- p.on("disconnect", () => process.exit(1))
241
- }
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
+ })
505
+
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
+ )