@livestore/utils-dev 0.4.0 → 0.5.0-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/node/DockerCompose/DockerCompose.d.ts +52 -0
  3. package/dist/node/DockerCompose/DockerCompose.d.ts.map +1 -0
  4. package/dist/node/DockerCompose/DockerCompose.js +158 -0
  5. package/dist/node/DockerCompose/DockerCompose.js.map +1 -0
  6. package/dist/node/DockerCompose/DockerCompose.test.d.ts +2 -0
  7. package/dist/node/DockerCompose/DockerCompose.test.d.ts.map +1 -0
  8. package/dist/node/{DockerComposeService/DockerComposeService.test.js → DockerCompose/DockerCompose.test.js} +16 -16
  9. package/dist/node/DockerCompose/DockerCompose.test.js.map +1 -0
  10. package/dist/node/DockerCompose/mod.d.ts +2 -0
  11. package/dist/node/DockerCompose/mod.d.ts.map +1 -0
  12. package/dist/node/DockerCompose/mod.js +2 -0
  13. package/dist/node/DockerCompose/mod.js.map +1 -0
  14. package/dist/node/FileLogger.d.ts +15 -8
  15. package/dist/node/FileLogger.d.ts.map +1 -1
  16. package/dist/node/FileLogger.js +63 -79
  17. package/dist/node/FileLogger.js.map +1 -1
  18. package/dist/node/cmd-log.d.ts.map +1 -1
  19. package/dist/node/cmd-log.js +19 -8
  20. package/dist/node/cmd-log.js.map +1 -1
  21. package/dist/node/cmd.d.ts +10 -12
  22. package/dist/node/cmd.d.ts.map +1 -1
  23. package/dist/node/cmd.js +73 -36
  24. package/dist/node/cmd.js.map +1 -1
  25. package/dist/node/cmd.test.js +17 -17
  26. package/dist/node/cmd.test.js.map +1 -1
  27. package/dist/node/mod.d.ts +4 -5
  28. package/dist/node/mod.d.ts.map +1 -1
  29. package/dist/node/mod.js +58 -55
  30. package/dist/node/mod.js.map +1 -1
  31. package/dist/node/workspace.d.ts +2 -2
  32. package/dist/node/workspace.d.ts.map +1 -1
  33. package/dist/node/workspace.js +7 -7
  34. package/dist/node/workspace.js.map +1 -1
  35. package/dist/node-vitest/Vitest.d.ts +17 -8
  36. package/dist/node-vitest/Vitest.d.ts.map +1 -1
  37. package/dist/node-vitest/Vitest.js +5 -5
  38. package/dist/node-vitest/Vitest.js.map +1 -1
  39. package/dist/node-vitest/Vitest.test.d.ts +3 -5
  40. package/dist/node-vitest/Vitest.test.d.ts.map +1 -1
  41. package/dist/node-vitest/Vitest.test.js +14 -3
  42. package/dist/node-vitest/Vitest.test.js.map +1 -1
  43. package/dist/wrangler/WranglerDevServer.d.ts +15 -20
  44. package/dist/wrangler/WranglerDevServer.d.ts.map +1 -1
  45. package/dist/wrangler/WranglerDevServer.js +77 -71
  46. package/dist/wrangler/WranglerDevServer.js.map +1 -1
  47. package/dist/wrangler/WranglerDevServer.test.js +28 -28
  48. package/dist/wrangler/WranglerDevServer.test.js.map +1 -1
  49. package/dist/wrangler/mod.d.ts +1 -1
  50. package/dist/wrangler/mod.d.ts.map +1 -1
  51. package/dist/wrangler/mod.js +1 -1
  52. package/dist/wrangler/mod.js.map +1 -1
  53. package/package.json +19 -37
  54. package/src/node/{DockerComposeService/DockerComposeService.test.ts → DockerCompose/DockerCompose.test.ts} +18 -18
  55. package/src/node/DockerCompose/DockerCompose.ts +328 -0
  56. package/src/node/DockerCompose/mod.ts +1 -0
  57. package/src/node/FileLogger.ts +89 -114
  58. package/src/node/cmd-log.ts +21 -10
  59. package/src/node/cmd.test.ts +19 -17
  60. package/src/node/cmd.ts +117 -86
  61. package/src/node/mod.ts +112 -104
  62. package/src/node/workspace.ts +9 -10
  63. package/src/node-vitest/Vitest.test.ts +22 -3
  64. package/src/node-vitest/Vitest.ts +32 -42
  65. package/src/wrangler/WranglerDevServer.test.ts +33 -37
  66. package/src/wrangler/WranglerDevServer.ts +145 -129
  67. package/src/wrangler/mod.ts +1 -6
  68. package/dist/node/DockerComposeService/DockerComposeService.d.ts +0 -63
  69. package/dist/node/DockerComposeService/DockerComposeService.d.ts.map +0 -1
  70. package/dist/node/DockerComposeService/DockerComposeService.js +0 -149
  71. package/dist/node/DockerComposeService/DockerComposeService.js.map +0 -1
  72. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts +0 -2
  73. package/dist/node/DockerComposeService/DockerComposeService.test.d.ts.map +0 -1
  74. package/dist/node/DockerComposeService/DockerComposeService.test.js.map +0 -1
  75. package/src/node/DockerComposeService/DockerComposeService.ts +0 -343
  76. /package/src/node/{DockerComposeService → DockerCompose}/test-fixtures/docker-compose.yml +0 -0
@@ -0,0 +1,328 @@
1
+ import { objectToString, omitUndefineds } from '@livestore/utils'
2
+ import {
3
+ ChildProcess,
4
+ ChildProcessSpawner,
5
+ Context,
6
+ Duration,
7
+ Effect,
8
+ Fiber,
9
+ Layer,
10
+ type PlatformError,
11
+ Schedule,
12
+ Schema,
13
+ type Scope,
14
+ Stream,
15
+ } from '@livestore/utils/effect'
16
+
17
+ export class DockerComposeError extends Schema.TaggedError<DockerComposeError>(
18
+ '~@livestore/utils-dev/DockerComposeError',
19
+ )('DockerComposeError', {
20
+ cause: Schema.Defect(),
21
+ note: Schema.String,
22
+ /** Compose env of the failed invocation, if any — structured instead of string-formatted into `note`. */
23
+ env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
24
+ }) {}
25
+
26
+ export interface Options {
27
+ readonly cwd: string
28
+ readonly serviceName?: string
29
+ /** Unique project name to isolate this compose instance. If not provided, a random one is generated. */
30
+ readonly projectName?: string
31
+ }
32
+
33
+ export interface StartOptions {
34
+ readonly detached?: boolean
35
+ readonly env?: Record<string, string>
36
+ readonly healthCheck?: {
37
+ readonly url: string
38
+ readonly timeout?: Duration.Duration
39
+ readonly interval?: Duration.Duration
40
+ }
41
+ }
42
+
43
+ export interface LogsOptions {
44
+ readonly follow?: boolean
45
+ readonly tail?: number
46
+ readonly since?: string
47
+ }
48
+
49
+ export interface Service {
50
+ readonly pull: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
51
+ readonly start: (
52
+ options?: StartOptions,
53
+ ) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
54
+ readonly stop: Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
55
+ readonly down: (options?: {
56
+ readonly env?: Record<string, string>
57
+ readonly volumes?: boolean
58
+ readonly removeOrphans?: boolean
59
+ }) => Effect.Effect<void, DockerComposeError | PlatformError.PlatformError>
60
+ readonly logs: (
61
+ options?: LogsOptions,
62
+ ) => Stream.Stream<string, DockerComposeError | PlatformError.PlatformError, Scope.Scope>
63
+ /** The unique project name used to isolate this compose instance */
64
+ readonly projectName: string
65
+ }
66
+
67
+ const generateProjectName = (): string => `ls-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
68
+
69
+ export class DockerCompose extends Context.Service<DockerCompose, Service>()('@livestore/utils-dev/DockerCompose') {}
70
+
71
+ export const make = (args: Options) =>
72
+ Effect.gen(function* () {
73
+ const { cwd, serviceName } = args
74
+ const projectName = args.projectName ?? generateProjectName()
75
+
76
+ const commandExecutorContext = yield* Effect.context<ChildProcessSpawner.ChildProcessSpawner>()
77
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
78
+
79
+ const baseComposeArgs = ['-p', projectName]
80
+
81
+ const composeCommand = (args: ReadonlyArray<string>, options: ChildProcess.CommandOptions = {}) =>
82
+ ChildProcess.make('docker', ['compose', ...baseComposeArgs, ...args], { ...options, cwd })
83
+
84
+ const envOptions = (
85
+ env: Record<string, string> | undefined,
86
+ ): Pick<ChildProcess.CommandOptions, 'env' | 'extendEnv'> => (env === undefined ? {} : { env, extendEnv: true })
87
+
88
+ const pull = Effect.gen(function* () {
89
+ yield* Effect.log(`Pulling Docker Compose images in ${cwd}`)
90
+
91
+ const process = yield* spawner.spawn(composeCommand(['pull'], { stdout: 'pipe', stderr: 'pipe' }))
92
+
93
+ const stdoutFiber = yield* process.stdout.pipe(
94
+ Stream.decodeText({ encoding: 'utf8' }),
95
+ Stream.runFold(
96
+ () => '',
97
+ (acc, chunk) => acc + chunk,
98
+ ),
99
+ // Drain subprocess output immediately so the child cannot block while we wait for its exit code.
100
+ Effect.forkChild,
101
+ )
102
+
103
+ const stderrFiber = yield* process.stderr.pipe(
104
+ Stream.decodeText({ encoding: 'utf8' }),
105
+ Stream.runFold(
106
+ () => '',
107
+ (acc, chunk) => acc + chunk,
108
+ ),
109
+ // Drain subprocess output immediately so the child cannot block while we wait for its exit code.
110
+ Effect.forkChild,
111
+ )
112
+
113
+ const exitCode = yield* process.exitCode
114
+ const stdout = yield* Fiber.join(stdoutFiber)
115
+ const stderr = yield* Fiber.join(stderrFiber)
116
+
117
+ const exitCodeNumber = Number(exitCode)
118
+
119
+ if (exitCodeNumber !== 0) {
120
+ const stdoutLog = stdout.length > 0 ? stdout : '<empty stdout>'
121
+ const stderrLog = stderr.length > 0 ? stderr : '<empty stderr>'
122
+ const failureMessage = [
123
+ `Docker compose pull failed with exit code ${exitCodeNumber} in ${cwd}`,
124
+ `stdout:\n${stdoutLog}`,
125
+ `stderr:\n${stderrLog}`,
126
+ ].join('\n')
127
+
128
+ yield* Effect.logError(failureMessage)
129
+
130
+ return yield* new DockerComposeError({
131
+ cause: new Error(`Docker compose pull failed with exit code ${exitCodeNumber}`),
132
+ note: failureMessage,
133
+ })
134
+ }
135
+
136
+ yield* Effect.log(`Successfully pulled Docker Compose images`)
137
+ }).pipe(
138
+ Effect.retry({
139
+ schedule: Schedule.exponentialBackoff10Sec,
140
+ while: Schema.is(DockerComposeError),
141
+ }),
142
+ Effect.withSpan('pullDockerComposeImages'),
143
+ Effect.scoped,
144
+ )
145
+
146
+ const start = Effect.fn('startDockerCompose')(function* (options: StartOptions = {}) {
147
+ const { detached = true, healthCheck } = options
148
+
149
+ const startArgs = ['up']
150
+ if (detached === true) startArgs.push('-d')
151
+ if (serviceName !== undefined) startArgs.push(serviceName)
152
+
153
+ const process = yield* spawner
154
+ .spawn(composeCommand(startArgs, { ...envOptions(options.env), stderr: 'inherit', stdout: 'inherit' }))
155
+ .pipe(
156
+ Effect.mapError(
157
+ (cause) =>
158
+ new DockerComposeError({
159
+ cause,
160
+ note: `Failed to start Docker Compose services in ${cwd}`,
161
+ }),
162
+ ),
163
+ )
164
+
165
+ const exitCode = yield* process.exitCode
166
+
167
+ if (Number(exitCode) !== 0) {
168
+ return yield* new DockerComposeError({
169
+ cause: new Error(`Docker compose exited with code ${exitCode}`),
170
+ note: `Docker Compose failed to start with exit code ${exitCode}`,
171
+ env: options.env,
172
+ })
173
+ }
174
+
175
+ if (healthCheck !== undefined) {
176
+ yield* performHealthCheck(healthCheck).pipe(Effect.provide(commandExecutorContext))
177
+ }
178
+
179
+ yield* Effect.log(`Docker Compose services started successfully in ${cwd}`)
180
+ })
181
+
182
+ const stop = Effect.gen(function* () {
183
+ yield* Effect.log(`Stopping Docker Compose services in ${cwd}`)
184
+
185
+ const stopArgs = serviceName === undefined ? ['stop'] : ['stop', serviceName]
186
+ const exitCode = yield* spawner.exitCode(composeCommand(stopArgs))
187
+
188
+ if (Number(exitCode) !== 0) {
189
+ return yield* new DockerComposeError({
190
+ cause: new Error(`Docker compose stop exited with code ${exitCode}`),
191
+ note: `Failed to stop Docker Compose services`,
192
+ })
193
+ }
194
+
195
+ yield* Effect.log(`Docker Compose services stopped successfully`)
196
+ }).pipe(Effect.withSpan('stopDockerCompose'))
197
+
198
+ const logs = (options: LogsOptions = {}) =>
199
+ Effect.gen(function* () {
200
+ const { follow = false, tail, since } = options
201
+
202
+ const logsArgs = ['logs']
203
+ if (follow === true) logsArgs.push('-f')
204
+ if (tail !== undefined) logsArgs.push('--tail', tail.toString())
205
+ if (since !== undefined) logsArgs.push('--since', since)
206
+ if (serviceName !== undefined) logsArgs.push(serviceName)
207
+
208
+ const process = yield* spawner.spawn(composeCommand(logsArgs)).pipe(
209
+ Effect.mapError(
210
+ (cause) =>
211
+ new DockerComposeError({
212
+ cause,
213
+ note: `Failed to read Docker Compose logs in ${cwd}`,
214
+ }),
215
+ ),
216
+ )
217
+
218
+ return process.stdout.pipe(
219
+ Stream.decodeText({ encoding: 'utf8' }),
220
+ Stream.splitLines,
221
+ Stream.mapError(
222
+ (cause) =>
223
+ new DockerComposeError({
224
+ cause,
225
+ note: `Error reading Docker Compose logs in ${cwd}`,
226
+ }),
227
+ ),
228
+ )
229
+ }).pipe(Stream.unwrap)
230
+
231
+ const down = Effect.fn('downDockerCompose')(function* (options?: {
232
+ readonly env?: Record<string, string>
233
+ readonly volumes?: boolean
234
+ readonly removeOrphans?: boolean
235
+ }) {
236
+ yield* Effect.log(`Tearing down Docker Compose services in ${cwd}`)
237
+
238
+ const downArgs = ['down']
239
+ if (options?.volumes === true) downArgs.push('-v')
240
+ if (options?.removeOrphans === true) downArgs.push('--remove-orphans')
241
+ if (serviceName !== undefined) downArgs.push(serviceName)
242
+
243
+ const exitCode = yield* spawner.exitCode(composeCommand(downArgs, envOptions(options?.env)))
244
+
245
+ if (Number(exitCode) !== 0) {
246
+ return yield* new DockerComposeError({
247
+ cause: new Error(`Docker compose down exited with code ${exitCode}`),
248
+ note: `Failed to tear down Docker Compose services`,
249
+ })
250
+ }
251
+
252
+ yield* Effect.log(`Docker Compose services torn down successfully`)
253
+ })
254
+
255
+ // Register cleanup finalizer to ensure containers are removed when scope closes
256
+ yield* Effect.addFinalizer(() =>
257
+ down({ volumes: true, removeOrphans: true }).pipe(
258
+ Effect.tap(() => Effect.log(`Docker Compose cleanup completed for project ${projectName}`)),
259
+ Effect.catchCause((cause) =>
260
+ Effect.log('Docker Compose cleanup failed for project', projectName, objectToString(cause)),
261
+ ),
262
+ ),
263
+ )
264
+
265
+ return DockerCompose.of({ pull, start, stop, down, logs, projectName })
266
+ })
267
+
268
+ export const layer = (options: Options) => Layer.effect(DockerCompose, make(options))
269
+
270
+ const performHealthCheck = ({
271
+ url,
272
+ timeout = Duration.minutes(2),
273
+ interval = Duration.seconds(2),
274
+ }: {
275
+ url: string
276
+ timeout?: Duration.Duration
277
+ interval?: Duration.Duration
278
+ }): Effect.Effect<void, DockerComposeError, ChildProcessSpawner.ChildProcessSpawner | Scope.Scope> =>
279
+ Effect.gen(function* () {
280
+ yield* Effect.log(`Performing health check on ${url}`)
281
+
282
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
283
+
284
+ const checkHealth = spawner.exitCode(ChildProcess.make('curl', ['-f', '-s', url])).pipe(
285
+ Effect.map((code: number) => code === 0),
286
+ Effect.orElseSucceed(() => false),
287
+ )
288
+
289
+ const healthCheck = checkHealth.pipe(
290
+ Effect.repeat({
291
+ while: (healthy: boolean) => healthy === false,
292
+ schedule: Schedule.fixed(interval),
293
+ }),
294
+ Effect.timeout(timeout),
295
+ Effect.mapError(
296
+ () =>
297
+ new DockerComposeError({
298
+ cause: new Error('Health check timeout'),
299
+ note: `Health check failed for ${url} after ${Duration.toMillis(timeout)}ms`,
300
+ }),
301
+ ),
302
+ )
303
+
304
+ yield* healthCheck
305
+ yield* Effect.log(`Health check passed for ${url}`)
306
+ })
307
+
308
+ // Convenience function for scoped Docker Compose operations with automatic cleanup
309
+ export const startDockerComposeServicesScoped = (
310
+ args: Options & {
311
+ healthCheck?: StartOptions['healthCheck']
312
+ },
313
+ ): Effect.Effect<
314
+ void,
315
+ DockerComposeError | PlatformError.PlatformError,
316
+ DockerCompose | ChildProcessSpawner.ChildProcessSpawner | Scope.Scope
317
+ > =>
318
+ Effect.gen(function* () {
319
+ const dockerCompose = yield* DockerCompose
320
+
321
+ // Start the services
322
+ yield* dockerCompose.start({
323
+ ...omitUndefineds({ healthCheck: args.healthCheck !== undefined ? args.healthCheck : undefined }),
324
+ })
325
+
326
+ // Add cleanup finalizer to the current scope
327
+ yield* Effect.addFinalizer(() => dockerCompose.stop.pipe(Effect.orDie))
328
+ })
@@ -0,0 +1 @@
1
+ export * as DockerCompose from './DockerCompose.ts'
@@ -1,48 +1,8 @@
1
- import * as fs from 'node:fs'
2
- import path from 'node:path'
3
1
  import util from 'node:util'
4
2
 
5
- import {
6
- Cause,
7
- Effect,
8
- FiberId,
9
- HashMap,
10
- Inspectable,
11
- Layer,
12
- List,
13
- Logger,
14
- type LogLevel,
15
- LogSpan,
16
- ReadonlyArray,
17
- } from '@livestore/utils/effect'
18
-
19
- export const makeFileLogger = (
20
- logFilePath: string,
21
- options?: {
22
- readonly threadName: string
23
- readonly colors?: boolean
24
- },
25
- ) =>
26
- Layer.unwrapScoped(
27
- Effect.gen(function* () {
28
- yield* Effect.sync(() => fs.mkdirSync(path.dirname(logFilePath), { recursive: true }))
29
-
30
- const logFile = yield* Effect.acquireRelease(
31
- Effect.sync(() => fs.openSync(logFilePath, 'a', 0o666)),
32
- (fd) => Effect.sync(() => fs.closeSync(fd)),
33
- )
34
-
35
- return Logger.replace(
36
- Logger.defaultLogger,
37
- prettyLoggerTty({
38
- colors: options?.colors ?? false,
39
- stderr: false,
40
- formatDate: (date) => `${defaultDateFormat(date)} ${options?.threadName ?? ''}`,
41
- onLog: (str) => fs.writeSync(logFile, str),
42
- }),
43
- )
44
- }),
45
- )
3
+ import { Redactable } from 'effect'
4
+
5
+ import { Cause, Inspectable, Logger, type LogLevel, References, ReadonlyArray } from '@livestore/utils/effect'
46
6
 
47
7
  const withColor = (text: string, ...colors: ReadonlyArray<string>) => {
48
8
  let out = ''
@@ -66,18 +26,18 @@ const colors = {
66
26
  bgBrightRed: '101',
67
27
  } as const
68
28
 
69
- const logLevelColors: Record<LogLevel.LogLevel['_tag'], ReadonlyArray<string>> = {
29
+ const logLevelColors: Record<LogLevel.LogLevel, ReadonlyArray<string>> = {
70
30
  None: [],
71
31
  All: [],
72
32
  Trace: [colors.gray],
73
33
  Debug: [colors.blue],
74
34
  Info: [colors.green],
75
- Warning: [colors.yellow],
35
+ Warn: [colors.yellow],
76
36
  Error: [colors.red],
77
37
  Fatal: [colors.bgBrightRed, colors.black],
78
38
  }
79
39
 
80
- export const defaultDateFormat = (date: Date): string =>
40
+ export const formatLogTime = (date: Date): string =>
81
41
  `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date
82
42
  .getSeconds()
83
43
  .toString()
@@ -91,7 +51,7 @@ export const structuredMessage = (u: unknown): unknown => {
91
51
  return String(u)
92
52
  }
93
53
  default: {
94
- return Inspectable.toJSON(u)
54
+ return Inspectable.toJson(u)
95
55
  }
96
56
  }
97
57
  }
@@ -115,93 +75,108 @@ const consoleLogToString = (...s: any[]) => {
115
75
  .join(' ')
116
76
  }
117
77
 
118
- export const prettyLoggerTty = (options: {
78
+ type PrettyLoggerOptions = {
119
79
  readonly colors: boolean
120
80
  readonly stderr: boolean
121
81
  readonly formatDate: (date: Date) => string
122
82
  readonly onLog?: (str: string) => void
83
+ }
84
+
85
+ export const formatPrettyLog = ({
86
+ annotations,
87
+ cause,
88
+ date,
89
+ fiberId,
90
+ logLevel,
91
+ message: message_,
92
+ options,
93
+ spans,
94
+ }: {
95
+ readonly annotations: Readonly<Record<string, unknown>>
96
+ readonly cause: Cause.Cause<unknown>
97
+ readonly date: Date
98
+ readonly fiberId: number
99
+ readonly logLevel: LogLevel.LogLevel
100
+ readonly message: unknown
101
+ readonly options: PrettyLoggerOptions
102
+ readonly spans: ReadonlyArray<readonly [label: string, timestamp: number]>
123
103
  }) => {
124
104
  const color = options.colors === true ? withColor : withColorNoop
125
- return Logger.make<unknown, string>(({ annotations, cause, date, fiberId, logLevel, message: message_, spans }) => {
126
- let str = ''
105
+ let str = ''
127
106
 
128
- const log = (...s: any[]) => {
129
- str += `${consoleLogToString(...s)}\n`
130
- options.onLog?.(str)
131
- }
107
+ const log = (...s: any[]) => {
108
+ str += `${consoleLogToString(...s)}\n`
109
+ options.onLog?.(str)
110
+ }
132
111
 
133
- const logIndented = (...s: any[]) => {
134
- str += `${consoleLogToString(...s).replace(/^/gm, ' ')}\n`
135
- options.onLog?.(str)
136
- }
112
+ const logIndented = (...s: any[]) => {
113
+ str += `${consoleLogToString(...s).replace(/^/gm, ' ')}\n`
114
+ options.onLog?.(str)
115
+ }
137
116
 
138
- const message = ReadonlyArray.ensure(message_)
117
+ const message = ReadonlyArray.ensure(message_)
139
118
 
140
- let firstLine =
141
- color(`[${options.formatDate(date)}]`, colors.white) +
142
- ` ${color(logLevel.label, ...logLevelColors[logLevel._tag])}` +
143
- ` (${FiberId.threadName(fiberId)})`
119
+ let firstLine =
120
+ color(`[${options.formatDate(date)}]`, colors.white) +
121
+ ` ${color(logLevel.toUpperCase(), ...logLevelColors[logLevel])}` +
122
+ ` (#${fiberId})`
144
123
 
145
- if (List.isCons(spans) === true) {
146
- const now = date.getTime()
147
- const render = LogSpan.render(now)
148
- for (const span of spans) {
149
- firstLine += ` ${render(span)}`
150
- }
124
+ if (spans.length > 0) {
125
+ const now = date.getTime()
126
+ for (const [label, timestamp] of spans) {
127
+ firstLine += ` ${label}=${now - timestamp}ms`
151
128
  }
129
+ }
152
130
 
153
- firstLine += ':'
154
- let messageIndex = 0
155
- if (message.length > 0) {
156
- const firstMaybeString = structuredMessage(message[0])
157
- if (typeof firstMaybeString === 'string') {
158
- firstLine += ` ${color(firstMaybeString, colors.bold, colors.cyan)}`
159
- messageIndex++
160
- }
131
+ firstLine += ':'
132
+ let messageIndex = 0
133
+ if (message.length > 0) {
134
+ const firstMaybeString = structuredMessage(message[0])
135
+ if (typeof firstMaybeString === 'string') {
136
+ firstLine += ` ${color(firstMaybeString, colors.bold, colors.cyan)}`
137
+ messageIndex++
161
138
  }
139
+ }
162
140
 
163
- log(firstLine)
164
- // if (!processIsBun) console.group()
141
+ log(firstLine)
142
+ // if (!processIsBun) console.group()
165
143
 
166
- if (Cause.isEmpty(cause) === false) {
167
- logIndented(Cause.pretty(cause, { renderErrorCause: true }))
168
- }
144
+ if (cause.reasons.length > 0) {
145
+ logIndented(Cause.pretty(cause))
146
+ }
169
147
 
170
- if (messageIndex < message.length) {
171
- for (; messageIndex < message.length; messageIndex++) {
172
- const msg = message[messageIndex]
173
- if (typeof msg === 'object' && msg !== null) {
174
- logIndented(
175
- util.inspect(structuredMessage(msg), {
176
- depth: 3,
177
- colors: false,
178
- compact: false,
179
- breakLength: 120,
180
- }),
181
- )
182
- } else {
183
- logIndented(Inspectable.redact(msg))
184
- }
148
+ if (messageIndex < message.length) {
149
+ for (; messageIndex < message.length; messageIndex++) {
150
+ const msg = message[messageIndex]
151
+ if (typeof msg === 'object' && msg !== null) {
152
+ logIndented(
153
+ util.inspect(structuredMessage(msg), {
154
+ depth: 3,
155
+ colors: false,
156
+ compact: false,
157
+ breakLength: 120,
158
+ }),
159
+ )
160
+ } else {
161
+ logIndented(Redactable.redact(msg))
185
162
  }
186
163
  }
164
+ }
187
165
 
188
- if (HashMap.size(annotations) > 0) {
189
- for (const [key, value] of annotations) {
190
- const formattedValue =
191
- typeof value === 'object' && value !== null
192
- ? util.inspect(structuredMessage(value), {
193
- depth: 3,
194
- colors: false,
195
- compact: false,
196
- breakLength: 120,
197
- })
198
- : Inspectable.redact(value)
199
- logIndented(color(`${key}:`, colors.bold, colors.white), formattedValue)
200
- }
201
- }
166
+ for (const [key, value] of Object.entries(annotations)) {
167
+ const formattedValue =
168
+ typeof value === 'object' && value !== null
169
+ ? util.inspect(structuredMessage(value), {
170
+ depth: 3,
171
+ colors: false,
172
+ compact: false,
173
+ breakLength: 120,
174
+ })
175
+ : Redactable.redact(value)
176
+ logIndented(color(`${key}:`, colors.bold, colors.white), formattedValue)
177
+ }
202
178
 
203
- // if (!processIsBun) console.groupEnd()
179
+ // if (!processIsBun) console.groupEnd()
204
180
 
205
- return str
206
- })
181
+ return str
207
182
  }
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
- import { isNotUndefined } from '@livestore/utils'
5
- import { Effect } from '@livestore/utils/effect'
4
+ import { Cause, Effect, Predicate } from '@livestore/utils/effect'
6
5
 
7
6
  export type TCmdLoggingOptions = {
8
7
  readonly logDir?: string
@@ -31,18 +30,27 @@ export const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<s
31
30
  const safeIso = new Date().toISOString().replaceAll(':', '-')
32
31
  const archivedBase = `${path.parse(logFileName).name}-${safeIso}.log`
33
32
  const archivedLog = path.join(archiveDir, archivedBase)
34
- yield* Effect.try(() => fs.renameSync(currentLogPath, archivedLog)).pipe(
35
- Effect.catchAll(() =>
36
- Effect.try(() => {
37
- fs.copyFileSync(currentLogPath, archivedLog)
38
- fs.truncateSync(currentLogPath, 0)
33
+ yield* Effect.try({
34
+ try: () => fs.renameSync(currentLogPath, archivedLog),
35
+ catch: (cause) => new Cause.UnknownError(cause),
36
+ }).pipe(
37
+ Effect.catch(() =>
38
+ Effect.try({
39
+ try: () => {
40
+ fs.copyFileSync(currentLogPath, archivedLog)
41
+ fs.truncateSync(currentLogPath, 0)
42
+ },
43
+ catch: (cause) => new Cause.UnknownError(cause),
39
44
  }),
40
45
  ),
41
46
  Effect.ignore,
42
47
  )
43
48
 
44
49
  // Prune archives to retain only the newest N
45
- yield* Effect.try(() => fs.readdirSync(archiveDir)).pipe(
50
+ yield* Effect.try({
51
+ try: () => fs.readdirSync(archiveDir),
52
+ catch: (cause) => new Cause.UnknownError(cause),
53
+ }).pipe(
46
54
  Effect.map((names) => names.filter((n) => n.endsWith('.log'))),
47
55
  Effect.map((names) =>
48
56
  names
@@ -51,7 +59,10 @@ export const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<s
51
59
  ),
52
60
  Effect.flatMap((entries) =>
53
61
  Effect.forEach(entries.slice(logRetention), (e) =>
54
- Effect.try(() => fs.unlinkSync(path.join(archiveDir, e.name))).pipe(Effect.ignore),
62
+ Effect.try({
63
+ try: () => fs.unlinkSync(path.join(archiveDir, e.name)),
64
+ catch: (cause) => new Cause.UnknownError(cause),
65
+ }).pipe(Effect.ignore),
55
66
  ),
56
67
  ),
57
68
  Effect.ignore,
@@ -71,7 +82,7 @@ export const applyLoggingToCommand: (
71
82
  ) => Effect.Effect<{ input: string | string[]; subshell: boolean; logPath?: string }> = Effect.fn('cmd.logging.apply')(
72
83
  function* (commandInput, options) {
73
84
  const asArray = Array.isArray(commandInput)
74
- const parts = asArray === true ? commandInput.filter(isNotUndefined) : undefined
85
+ const parts = asArray === true ? commandInput.filter(Predicate.isNotUndefined) : undefined
75
86
 
76
87
  const logPath = yield* prepareCmdLogging(options)
77
88