diff --git a/context/effect-4/recipes/schema-codec-members.md b/context/effect-4/recipes/schema-codec-members.md index fc6581074b..5c4f45bf64 100644 --- a/context/effect-4/recipes/schema-codec-members.md +++ b/context/effect-4/recipes/schema-codec-members.md @@ -76,6 +76,21 @@ The Date pair is another surviving-name trap: v3 `DateFromSelf` becomes v4 `Date wire schemas become v4 `DateFromString`. Keep those rewrites separate and follow the `schema-date` recipe for the wire contract. +## Pretty-printed JSON + +V4 has no pretty-print JSON codec. `Schema.fromJsonString(S)` accepts no formatting options and +serializes with plain `JSON.stringify`. When formatted JSON is a persisted wire format, encode the +value with the schema and stringify the encoded JSON value separately: + +```ts +const encoded = Schema.encodeSync(Config)(value) +const content = JSON.stringify(encoded, null, 2) +``` + +Use `Schema.encodeEffect` instead when the surrounding path handles schema failures as an Effect. +Preserve any existing trailing newline separately. Compare the resulting bytes against the v3 +baseline; accepting file churn is not a mechanical migration. + ## Affected inventory The measured codec/member heatmap is: diff --git a/packages/@overeng/megarepo/bin/mr.ts b/packages/@overeng/megarepo/bin/mr.ts index f00e6ab46e..55966342a4 100644 --- a/packages/@overeng/megarepo/bin/mr.ts +++ b/packages/@overeng/megarepo/bin/mr.ts @@ -68,10 +68,9 @@ const program = Effect.gen(function* () { metricsExportInterval: 1000, }) - yield* Cli.Command.run(mrCommand, { - name: 'mr', + yield* Cli.Command.runWith(mrCommand, { version, - })(rewriteHelpSubcommand(process.argv)).pipe( + })(rewriteHelpSubcommand(process.argv).slice(2)).pipe( Effect.scoped, CliVersion.enrichErrors, Effect.provideService(CliVersion, { name: 'mr', version }), diff --git a/packages/@overeng/megarepo/src/cli/cli.integration.test.ts b/packages/@overeng/megarepo/src/cli/cli.integration.test.ts index a23c0bc6bd..c4daf99477 100644 --- a/packages/@overeng/megarepo/src/cli/cli.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/cli.integration.test.ts @@ -7,7 +7,7 @@ import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Exit, Option, Schema } from 'effect' import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' @@ -15,6 +15,7 @@ import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' import { CONFIG_FILE_NAME_JSON, MegarepoConfig, validateMemberName } from '../lib/config.ts' +import { encodePrettyJson } from '../lib/json.ts' import { makeConsoleCapture } from '../test-utils/consoleCapture.ts' import { initGitRepo, readConfig } from '../test-utils/setup.ts' import { mrCommand } from './mod.ts' @@ -81,9 +82,7 @@ describe('mr init', () => { 'https://raw.githubusercontent.com/overengineeringstudio/megarepo/main/schema/megarepo.schema.json', members: {}, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(initialConfig) + const configContent = yield* encodePrettyJson(MegarepoConfig)(initialConfig) yield* fs.writeFileString(configPath, configContent + '\n') // Verify config was created @@ -119,9 +118,7 @@ describe('mr init', () => { workDir, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(existingConfig) + const configContent = yield* encodePrettyJson(MegarepoConfig)(existingConfig) yield* fs.writeFileString(configPath, configContent + '\n') // Verify existing config @@ -283,9 +280,7 @@ describe('mr add', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const initialConfig: MegarepoConfig = { members: {} } - const initialContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(initialConfig) + const initialContent = yield* encodePrettyJson(MegarepoConfig)(initialConfig) yield* fs.writeFileString(configPath, initialContent + '\n') // Add a member @@ -298,9 +293,7 @@ describe('mr add', () => { ...config, members: { ...config.members, [memberName]: memberSource }, } - const updatedContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(updatedConfig) + const updatedContent = yield* encodePrettyJson(MegarepoConfig)(updatedConfig) yield* fs.writeFileString(configPath, updatedContent + '\n') // Verify member was added @@ -339,9 +332,7 @@ describe('mr add', () => { ...config, members: { ...config.members, [customName]: memberSource }, } - const updatedContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(updatedConfig) + const updatedContent = yield* encodePrettyJson(MegarepoConfig)(updatedConfig) yield* fs.writeFileString(configPath, updatedContent + '\n') const finalConfig = yield* readConfig(workDir) @@ -379,9 +370,7 @@ describe('mr add', () => { const initialConfig: MegarepoConfig = { members: { effect: 'effect-ts/effect' }, } - const initialContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(initialConfig) + const initialContent = yield* encodePrettyJson(MegarepoConfig)(initialConfig) yield* fs.writeFileString(configPath, initialContent + '\n') // Check that member already exists @@ -428,7 +417,7 @@ describe('megarepo.json parsing', () => { local: './packages/local', }, } - const content = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + const content = yield* encodePrettyJson(MegarepoConfig)( config, ) yield* fs.writeFileString(configPath, content + '\n') @@ -465,7 +454,7 @@ describe('megarepo.json parsing', () => { vscode: { enabled: true, exclude: ['large-repo'] }, }, } - const content = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + const content = yield* encodePrettyJson(MegarepoConfig)( config, ) yield* fs.writeFileString(configPath, content + '\n') @@ -496,7 +485,7 @@ const runRootWithCwd = ({ cwdPath }: { cwdPath: string }) => const { consoleLayer, getStdoutLines } = yield* makeConsoleCapture const argv = ['node', 'mr', '--cwd', cwdPath, 'root', '--output', 'json'] - const effect = Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + const effect = Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provide(consoleLayer), ) const exit = yield* Effect.exit(effect) @@ -505,7 +494,7 @@ const runRootWithCwd = ({ cwdPath }: { cwdPath: string }) => let state: RootState | undefined if (stdout.trim() !== '') { - state = yield* Schema.decodeUnknown(Schema.fromJsonString(RootState))(stdout) + state = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(RootState))(stdout) } return { @@ -575,7 +564,7 @@ describe('--cwd option', () => { const cwdPath = '/nonexistent/path/that/does/not/exist/' const argv = ['node', 'mr', '--cwd', cwdPath, 'root', '--output', 'json'] - const effect = Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + const effect = Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provide(consoleLayer), ) const exit = yield* Effect.exit(effect) diff --git a/packages/@overeng/megarepo/src/cli/commands/check.ts b/packages/@overeng/megarepo/src/cli/commands/check.ts index 1969c502b5..fcd6e9d78e 100644 --- a/packages/@overeng/megarepo/src/cli/commands/check.ts +++ b/packages/@overeng/megarepo/src/cli/commands/check.ts @@ -4,6 +4,7 @@ import * as Cli from 'effect/unstable/cli' import { EffectPath } from '@overeng/effect-path' import { readMegarepoConfig } from '../../lib/config.ts' +import { encodePrettyJson } from '../../lib/json.ts' import { LOCK_FILE_NAME, readLockFile } from '../../lib/lock.ts' import { checkSourcePolicy, formatSourcePolicyViolation } from '../../lib/source-policy.ts' import { Cwd, findMegarepoRoot, jsonOption } from '../context.ts' @@ -11,7 +12,7 @@ import { CheckCommandError, LockFileRequiredError, NotInMegarepoError } from '.. import * as Observability from '../observability.ts' /** Encodes the structured check result as pretty-printed JSON for `--json` output. */ -const CheckReportJson = Schema.fromJsonString(Schema.Unknown, { space: 2 }) +const CheckReportJson = Schema.Unknown const allOption = Cli.Flag.boolean('all').pipe( Cli.Flag.withDescription('Check member source and lock files in repos/ as well as the root'), @@ -63,7 +64,7 @@ export const checkCommand = Cli.Command.make( } if (json === true) { - yield* Console.log(yield* Schema.encode(CheckReportJson)(result)) + yield* Console.log(yield* encodePrettyJson(CheckReportJson)(result)) } else if (result.violations.length === 0) { yield* Console.log('Megarepo checks OK') } else { diff --git a/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts b/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts index f4f3eec6ff..bc2585ce27 100644 --- a/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts +++ b/packages/@overeng/megarepo/src/cli/commands/config/push-refs.ts @@ -6,7 +6,7 @@ * Matching is done by canonical URL (org/repo), not by member name. */ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option } from 'effect' import * as Cli from 'effect/unstable/cli' import React from 'react' diff --git a/packages/@overeng/megarepo/src/cli/commands/engine.ts b/packages/@overeng/megarepo/src/cli/commands/engine.ts index b274aa8ae6..bd882f9ec3 100644 --- a/packages/@overeng/megarepo/src/cli/commands/engine.ts +++ b/packages/@overeng/megarepo/src/cli/commands/engine.ts @@ -8,10 +8,11 @@ import { Prompt } from 'effect/unstable/cli' import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' -import type { Terminal } from 'effect/Terminal' -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' -import { Clock, Effect, Option, type ParseResult } from 'effect' +import * as Terminal from 'effect/Terminal' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' +import { Clock, Effect, Option } from 'effect' +import * as SchemaError from 'effect/SchemaError' import React from 'react' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -126,9 +127,9 @@ export const syncMegarepo = ({ | StoreHygieneError | ConfigNotFoundError | PlatformError.PlatformError - | ParseResult.ParseError + | SchemaError.SchemaError | Error, - FileSystem.FileSystem | CommandExecutor.CommandExecutor | Store | StoreLock | R + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner | Store | StoreLock | R > => Effect.gen(function* () { const { mode, dryRun, force, all, only, skip, gitProtocol, createBranches } = options @@ -325,7 +326,7 @@ export const syncMegarepo = ({ if (linkTarget !== null) { if (dryRun === false) { - yield* fs.remove(entryPath).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.remove(entryPath).pipe(Effect.catch(() => Effect.void)) } return { name: entry, @@ -511,7 +512,7 @@ export const syncMegarepo = ({ ...(onMissingRef !== undefined ? { onMissingRef } : {}), }) }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.succeed({ root: nestedRoot, results: [ @@ -594,7 +595,7 @@ const createMissingRefPrompt = ( }) return yield* prompt.pipe( - Effect.catchTag('QuitException', () => Effect.succeed('abort' as const)), + Effect.catchTag('QuitError', () => Effect.succeed('abort' as const)), ) }) diff --git a/packages/@overeng/megarepo/src/cli/commands/ls.ts b/packages/@overeng/megarepo/src/cli/commands/ls.ts index 312a818214..cd66efa319 100644 --- a/packages/@overeng/megarepo/src/cli/commands/ls.ts +++ b/packages/@overeng/megarepo/src/cli/commands/ls.ts @@ -4,9 +4,10 @@ * List all members in the megarepo. */ -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' -import { Effect, Option, type ParseResult } from 'effect' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' +import { Effect, Option } from 'effect' +import * as SchemaError from 'effect/SchemaError' import * as Cli from 'effect/unstable/cli' import React from 'react' @@ -50,7 +51,7 @@ const scanMembersRecursive = ({ depth?: number }): Effect.Effect< MemberInfo[], - PlatformError.PlatformError | ParseResult.ParseError | Error, + PlatformError.PlatformError | SchemaError.SchemaError | Error, FileSystem.FileSystem > => Effect.gen(function* () { diff --git a/packages/@overeng/megarepo/src/cli/commands/status.ts b/packages/@overeng/megarepo/src/cli/commands/status.ts index 696635bade..c9ec174412 100644 --- a/packages/@overeng/megarepo/src/cli/commands/status.ts +++ b/packages/@overeng/megarepo/src/cli/commands/status.ts @@ -5,9 +5,10 @@ */ import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' -import { Clock, Effect, Option, type ParseResult } from 'effect' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' +import { Clock, Effect, Option } from 'effect' +import * as SchemaError from 'effect/SchemaError' import * as Cli from 'effect/unstable/cli' import React from 'react' @@ -65,8 +66,8 @@ const scanMembersRecursive = ({ depth?: number }): Effect.Effect< MemberStatus[], - PlatformError.PlatformError | ParseResult.ParseError | Error, - FileSystem.FileSystem | CommandExecutor.CommandExecutor | Store + PlatformError.PlatformError | SchemaError.SchemaError | Error, + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner | Store > => Effect.gen(function* () { const enterResult = yield* traversal.enterRoot({ root: megarepoRoot, depth }) @@ -404,7 +405,7 @@ export const statusCommand = Cli.Command.make( }) const memberRealPath = yield* fs .realPath(memberSymlinkPath.replace(/\/$/, '')) - .pipe(Effect.catchAll(() => Effect.void)) + .pipe(Effect.catch(() => Effect.void)) if (memberRealPath !== undefined) { const memberRealPathNorm = memberRealPath.replace(/\/$/, '') diff --git a/packages/@overeng/megarepo/src/cli/context.ts b/packages/@overeng/megarepo/src/cli/context.ts index 84ed25b777..a482b3bc53 100644 --- a/packages/@overeng/megarepo/src/cli/context.ts +++ b/packages/@overeng/megarepo/src/cli/context.ts @@ -6,7 +6,7 @@ import { resolve } from 'node:path' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Context, Effect, Layer, Option } from 'effect' import * as Cli from 'effect/unstable/cli' diff --git a/packages/@overeng/megarepo/src/cli/mod.ts b/packages/@overeng/megarepo/src/cli/mod.ts index da17578d64..d0020c3ae6 100644 --- a/packages/@overeng/megarepo/src/cli/mod.ts +++ b/packages/@overeng/megarepo/src/cli/mod.ts @@ -73,7 +73,6 @@ export const mrCommand = Cli.Command.make('mr', { cwd: cwdOption }).pipe( ) /** Exported CLI for external use */ -export const cli = Cli.Command.run(mrCommand, { - name: 'mr', +export const cli = Cli.Command.runWith(mrCommand, { version: MR_VERSION, -})(rewriteHelpSubcommand(process.argv)) +})(rewriteHelpSubcommand(process.argv).slice(2)) diff --git a/packages/@overeng/megarepo/src/cli/observability.ts b/packages/@overeng/megarepo/src/cli/observability.ts index f58f031a21..484de35b19 100644 --- a/packages/@overeng/megarepo/src/cli/observability.ts +++ b/packages/@overeng/megarepo/src/cli/observability.ts @@ -78,7 +78,7 @@ const trustOtelContract = ( effect: Effect.Effect, ): Effect.Effect => effect.pipe( - Effect.catchAll((error) => + Effect.catch((error) => typeof error === 'object' && error !== null && '_tag' in error && diff --git a/packages/@overeng/megarepo/src/cli/pin.integration.test.ts b/packages/@overeng/megarepo/src/cli/pin.integration.test.ts index 2cd22fd95c..e64d2eb19a 100644 --- a/packages/@overeng/megarepo/src/cli/pin.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/pin.integration.test.ts @@ -7,7 +7,7 @@ import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { expect } from 'vitest' @@ -19,6 +19,7 @@ import { MegarepoConfig, parseSourceString, } from '../lib/config.ts' +import { encodePrettyJson } from '../lib/json.ts' import { createLockedMember, LOCK_FILE_NAME, @@ -52,7 +53,7 @@ const createMinimalTestSetup = () => 'test-repo': 'test-owner/test-repo', }, } - const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + const configContent = yield* encodePrettyJson(MegarepoConfig)( config, ) yield* fs.writeFileString( @@ -101,9 +102,7 @@ describe('mr config pin', () => { workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) - const newConfigContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(updatedConfig) + const newConfigContent = yield* encodePrettyJson(MegarepoConfig)(updatedConfig) yield* fs.writeFileString(configPath, newConfigContent + '\n') // Verify the update @@ -135,7 +134,7 @@ describe('mr config pin', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config1)) + + (yield* encodePrettyJson(MegarepoConfig)(config1)) + '\n', ) diff --git a/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/schema.ts b/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/schema.ts index ee1575e246..cfc73ed639 100644 --- a/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/schema.ts +++ b/packages/@overeng/megarepo/src/cli/renderers/StatusOutput/schema.ts @@ -260,10 +260,10 @@ export type StatusState = Schema.Schema.Type * * Status is static output, so we only need SetState to populate the final result. */ -export const StatusAction = Schema.Union( +export const StatusAction = Schema.Union([ /** Replace entire state */ Schema.TaggedStruct('SetState', { state: StatusState }), -) +]) /** Inferred type for status actions. */ export type StatusAction = Schema.Schema.Type diff --git a/packages/@overeng/megarepo/src/cli/status.integration.test.ts b/packages/@overeng/megarepo/src/cli/status.integration.test.ts index 92abdde009..709ec17645 100644 --- a/packages/@overeng/megarepo/src/cli/status.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/status.integration.test.ts @@ -9,7 +9,7 @@ import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Exit, Schema } from 'effect' import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' @@ -17,6 +17,7 @@ import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' import { MegarepoConfig } from '../lib/config.ts' +import { encodePrettyJson } from '../lib/json.ts' import { createLockedMember, type LockFile, LOCK_FILE_NAME, writeLockFile } from '../lib/lock.ts' import { makeConsoleCapture } from '../test-utils/consoleCapture.ts' import { @@ -43,7 +44,7 @@ const runStatusCommand = ({ const { consoleLayer, getStdoutLines } = yield* makeConsoleCapture const argv = ['node', 'mr', '--cwd', cwd, 'status', '--output', 'json', ...args] - const effect = Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + const effect = Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provide(consoleLayer), ) const exit = yield* Effect.exit(effect) @@ -53,7 +54,7 @@ const runStatusCommand = ({ // Parse JSON output let status: StatusState | undefined if (stdout.trim() !== '') { - status = yield* Schema.decodeUnknown(Schema.fromJsonString(StatusState))(stdout) + status = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(StatusState))(stdout) } return { @@ -89,7 +90,7 @@ const createTestWorkspace = (args: { const config: MegarepoConfig = { members: args.members, } - const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + const configContent = yield* encodePrettyJson(MegarepoConfig)( config, ) yield* fs.writeFileString( @@ -533,9 +534,7 @@ describe('mr status --output json', () => { const writeConfig = (workspacePath: AbsoluteDirPath, members: Record) => Effect.gen(function* () { - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )({ members }) + const configContent = yield* encodePrettyJson(MegarepoConfig)({ members }) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile('megarepo.json')), `${configContent}\n`, diff --git a/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts b/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts index f9b721cbd0..f38675ef0e 100644 --- a/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/store-gc-cold.integration.test.ts @@ -28,8 +28,8 @@ import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { Clock, Effect, Exit, Layer, Schema } from 'effect' import * as Cli from 'effect/unstable/cli' import { expect, vi } from 'vitest' @@ -64,11 +64,12 @@ vi.setConfig({ const git = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - return (yield* Command.string(command)).trim() + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + return (yield* spawner.string(command)).trim() }) -const liveClock = Clock.make() +const liveClock = Clock.Clock.defaultValue() /** * Deterministic decision clock so grace/retention decisions are reproducible. @@ -76,13 +77,12 @@ const liveClock = Clock.make() * deadlines instead of firing immediately under the fixed decision time. */ const fixedClockLayer = (nowMs: number) => - Layer.setClock({ - [Clock.ClockTypeId]: Clock.ClockTypeId, + Layer.succeed(Clock.Clock, { currentTimeMillis: Effect.succeed(nowMs), currentTimeNanos: Effect.succeed(BigInt(nowMs) * 1_000_000n), sleep: (duration) => liveClock.sleep(duration), - unsafeCurrentTimeMillis: () => nowMs, - unsafeCurrentTimeNanos: () => BigInt(nowMs) * 1_000_000n, + currentTimeMillisUnsafe: () => nowMs, + currentTimeNanosUnsafe: () => BigInt(nowMs) * 1_000_000n, }) const StoreGcJsonOutput = Schema.Struct({ @@ -129,7 +129,7 @@ const runGc = ({ process.env['MEGAREPO_STORE'] = storePath const argv = ['node', 'mr', 'store', 'gc', ...args, '--output', 'json'] - const exit = yield* Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + const exit = yield* Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provideService(Cwd, cwd), Effect.provide( Layer.mergeAll(consoleLayer, makeStubPrStateResolverLayer(prRepos), fixedClockLayer(now)), diff --git a/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts b/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts index 96b1c3ab9d..7f0e2b0547 100644 --- a/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/store-gc-otel.integration.test.ts @@ -21,8 +21,8 @@ import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { Clock, Effect, Layer, Option, Ref, Schema } from 'effect' import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' @@ -44,14 +44,15 @@ const NOW = Date.parse('2026-06-11T12:00:00.000Z') const git = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - return (yield* Command.string(command)).trim() + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + return (yield* spawner.string(command)).trim() }) /** * Deterministic DECISION clock — `currentTime{Millis,Nanos}` are pinned to * `nowMs` so every grace/retention decision is reproducible — but `sleep` - * delegates to a REAL live clock (`Clock.make()`) instead of the cold test's + * delegates to a REAL live clock (`Clock.Clock.defaultValue()`) instead of the cold test's * `() => Effect.void`. * * Why real sleep here: this test runs the gc with the OTEL exporter active, and @@ -61,15 +62,14 @@ const git = (cwd: string, ...args: ReadonlyArray) => * still fixed) lets the infra timers tick on wall time while keeping gc decisions * deterministic — the root-cause fix, with no production workaround. */ -const liveClock = Clock.make() +const liveClock = Clock.Clock.defaultValue() const fixedClockLayer = (nowMs: number) => - Layer.setClock({ - [Clock.ClockTypeId]: Clock.ClockTypeId, + Layer.succeed(Clock.Clock, { currentTimeMillis: Effect.succeed(nowMs), currentTimeNanos: Effect.succeed(BigInt(nowMs) * 1_000_000n), sleep: (duration) => liveClock.sleep(duration), - unsafeCurrentTimeMillis: () => nowMs, - unsafeCurrentTimeNanos: () => BigInt(nowMs) * 1_000_000n, + currentTimeMillisUnsafe: () => nowMs, + currentTimeNanosUnsafe: () => BigInt(nowMs) * 1_000_000n, }) const REPO = { host: 'github.com', owner: 'acme', repo: 'widget' } as const @@ -129,7 +129,7 @@ const runGc = ({ process.env['MEGAREPO_STORE'] = storePath const argv = ['node', 'mr', 'store', 'gc', '--output', 'json'] - yield* Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + yield* Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provideService(Cwd, cwd), Effect.provideService(OtelConfig, { endpoint: telemetry }), Effect.provide( diff --git a/packages/@overeng/megarepo/src/cli/store.integration.test.ts b/packages/@overeng/megarepo/src/cli/store.integration.test.ts index 9591cf84b3..ebf417f86c 100644 --- a/packages/@overeng/megarepo/src/cli/store.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/store.integration.test.ts @@ -6,7 +6,7 @@ import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Exit, Option, Schema } from 'effect' import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' @@ -79,7 +79,7 @@ const runMrCommand = ({ ) const argv = ['node', 'mr', ...command] - const exit = yield* Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + const exit = yield* Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provideService(Cwd, cwd), Effect.provide(consoleLayer), Effect.exit, @@ -525,7 +525,7 @@ describe('store discovery is bounded to the layout', () => { .remove(EffectPath.ops.join(wt, EffectPath.unsafe.relativeFile('.git')), { recursive: true, }) - .pipe(Effect.catchAll(() => Effect.void)) + .pipe(Effect.catch(() => Effect.void)) for (const sub of ['node_modules/a/b/c', 'src/x/y', 'dist/p/q']) { yield* fs.makeDirectory( EffectPath.ops.join(wt, EffectPath.unsafe.relativeDir(`${sub}/`)), diff --git a/packages/@overeng/megarepo/src/cli/sync.integration.test.ts b/packages/@overeng/megarepo/src/cli/sync.integration.test.ts index b7482bfe1d..5088df0cb8 100644 --- a/packages/@overeng/megarepo/src/cli/sync.integration.test.ts +++ b/packages/@overeng/megarepo/src/cli/sync.integration.test.ts @@ -2,14 +2,15 @@ import { pathToFileURL } from 'node:url' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { FileSystem } from 'effect/FileSystem' -import { Cause, Chunk, Effect, Exit, Option, Schema } from 'effect' +import * as FileSystem from 'effect/FileSystem' +import { Cause, Effect, Exit, Option, Schema } from 'effect' import * as Cli from 'effect/unstable/cli' import { expect } from 'vitest' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' import { CONFIG_FILE_NAME_JSON, MegarepoConfig } from '../lib/config.ts' +import { encodePrettyJson } from '../lib/json.ts' import { checkLockStaleness, createEmptyLockFile, @@ -146,7 +147,7 @@ const runMrCommand = ({ ) const argv = ['node', 'mr', ...command, ...args] - const effect = Cli.Command.run(mrCommand, { name: 'mr', version: 'test' })(argv).pipe( + const effect = Cli.Command.runWith(mrCommand, { version: 'test' })(argv.slice(2)).pipe( Effect.provideService(Cwd, cwd), Effect.provide(consoleLayer), ) @@ -632,9 +633,7 @@ const createNestedMegarepoFixture = () => 'grandchild-lib': grandchildPath, }, } - const childConfigContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(childConfig) + const childConfigContent = yield* encodePrettyJson(MegarepoConfig)(childConfig) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), childConfigContent + '\n', @@ -658,9 +657,7 @@ const createNestedMegarepoFixture = () => 'child-megarepo': childPath, }, } - const parentConfigContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(parentConfig) + const parentConfigContent = yield* encodePrettyJson(MegarepoConfig)(parentConfig) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), parentConfigContent + '\n', @@ -702,7 +699,7 @@ describe('--all sync mode', () => { // Read parent config and verify it points to child const parentConfigContent = yield* fs.readFileString(parentConfigPath) - const parentConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( + const parentConfig = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))( parentConfigContent, ) expect(parentConfig.members['child-megarepo']).toBe(childPath) @@ -725,7 +722,7 @@ describe('--all sync mode', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const childConfigContent = yield* fs.readFileString(childConfigPath) - const childConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( + const childConfig = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))( childConfigContent, ) expect(childConfig.members['grandchild-lib']).toBe(grandchildPath) @@ -763,7 +760,7 @@ describe('--all nested error reporting', () => { yield* initGitRepo(childPath) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { bad: 'not-a-valid-source', }, @@ -780,7 +777,7 @@ describe('--all nested error reporting', () => { yield* initGitRepo(parentPath) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { child: childPath, }, @@ -806,7 +803,7 @@ describe('--all nested error reporting', () => { syncErrors: Schema.Array(SyncErrorItem), syncTree: MegarepoSyncTree, }) - const out = yield* Schema.decodeUnknown(Schema.fromJsonString(SyncOutput))( + const out = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(SyncOutput))( result.stdout.trim(), ) @@ -878,9 +875,7 @@ const createDiamondDependencyFixture = () => const childAConfig: MegarepoConfig = { members: { 'shared-lib': sharedLibPath }, } - const childAConfigContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(childAConfig) + const childAConfigContent = yield* encodePrettyJson(MegarepoConfig)(childAConfig) yield* fs.writeFileString( EffectPath.ops.join(childAPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), childAConfigContent + '\n', @@ -897,9 +892,7 @@ const createDiamondDependencyFixture = () => const childBConfig: MegarepoConfig = { members: { 'shared-lib': sharedLibPath }, } - const childBConfigContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(childBConfig) + const childBConfigContent = yield* encodePrettyJson(MegarepoConfig)(childBConfig) yield* fs.writeFileString( EffectPath.ops.join(childBPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), childBConfigContent + '\n', @@ -919,9 +912,7 @@ const createDiamondDependencyFixture = () => 'child-b': childBPath, }, } - const rootConfigContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(rootConfig) + const rootConfigContent = yield* encodePrettyJson(MegarepoConfig)(rootConfig) yield* fs.writeFileString( EffectPath.ops.join(rootPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), rootConfigContent + '\n', @@ -954,7 +945,7 @@ describe('--all sync deduplication', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const rootConfigContent = yield* fs.readFileString(rootConfigPath) - const rootConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( + const rootConfig = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))( rootConfigContent, ) expect(rootConfig.members['child-a']).toBe(childAPath) @@ -966,7 +957,7 @@ describe('--all sync deduplication', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const childAConfigContent = yield* fs.readFileString(childAConfigPath) - const childAConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( + const childAConfig = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))( childAConfigContent, ) expect(childAConfig.members['shared-lib']).toBe(sharedLibPath) @@ -976,7 +967,7 @@ describe('--all sync deduplication', () => { EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const childBConfigContent = yield* fs.readFileString(childBConfigPath) - const childBConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( + const childBConfig = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))( childBConfigContent, ) expect(childBConfig.members['shared-lib']).toBe(sharedLibPath) @@ -1002,7 +993,7 @@ const createPinnedStaleCommitPullFixture = (options?: { readonly useCommitRef?: yield* fs.makeDirectory(sourceRepoPath, { recursive: true }) yield* initGitRepo(sourceRepoPath) yield* runGitCommand(sourceRepoPath, 'checkout', '-b', 'main').pipe( - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), ) yield* fs.writeFileString( EffectPath.ops.join(sourceRepoPath, EffectPath.unsafe.relativeFile('README.md')), @@ -1020,7 +1011,7 @@ const createPinnedStaleCommitPullFixture = (options?: { readonly useCommitRef?: yield* runGitCommand(sourceRepoPath, 'push', '-u', 'origin', 'main') yield* runGitCommand(sourceRepoPath, 'checkout', '--orphan', 'rewritten-main') - yield* runGitCommand(sourceRepoPath, 'rm', '-rf', '.').pipe(Effect.catchAll(() => Effect.void)) + yield* runGitCommand(sourceRepoPath, 'rm', '-rf', '.').pipe(Effect.catch(() => Effect.void)) yield* fs.writeFileString( EffectPath.ops.join(sourceRepoPath, EffectPath.unsafe.relativeFile('README.md')), '# Rewritten history\n', @@ -1119,7 +1110,7 @@ const createNestedWorkspaceFixtureFromStore = (store: StoreFixtureResult) => yield* initGitRepo(childPath) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { shared: 'https://example.com/acme/shared#main', }, @@ -1146,7 +1137,7 @@ const createNestedWorkspaceFixtureFromStore = (store: StoreFixtureResult) => yield* initGitRepo(parentPath) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { shared: 'https://example.com/acme/shared#main', child: childPath, @@ -1204,7 +1195,7 @@ const createAliasWorkspaceFixture = () => EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON), ) const parentConfigContent = yield* fs.readFileString(parentConfigPath) - const parentConfig = yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))( + const parentConfig = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))( parentConfigContent, ) const updatedConfig = { @@ -1216,7 +1207,7 @@ const createAliasWorkspaceFixture = () => } yield* fs.writeFileString( parentConfigPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(updatedConfig)) + + (yield* encodePrettyJson(MegarepoConfig)(updatedConfig)) + '\n', ) @@ -1286,7 +1277,7 @@ const createNestedMegarepoLockRefMatchFixture = () => yield* initGitRepo(childPath) yield* fs.writeFileString( EffectPath.ops.join(childPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { 'shared-dev': 'https://example.com/acme/shared#dev', }, @@ -1313,7 +1304,7 @@ const createNestedMegarepoLockRefMatchFixture = () => yield* initGitRepo(parentPath) yield* fs.writeFileString( EffectPath.ops.join(parentPath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { 'shared-main': 'https://example.com/acme/shared#main', 'shared-dev': 'https://example.com/acme/shared#dev', @@ -1793,9 +1784,7 @@ describe('mr lock', () => { effect: 'effect-ts/effect', }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join( workspacePath, @@ -1875,7 +1864,7 @@ describe('mr lock', () => { ) yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + (yield* encodePrettyJson(MegarepoConfig)( initialConfig, )) + '\n', ) @@ -1911,7 +1900,7 @@ describe('mr lock', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + (yield* encodePrettyJson(MegarepoConfig)( updatedConfig, )) + '\n', ) @@ -1990,7 +1979,7 @@ describe('mr lock', () => { ) yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { 'my-lib': mainRepoPath }, })) + '\n', ) @@ -2008,7 +1997,7 @@ describe('mr lock', () => { // Update config to point to feature branch yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { 'my-lib': featureRepoPath }, })) + '\n', ) @@ -2220,7 +2209,7 @@ describe('mr lock', () => { ) yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { 'my-lib': mainRepoPath }, })) + '\n', ) @@ -2238,7 +2227,7 @@ describe('mr lock', () => { // Update config yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))({ + (yield* encodePrettyJson(MegarepoConfig)({ members: { 'my-lib': featureRepoPath }, })) + '\n', ) @@ -2509,7 +2498,7 @@ describe('mr fetch', () => { yield* initGitRepo(sourceRepoPath) // Force branch name to 'main' regardless of git config default yield* runGitCommand(sourceRepoPath, 'checkout', '-b', 'main').pipe( - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), ) yield* fs.writeFileString( EffectPath.ops.join(sourceRepoPath, EffectPath.unsafe.relativeFile('README.md')), @@ -2652,9 +2641,7 @@ describe('sync status types', () => { 'new-lib': localRepoPath, }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -2711,9 +2698,7 @@ describe('sync error handling', () => { 'non-existent-repo': pathToFileURL(missingRemotePath).href, }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -2803,9 +2788,7 @@ describe('sync member filtering', () => { repo2: repo2Path, }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join( workspacePath, @@ -2874,9 +2857,7 @@ describe('sync member filtering', () => { repo2: repo2Path, }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join( workspacePath, @@ -2928,9 +2909,7 @@ describe('sync member filtering', () => { repo1: 'owner/repo1', }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join( workspacePath, @@ -2954,7 +2933,7 @@ describe('sync member filtering', () => { expect(Exit.isFailure(result.exit)).toBe(true) if (Exit.isFailure(result.exit) === true) { const cause = result.exit.cause - const failureMessages = Chunk.toReadonlyArray(Cause.failures(cause)) + const failureMessages = cause.reasons.filter(Cause.isFailReason).map((reason) => reason.error) .map((error: unknown) => String(error)) .join('\n') expect(failureMessages.toLowerCase()).toContain('mutually exclusive') @@ -3027,9 +3006,7 @@ describe('sync worktree ref mismatch detection', () => { 'test-repo': 'https://example.com/org/test-repo#main', }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -3133,9 +3110,7 @@ describe('sync worktree ref mismatch detection', () => { 'test-repo': 'https://example.com/org/test-repo#main', }, } - const configContent = yield* Schema.encode( - Schema.fromJsonString(MegarepoConfig, { space: 2 }), - )(config) + const configContent = yield* encodePrettyJson(MegarepoConfig)(config) yield* fs.writeFileString( EffectPath.ops.join(workspacePath, EffectPath.unsafe.relativeFile(CONFIG_FILE_NAME_JSON)), configContent + '\n', @@ -3250,7 +3225,7 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + (yield* encodePrettyJson(MegarepoConfig)( initialConfig, )) + '\n', ) @@ -3283,7 +3258,7 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + (yield* encodePrettyJson(MegarepoConfig)( updatedConfig, )) + '\n', ) @@ -3360,7 +3335,7 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + (yield* encodePrettyJson(MegarepoConfig)( initialConfig, )) + '\n', ) @@ -3380,7 +3355,7 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + (yield* encodePrettyJson(MegarepoConfig)( updatedConfig, )) + '\n', ) @@ -3456,7 +3431,7 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config)) + + (yield* encodePrettyJson(MegarepoConfig)(config)) + '\n', ) yield* addCommit({ @@ -3527,7 +3502,7 @@ describe('sync member removal detection', () => { } yield* fs.writeFileString( configPath, - (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config)) + + (yield* encodePrettyJson(MegarepoConfig)(config)) + '\n', ) yield* addCommit({ diff --git a/packages/@overeng/megarepo/src/lib/config.ts b/packages/@overeng/megarepo/src/lib/config.ts index 1ec0eab2b7..e1fc58e3f0 100644 --- a/packages/@overeng/megarepo/src/lib/config.ts +++ b/packages/@overeng/megarepo/src/lib/config.ts @@ -14,7 +14,7 @@ */ import { Effect, JsonSchema, Option, Schema } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { EffectPath, @@ -261,7 +261,7 @@ export const readMegarepoConfig = (megarepoRoot: AbsoluteDirPath) => const content = yield* fs.readFileString(configPath) const format: ConfigFormat = fileName.endsWith('.kdl') === true ? 'kdl' : 'json' - const config = yield* Schema.decodeUnknown( + const config = yield* Schema.decodeUnknownEffect( format === 'kdl' ? MegarepoConfigFromKdl : Schema.fromJsonString(MegarepoConfig), )(content) @@ -288,8 +288,8 @@ export const writeMegarepoConfig = ({ const content = format === 'kdl' - ? yield* Schema.encode(MegarepoConfigFromKdl)(config) - : (yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))(config)) + '\n' + ? yield* Schema.encodeEffect(MegarepoConfigFromKdl)(config) + : JSON.stringify(yield* Schema.encodeEffect(MegarepoConfig)(config), null, 2) + '\n' yield* fs.writeFileString(configPath, content) }) diff --git a/packages/@overeng/megarepo/src/lib/generators/schema.ts b/packages/@overeng/megarepo/src/lib/generators/schema.ts index 8ef8689e68..02112c91e2 100644 --- a/packages/@overeng/megarepo/src/lib/generators/schema.ts +++ b/packages/@overeng/megarepo/src/lib/generators/schema.ts @@ -5,7 +5,7 @@ * Output: schema/megarepo.schema.json in the specified location. */ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect } from 'effect' import { diff --git a/packages/@overeng/megarepo/src/lib/generators/vscode.ts b/packages/@overeng/megarepo/src/lib/generators/vscode.ts index 6e3d278284..7df846a266 100644 --- a/packages/@overeng/megarepo/src/lib/generators/vscode.ts +++ b/packages/@overeng/megarepo/src/lib/generators/vscode.ts @@ -5,7 +5,7 @@ * Output: .vscode/megarepo.code-workspace in the megarepo root. */ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect } from 'effect' import { diff --git a/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts b/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts index cdcc1eba16..4e7336b0cb 100644 --- a/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/git-memory.integration.test.ts @@ -24,7 +24,7 @@ import { fileURLToPath } from 'node:url' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' -import { ChildProcess as Command } from 'effect/unstable/process' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' import { Effect, Schema } from 'effect' import { expect } from 'vitest' @@ -82,14 +82,14 @@ describe('git memory regression', () => { // only — it must stay generous because bun/JSC reserves a large virtual // address space regardless of resident set; the assertion below (on // resident growth) is the real bound, not this limit. - const probe = Command.make( - 'bash', + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const probe = Command.make('bash', [ '-c', 'ulimit -v 16777216; exec bun "$0" "$1"', probeScript, worktreePath, - ) - const stdout = yield* Command.string(probe) + ]) + const stdout = yield* spawner.string(probe) const result = decodeProbe(stdout.trim()) const growthKb = result.vmHwmKb - result.rssStartKb diff --git a/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts b/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts index 529ef3b13c..9334b152ae 100644 --- a/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/git-streaming-parsers.integration.test.ts @@ -11,8 +11,8 @@ * helper is proven separately by `git-memory.integration.test.ts`. */ -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect, Option } from 'effect' @@ -24,8 +24,9 @@ import * as Git from './git.ts' const git = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - return (yield* Command.string(command)).trim() + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + return (yield* spawner.string(command)).trim() }) const makeRepoDir = Effect.gen(function* () { diff --git a/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts b/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts index 34200b3040..878bee089b 100644 --- a/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/git-timeout.integration.test.ts @@ -14,8 +14,8 @@ * used to break for large members. */ -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' @@ -31,8 +31,9 @@ const GIT_USER = ['-c', 'user.email=test@example.com', '-c', 'user.name=Test Use /** Run git in `cwd`, returning trimmed stdout (fixture setup only). */ const git = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...GIT_USER, ...args).pipe(Command.workingDirectory(cwd)) - return (yield* Command.string(command)).trim() + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', [...GIT_USER, ...args], { cwd }) + return (yield* spawner.string(command)).trim() }) /** diff --git a/packages/@overeng/megarepo/src/lib/git.ts b/packages/@overeng/megarepo/src/lib/git.ts index 6db5f16ffd..610f41fcf8 100644 --- a/packages/@overeng/megarepo/src/lib/git.ts +++ b/packages/@overeng/megarepo/src/lib/git.ts @@ -4,7 +4,7 @@ * Provides Effect-wrapped git operations for cloning, fetching, and managing worktrees. */ -import { ChildProcess as Command } from 'effect/unstable/process' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' import { Cause, Chunk, Duration, Effect, Option, Schedule, Sink, Stream } from 'effect' import * as Observability from './observability.ts' @@ -215,23 +215,24 @@ const decodeChunks = (chunks: Chunk.Chunk): string => { */ const startGitProcess = ({ args, cwd }: { args: ReadonlyArray; cwd?: string }) => Effect.gen(function* () { - const cmd = Command.make('git', ...args).pipe( - cwd !== undefined ? Command.workingDirectory(cwd) : (x) => x, - Command.stderr('pipe'), - Command.stdout('pipe'), - ) + const cmd = Command.make('git', args, { + ...(cwd !== undefined ? { cwd } : {}), + stderr: 'pipe', + stdout: 'pipe', + }) - const process = yield* Command.start(cmd) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const process = yield* spawner.spawn(cmd) yield* Effect.addFinalizer((exit) => Effect.gen(function* () { - if (exit._tag !== 'Failure' || Cause.isInterruptedOnly(exit.cause) === false) { + if (exit._tag !== 'Failure' || Cause.hasInterruptsOnly(exit.cause) === false) { return } const isRunning = yield* process.isRunning.pipe(Effect.orElseSucceed(() => false)) if (isRunning === false) return - yield* process.kill('SIGKILL').pipe(Effect.catchAll(() => Effect.void)) + yield* process.kill('SIGKILL').pipe(Effect.catch(() => Effect.void)) }), ) return process diff --git a/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts b/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts index 5345d5a303..9af50e1bef 100644 --- a/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts +++ b/packages/@overeng/megarepo/src/lib/json-wire-baseline.test.ts @@ -3,10 +3,11 @@ import { describe, expect, it } from 'vitest' import { StoreState } from '../cli/renderers/StoreOutput/schema.ts' import { MegarepoConfig } from './config.ts' +import { encodePrettyJsonSync } from './json.ts' import { LockFile } from './lock.ts' const encodeJson = (schema: Schema.Schema, value: A): string => - Schema.encodeSync(Schema.fromJsonString(schema, { space: 2 }))(value) + encodePrettyJsonSync(schema, value) const decodeJson = (schema: Schema.Schema, encoded: string): A => Schema.decodeUnknownSync(Schema.fromJsonString(schema))(encoded) diff --git a/packages/@overeng/megarepo/src/lib/json.ts b/packages/@overeng/megarepo/src/lib/json.ts new file mode 100644 index 0000000000..571a536ad7 --- /dev/null +++ b/packages/@overeng/megarepo/src/lib/json.ts @@ -0,0 +1,13 @@ +import { Effect, Schema } from 'effect' + +export const encodePrettyJson = + (schema: Schema.ConstraintCodec) => + (value: A) => + Schema.encodeEffect(schema)(value).pipe( + Effect.map((encoded) => JSON.stringify(encoded, null, 2)), + ) + +export const encodePrettyJsonSync = ( + schema: Schema.ConstraintCodec, + value: A, +): string => JSON.stringify(Schema.encodeSync(schema)(value), null, 2) diff --git a/packages/@overeng/megarepo/src/lib/lock.ts b/packages/@overeng/megarepo/src/lib/lock.ts index 7ed3044b0e..45b1e263e4 100644 --- a/packages/@overeng/megarepo/src/lib/lock.ts +++ b/packages/@overeng/megarepo/src/lib/lock.ts @@ -9,13 +9,15 @@ * Note: Local path sources are NOT in the lock file - they're already local. */ -import type { Error as PlatformError } from 'effect' -import type { ParseResult } from 'effect' +import * as PlatformError from 'effect/PlatformError' +import * as SchemaError from 'effect/SchemaError' import { Effect, Option, Schema } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import type { AbsoluteFilePath } from '@overeng/effect-path' +import { encodePrettyJson } from './json.ts' + // ============================================================================= // Lock File Schema // ============================================================================= @@ -68,7 +70,7 @@ export const readLockFile = ( lockPath: AbsoluteFilePath, ): Effect.Effect< Option.Option, - PlatformError.PlatformError | ParseResult.ParseError, + PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { @@ -80,7 +82,7 @@ export const readLockFile = ( } const content = yield* fs.readFileString(lockPath) - const parsed = yield* Schema.decodeUnknown(Schema.fromJsonString(LockFile))(content) + const parsed = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(LockFile))(content) return Option.some(parsed) }) @@ -95,12 +97,12 @@ export const writeLockFile = ({ lockFile: LockFile }): Effect.Effect< void, - PlatformError.PlatformError | ParseResult.ParseError, + PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem - const content = yield* Schema.encode(Schema.fromJsonString(LockFile, { space: 2 }))(lockFile) + const content = yield* encodePrettyJson(LockFile)(lockFile) yield* fs.writeFileString(lockPath, content + '\n') }) diff --git a/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts b/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts index e4873b3fe4..cfeb0a683e 100644 --- a/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts +++ b/packages/@overeng/megarepo/src/lib/megarepo-traversal.ts @@ -6,9 +6,10 @@ * Raw traversal paths can grow forever in symlink cycles. */ -import type { Error as PlatformError } from 'effect' -import { Effect, Ref, Schema, type ParseResult } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import { Effect, Ref, Schema } from 'effect' +import * as SchemaError from 'effect/SchemaError' +import * as FileSystem from 'effect/FileSystem' import type { AbsoluteDirPath } from '@overeng/effect-path' @@ -64,7 +65,7 @@ export interface MegarepoTraversal { readonly depth: number }) => Effect.Effect< MegarepoTraversalEnterResult, - PlatformError.PlatformError | ParseResult.ParseError, + PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > readonly stats: Effect.Effect @@ -79,7 +80,7 @@ const canonicalizeRoot = Effect.fn('megarepo/traversal/canonicalize-root')(funct Effect.map(stripTrailingSlashesPreservingRoot), Effect.orElseSucceed(() => normalizedRoot), ) - const key = yield* Schema.decodeUnknown(MegarepoTraversalNodeKey)(resolvedRoot) + const key = yield* Schema.decodeUnknownEffect(MegarepoTraversalNodeKey)(resolvedRoot) return { key, resolvedRoot } }) diff --git a/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts b/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts index d5e8a5856c..14747591a7 100644 --- a/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts +++ b/packages/@overeng/megarepo/src/lib/nix-lock/input-discovery.ts @@ -5,8 +5,8 @@ * which megarepo members are referenced as inputs by other members. */ -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' import { Effect } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts b/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts index b647fac0e1..ba84d30d13 100644 --- a/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts +++ b/packages/@overeng/megarepo/src/lib/nix-lock/mod.ts @@ -8,17 +8,19 @@ * source of truth for dependency versions. */ -import type { Error as PlatformError } from 'effect' -import { Effect, Option, Schema, type ParseResult } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import { Effect, Option, Schema } from 'effect' +import * as SchemaError from 'effect/SchemaError' +import * as FileSystem from 'effect/FileSystem' import { ChildProcess as Command, - type ChildProcessSpawner as CommandExecutor, + ChildProcessSpawner as CommandExecutor, } from 'effect/unstable/process' import { EffectPath, type AbsoluteDirPath, type AbsoluteFilePath } from '@overeng/effect-path' import { findConfigPath, getMemberPath, type MegarepoConfig } from '../config.ts' +import { encodePrettyJson, encodePrettyJsonSync } from '../json.ts' import { LOCK_FILE_NAME, readLockFile, @@ -127,18 +129,12 @@ const DEVENV_LOCK = 'devenv.lock' const MEGAREPO_LOCK = LOCK_FILE_NAME /** Schema for raw flake lock JSON (used to preserve key order during manipulation) */ -const RawFlakeLockJson = Schema.fromJsonString( - Schema.mutable( - Schema.Struct({ - nodes: Schema.mutable( - Schema.Record(Schema.String, Schema.mutable(Schema.Record(Schema.String, Schema.Unknown))), - ), - root: Schema.String, - version: Schema.Number, - }), - ), - { space: 2 }, -) +const RawFlakeLock = Schema.Struct({ + nodes: Schema.Record(Schema.String, Schema.Record(Schema.String, Schema.Unknown)), + root: Schema.String, + version: Schema.Number, +}) +const RawFlakeLockJson = Schema.fromJsonString(RawFlakeLock) /** * Schema for an opaque JSON object (devenv.lock / flake.lock) whose nodes are @@ -146,14 +142,12 @@ const RawFlakeLockJson = Schema.fromJsonString( * with 2-space indentation, matching the prior `JSON.stringify(value, null, 2)` * output byte-for-byte. */ -const RawLockJson = Schema.fromJsonString( - Schema.mutable(Schema.Record(Schema.String, Schema.Unknown)), - { space: 2 }, -) +const RawLock = Schema.Record(Schema.String, Schema.Unknown) +const RawLockJson = Schema.fromJsonString(RawLock) /** Encode a manipulated lock object back to its 2-space JSON file content (with trailing newline). */ const encodeRawLockJson = (value: Record): string => - Schema.encodeSync(RawLockJson)(value) + '\n' + encodePrettyJsonSync(RawLock, value) + '\n' // ============================================================================= // Nix Metadata Fetching @@ -198,14 +192,15 @@ export const fetchNixFlakeMetadata = ({ rev: string }): Effect.Effect< NixFlakeMetadata, - PlatformError.PlatformError | ParseResult.ParseError | NixFlakeMetadataError, - CommandExecutor.CommandExecutor + PlatformError.PlatformError | SchemaError.SchemaError | NixFlakeMetadataError, + CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { + const executor = yield* CommandExecutor.ChildProcessSpawner const flakeRef = `github:${owner}/${repo}/${rev}` - const command = Command.make('nix', 'flake', 'prefetch', flakeRef, '--json') - const result = yield* Command.string(command) + const command = Command.make('nix', ['flake', 'prefetch', flakeRef, '--json']) + const result = yield* executor.string(command) // Check for empty output - this typically means the command failed. // Nix outputs errors to stderr and returns empty stdout with non-zero exit code. @@ -220,7 +215,7 @@ export const fetchNixFlakeMetadata = ({ } // Attempt to parse the JSON output - const parsed = yield* Schema.decodeUnknown(NixFlakePrefetchOutput)(result).pipe( + const parsed = yield* Schema.decodeUnknownEffect(NixFlakePrefetchOutput)(result).pipe( Effect.mapError((parseError) => { // Check if output looks like a nix error message (shouldn't normally happen // since nix errors go to stderr, but handle defensively) @@ -340,15 +335,15 @@ const syncSingleLockFile = ({ megarepoMembers: Record }): Effect.Effect< NixLockSyncFileResult, - PlatformError.PlatformError | ParseResult.ParseError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + PlatformError.PlatformError | SchemaError.SchemaError, + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem // Read and parse the lock file (Schema.parseJson handles both parsing and validation) const content = yield* fs.readFileString(lockPath) - const rawJson = yield* Schema.decodeUnknown(RawFlakeLockJson)(content) + const rawJson = yield* Schema.decodeUnknownEffect(RawFlakeLockJson)(content) // First pass: collect all nodes that need metadata fetching const nodesToUpdate: NodeUpdateInfo[] = [] @@ -498,7 +493,7 @@ const syncSingleLockFile = ({ // Write updated lock file if any changes were made if (updatedInputs.length > 0 || schemeNormalized === true) { - const updatedContent = yield* Schema.encode(RawFlakeLockJson)(rawJson) + const updatedContent = yield* encodePrettyJson(RawFlakeLock)(rawJson) yield* fs.writeFileString(lockPath, updatedContent + '\n') } @@ -526,7 +521,7 @@ const syncNestedMegarepoLockFile = ({ megarepoMembers: Record }): Effect.Effect< NixLockSyncFileResult, - PlatformError.PlatformError | ParseResult.ParseError, + PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { @@ -776,9 +771,9 @@ const validateSharedInputSource = ({ } const sourceContent = yield* fs.readFileString(sourceLockPath) - const sourceJson = yield* Schema.decodeUnknown(RawLockJson)(sourceContent).pipe( + const sourceJson = yield* Schema.decodeUnknownEffect(RawLockJson)(sourceContent).pipe( Effect.catchTag( - 'ParseError', + 'SchemaError', () => new SharedInputSourceError({ message: `Source member '${sourceMemberName}' has invalid devenv.lock (not valid JSON)`, @@ -1219,7 +1214,7 @@ export const syncNixLocks = Effect.fn('megarepo/nix-lock/sync')((options: NixLoc lockType: 'flake.lock', megarepoMembers, }).pipe( - Effect.catchTag('ParseError', (e) => + Effect.catchTag('SchemaError', (e) => Effect.gen(function* () { yield* Effect.logWarning(`Failed to parse ${flakeLockPath}: ${e.message}`) return { @@ -1247,7 +1242,7 @@ export const syncNixLocks = Effect.fn('megarepo/nix-lock/sync')((options: NixLoc lockType: 'devenv.lock', megarepoMembers, }).pipe( - Effect.catchTag('ParseError', (e) => + Effect.catchTag('SchemaError', (e) => Effect.gen(function* () { yield* Effect.logWarning(`Failed to parse ${devenvLockPath}: ${e.message}`) return { @@ -1277,7 +1272,7 @@ export const syncNixLocks = Effect.fn('megarepo/nix-lock/sync')((options: NixLoc lockPath: nestedMegarepoLockPath, megarepoMembers, }).pipe( - Effect.catchTag('ParseError', (e) => + Effect.catchTag('SchemaError', (e) => Effect.gen(function* () { yield* Effect.logWarning( `Failed to parse ${nestedMegarepoLockPath}: ${e.message}`, diff --git a/packages/@overeng/megarepo/src/lib/observability.ts b/packages/@overeng/megarepo/src/lib/observability.ts index e79f498974..13abd242b2 100644 --- a/packages/@overeng/megarepo/src/lib/observability.ts +++ b/packages/@overeng/megarepo/src/lib/observability.ts @@ -85,7 +85,7 @@ const trustOtelContract = ( effect: Effect.Effect, ): Effect.Effect => effect.pipe( - Effect.catchAll((error) => + Effect.catch((error) => typeof error === 'object' && error !== null && '_tag' in error && diff --git a/packages/@overeng/megarepo/src/lib/source-policy.ts b/packages/@overeng/megarepo/src/lib/source-policy.ts index 4407e52082..460953031f 100644 --- a/packages/@overeng/megarepo/src/lib/source-policy.ts +++ b/packages/@overeng/megarepo/src/lib/source-policy.ts @@ -1,5 +1,5 @@ -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' diff --git a/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts b/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts index 21db9af33a..548a950a07 100644 --- a/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/source-policy.unit.test.ts @@ -1,4 +1,4 @@ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { Effect, type Scope } from 'effect' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts b/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts index 02413cc2ef..a92462c377 100644 --- a/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-archive.integration.test.ts @@ -14,8 +14,8 @@ * contain `-`/`--`/`/`; only a trailing valid ISO8601 instant is a timestamp). */ -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect, Option } from 'effect' @@ -34,8 +34,9 @@ import { archiveWorktree, parseArchiveDirName, reapArchive, scanArchives } from const git = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - return (yield* Command.string(command)).trim() + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + return (yield* spawner.string(command)).trim() }) /** `/github.com///` repo root for a fixture repo key. */ diff --git a/packages/@overeng/megarepo/src/lib/store-archive.ts b/packages/@overeng/megarepo/src/lib/store-archive.ts index 7aa291180c..b92fed3f81 100644 --- a/packages/@overeng/megarepo/src/lib/store-archive.ts +++ b/packages/@overeng/megarepo/src/lib/store-archive.ts @@ -32,8 +32,8 @@ */ import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -164,7 +164,7 @@ export const archiveWorktree = (args: { }): Effect.Effect< ArchiveOutcome, Git.GitCommandError | PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -202,7 +202,7 @@ export const archiveWorktree = (args: { Effect.flatMap(() => Git.deleteBranch({ repoPath: args.bareRepoPath, branch: args.branch, force: true }), ), - Effect.catchAll((error) => + Effect.catch((error) => Effect.sync(() => { warnings.push( `branch '${args.branch}' could not be freed (re-add may fail until cleaned up): ${error.message}`, @@ -221,7 +221,7 @@ export const archiveWorktree = (args: { yield* fs.readFileString(readmePath).pipe( Effect.orElseSucceed(() => ''), Effect.flatMap((existing) => writeFileAtomic({ path: readmePath, content: existing + line })), - Effect.catchAll((error) => + Effect.catch((error) => Effect.sync(() => { warnings.push(`archive README metadata not recorded: ${error.message}`) }), @@ -260,7 +260,7 @@ export const archiveRefMismatchWorktree = (args: { }): Effect.Effect< ArchiveOutcome, Git.GitCommandError | PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -285,7 +285,7 @@ export const archiveRefMismatchWorktree = (args: { const warnings: Array = [] yield* Git.detachWorktreeHead({ worktreePath: destPath }).pipe( - Effect.catchAll((error) => + Effect.catch((error) => Effect.sync(() => { warnings.push( `archived worktree HEAD could not be detached (branch refs were preserved): ${error.message}`, @@ -302,7 +302,7 @@ export const archiveRefMismatchWorktree = (args: { yield* fs.readFileString(readmePath).pipe( Effect.orElseSucceed(() => ''), Effect.flatMap((existing) => writeFileAtomic({ path: readmePath, content: existing + line })), - Effect.catchAll((error) => + Effect.catch((error) => Effect.sync(() => { warnings.push(`archive README metadata not recorded: ${error.message}`) }), @@ -332,7 +332,7 @@ export const scanArchives = (args: { }): Effect.Effect< ReadonlyArray, Git.GitCommandError | PlatformError.PlatformError, - CommandExecutor.CommandExecutor + CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const archiveDir = archiveDirPath(args.repoRoot) @@ -383,7 +383,7 @@ export const reapArchive = (args: { }): Effect.Effect< void, Git.GitCommandError | PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem diff --git a/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts b/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts index 61baea22bc..33d7b5e896 100644 --- a/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts +++ b/packages/@overeng/megarepo/src/lib/store-fs-atomic.ts @@ -10,8 +10,8 @@ import { randomBytes } from 'node:crypto' -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' import { Effect } from 'effect' import { EffectPath, type AbsoluteFilePath } from '@overeng/effect-path' @@ -48,10 +48,10 @@ export const writeFileAtomic = ({ const tempPath = tempPathFor(path) yield* fs .writeFileString(tempPath, content) - .pipe(Effect.tapError(() => fs.remove(tempPath).pipe(Effect.catchAll(() => Effect.void)))) + .pipe(Effect.tapError(() => fs.remove(tempPath).pipe(Effect.catch(() => Effect.void)))) yield* fs .rename(tempPath, path) - .pipe(Effect.tapError(() => fs.remove(tempPath).pipe(Effect.catchAll(() => Effect.void)))) + .pipe(Effect.tapError(() => fs.remove(tempPath).pipe(Effect.catch(() => Effect.void)))) }).pipe( Observability.withLabelSpan({ name: 'megarepo/store/fs/write-atomic', diff --git a/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts index 5a26603123..2353fa66d2 100644 --- a/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-fs-atomic.unit.test.ts @@ -7,7 +7,7 @@ * no `.tmp-*` sibling lingering as garbage (the `tapError` cleanup branch). */ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' diff --git a/packages/@overeng/megarepo/src/lib/store-gc-config.ts b/packages/@overeng/megarepo/src/lib/store-gc-config.ts index 8c1a008986..fad94d8fcb 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-config.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-config.ts @@ -7,8 +7,8 @@ * unknown/invalid files fall back to the defaults (never fail the gc path). */ -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' import { Effect, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -84,7 +84,7 @@ export const loadStoreGcConfig = ({ const path = gcConfigPath(storeBasePath) const override = yield* fs.readFileString(path).pipe( Effect.flatMap((content) => - Schema.decodeUnknown(Schema.fromJsonString(StoreGcConfigOverride))(content), + Schema.decodeUnknownEffect(Schema.fromJsonString(StoreGcConfigOverride))(content), ), Effect.orElseSucceed(() => ({}) as StoreGcConfigOverride), ) diff --git a/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts index 2c81a589db..7b3be942ba 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-config.unit.test.ts @@ -1,4 +1,4 @@ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { it as effectIt } from '@effect/vitest' import { Effect } from 'effect' diff --git a/packages/@overeng/megarepo/src/lib/store-gc-observations.ts b/packages/@overeng/megarepo/src/lib/store-gc-observations.ts index 81d0a28b23..f3e7ba60e5 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-observations.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-observations.ts @@ -23,12 +23,14 @@ * conservatively re-arms all grace windows. */ -import type { Error as PlatformError } from 'effect' -import { Effect, Schema, type ParseResult } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import { Effect, Schema } from 'effect' +import * as SchemaError from 'effect/SchemaError' +import * as FileSystem from 'effect/FileSystem' import { EffectPath, type AbsoluteDirPath, type AbsoluteFilePath } from '@overeng/effect-path' +import { encodePrettyJson } from './json.ts' import * as Observability from './observability.ts' import { writeFileAtomic } from './store-fs-atomic.ts' @@ -87,7 +89,7 @@ export const readObservationLedger = ({ const path = ledgerPath(storeBasePath) return yield* fs.readFileString(path).pipe( Effect.flatMap((content) => - Schema.decodeUnknown(Schema.fromJsonString(GcObservationLedger))(content), + Schema.decodeUnknownEffect(Schema.fromJsonString(GcObservationLedger))(content), ), Effect.orElseSucceed(() => ({}) as GcObservationLedger), ) @@ -107,7 +109,7 @@ const writeObservationLedger = ({ ledger: GcObservationLedger }): Effect.Effect< void, - PlatformError.PlatformError | ParseResult.ParseError, + PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { @@ -115,7 +117,7 @@ const writeObservationLedger = ({ const path = ledgerPath(storeBasePath) const stateDir = EffectPath.ops.join(storeBasePath, EffectPath.unsafe.relativeDir('.state/')) yield* fs.makeDirectory(stateDir, { recursive: true }) - const content = yield* Schema.encode(Schema.fromJsonString(GcObservationLedger, { space: 2 }))( + const content = yield* encodePrettyJson(GcObservationLedger)( ledger, ) yield* writeFileAtomic({ path, content: content + '\n' }) @@ -145,7 +147,7 @@ export const recordObservations = ({ now: number }): Effect.Effect< GcObservationLedger, - PlatformError.PlatformError | ParseResult.ParseError, + PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { diff --git a/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts index b46cfd3602..faffe89357 100644 --- a/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-gc-observations.unit.test.ts @@ -1,4 +1,4 @@ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { Effect } from 'effect' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/megarepo/src/lib/store-hygiene.ts b/packages/@overeng/megarepo/src/lib/store-hygiene.ts index 8d37b2ff53..6dbaa37bc5 100644 --- a/packages/@overeng/megarepo/src/lib/store-hygiene.ts +++ b/packages/@overeng/megarepo/src/lib/store-hygiene.ts @@ -5,9 +5,9 @@ * Used by pre-flight checks (sync/lock/pin) and `mr store fix`. */ -import type { Error as PlatformError } from 'effect' +import * as PlatformError from 'effect/PlatformError' import { Effect, Option, Schema } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import type { ChildProcessSpawner as CommandExecutor } from 'effect/unstable/process' import { @@ -112,7 +112,7 @@ export const validateStoreMembers = ({ }): Effect.Effect< StoreIssue[], PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -279,7 +279,7 @@ export const runPreflightChecks = ({ }): Effect.Effect< void, StoreHygieneError | PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const issues = yield* validateStoreMembers({ @@ -363,7 +363,7 @@ export const fixStoreIssues = ({ }): Effect.Effect< FixResult[], PlatformError.PlatformError, - FileSystem.FileSystem | CommandExecutor.CommandExecutor + FileSystem.FileSystem | CommandExecutor.ChildProcessSpawner > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem @@ -447,13 +447,13 @@ export const fixStoreIssues = ({ // Remove existing broken worktree yield* fs .remove(worktreePath, { recursive: true }) - .pipe(Effect.catchAll(() => Effect.void)) + .pipe(Effect.catch(() => Effect.void)) // Recreate the worktree yield* Effect.gen(function* () { yield* fs .makeDirectory(worktreePath, { recursive: true }) - .pipe(Effect.catchAll(() => Effect.void)) + .pipe(Effect.catch(() => Effect.void)) const parsed = parseWorktreeRef(worktreePath) @@ -487,7 +487,7 @@ export const fixStoreIssues = ({ }) } }).pipe( - Effect.catchAll((err) => { + Effect.catch((err) => { results.push({ memberName: issue.memberName, issueType: issue.type, @@ -553,7 +553,7 @@ export const fixStoreIssues = ({ message: `cloned bare repo from ${cloneUrl}`, }) }).pipe( - Effect.catchAll((err) => { + Effect.catch((err) => { results.push({ memberName: issue.memberName, issueType: issue.type, diff --git a/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts b/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts index 39992ab190..e8750e74e3 100644 --- a/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-hygiene.unit.test.ts @@ -1,4 +1,4 @@ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { Effect, Option } from 'effect' import { describe, expect, it } from 'vitest' diff --git a/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts b/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts index 34d744de58..a3462d32f3 100644 --- a/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-liveness.integration.test.ts @@ -1,5 +1,5 @@ -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect, Option } from 'effect' @@ -26,8 +26,9 @@ const normalizePath = (path: string): string => path.replace(/\/+$/, '') const runGitCommand = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - const result = yield* Command.string(command) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + const result = yield* spawner.string(command) return result.trim() }) @@ -333,7 +334,7 @@ describe('store-liveness', () => { store, }).pipe(Effect.either) // Restore perms regardless of assertion outcome so scoped cleanup works. - yield* fs.chmod(reposDir, 0o755).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.chmod(reposDir, 0o755).pipe(Effect.catch(() => Effect.void)) // Re-break for the reconcile-all assertion below. yield* fs.chmod(reposDir, 0o000) expect(strictResult._tag).toBe('Left') @@ -345,7 +346,7 @@ describe('store-liveness', () => { reconcileAllWorkspaces: true, now: 1_700_000_002_000, }) - yield* fs.chmod(reposDir, 0o755).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.chmod(reposDir, 0o755).pipe(Effect.catch(() => Effect.void)) expect(reconciled.paths).toContain(normalizePath(mainWorktreePath)) expect([...reconciled.uncleanReconcilePaths]).toContain(normalizePath(mainWorktreePath)) diff --git a/packages/@overeng/megarepo/src/lib/store-liveness.ts b/packages/@overeng/megarepo/src/lib/store-liveness.ts index f95bae1231..14735f95a3 100644 --- a/packages/@overeng/megarepo/src/lib/store-liveness.ts +++ b/packages/@overeng/megarepo/src/lib/store-liveness.ts @@ -9,9 +9,10 @@ import { createHash } from 'node:crypto' -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' -import { Effect, Option, Schema, type ParseResult } from 'effect' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' +import { Effect, Option, Schema } from 'effect' +import * as SchemaError from 'effect/SchemaError' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -22,6 +23,7 @@ import { parseSourceString, readMegarepoConfig, } from './config.ts' +import { encodePrettyJson } from './json.ts' import { LOCK_FILE_NAME, readLockFile } from './lock.ts' import * as Observability from './observability.ts' import { writeFileAtomic } from './store-fs-atomic.ts' @@ -139,7 +141,7 @@ export const collectWorkspaceLivePaths = ({ strict?: boolean }): Effect.Effect< Set, - ConfigNotFoundError | PlatformError.PlatformError | ParseResult.ParseError, + ConfigNotFoundError | PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { @@ -205,7 +207,7 @@ export const collectWorkspaceLivePathsStrict = ({ store: MegarepoStore }): Effect.Effect< Set, - ConfigNotFoundError | PlatformError.PlatformError | ParseResult.ParseError, + ConfigNotFoundError | PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => collectWorkspaceLivePaths({ workspaceRoot, store, strict: true }) @@ -225,7 +227,7 @@ export const refreshWorkspaceRegistry = ({ now: number }): Effect.Effect< StoreWorkspaceRecord, - ConfigNotFoundError | PlatformError.PlatformError | ParseResult.ParseError, + ConfigNotFoundError | PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { @@ -240,7 +242,7 @@ export const refreshWorkspaceRegistry = ({ const registryDir = workspaceRegistryDir(store) yield* fs.makeDirectory(registryDir, { recursive: true }) - const content = yield* Schema.encode(Schema.fromJsonString(StoreWorkspaceRecord, { space: 2 }))( + const content = yield* encodePrettyJson(StoreWorkspaceRecord)( record, ) // Atomic (write-temp-then-rename): a concurrent reader (e.g. an under-lock @@ -286,7 +288,7 @@ const readRegistryRecords = ({ reconcile?: { now: number } | undefined }): Effect.Effect< RegistryReadResult, - ConfigNotFoundError | PlatformError.PlatformError | ParseResult.ParseError, + ConfigNotFoundError | PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { @@ -306,7 +308,7 @@ const readRegistryRecords = ({ const recordPath = EffectPath.ops.join(registryDir, EffectPath.unsafe.relativeFile(entry)) const parsed = yield* fs.readFileString(recordPath).pipe( Effect.flatMap((content) => - Schema.decodeUnknown(Schema.fromJsonString(StoreWorkspaceRecord))(content), + Schema.decodeUnknownEffect(Schema.fromJsonString(StoreWorkspaceRecord))(content), ), Effect.orElseSucceed(() => null), ) @@ -321,7 +323,7 @@ const readRegistryRecords = ({ // present-but-unreadable workspace must never be pruned. if (workspaceExists === false) { if (pruneStale === true) { - yield* fs.remove(recordPath).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.remove(recordPath).pipe(Effect.catch(() => Effect.void)) } continue } @@ -345,9 +347,7 @@ const readRegistryRecords = ({ updatedAt: new Date(reconcile.now).toISOString(), livePaths: [...reconciled.paths].toSorted(), } - const content = yield* Schema.encode( - Schema.fromJsonString(StoreWorkspaceRecord, { space: 2 }), - )(record) + const content = yield* encodePrettyJson(StoreWorkspaceRecord)(record) // Atomic rewrite so a concurrent reader never sees a torn record and // drops a live workspace's veto right before deletion (decision 0010). yield* writeFileAtomic({ path: recordPath, content: content + '\n' }) @@ -396,7 +396,7 @@ export const collectStoreLiveSet = ({ now?: number | undefined }): Effect.Effect< StoreLiveSet, - ConfigNotFoundError | PlatformError.PlatformError | ParseResult.ParseError, + ConfigNotFoundError | PlatformError.PlatformError | SchemaError.SchemaError, FileSystem.FileSystem > => Effect.gen(function* () { diff --git a/packages/@overeng/megarepo/src/lib/store-lock.ts b/packages/@overeng/megarepo/src/lib/store-lock.ts index 49f558eac4..83afe4f76c 100644 --- a/packages/@overeng/megarepo/src/lib/store-lock.ts +++ b/packages/@overeng/megarepo/src/lib/store-lock.ts @@ -119,7 +119,7 @@ const withoutPushAcquire = ( export const makeStoreLockLayerFromBacking = ( backingLayer: Layer.Layer, ) => - Layer.scoped( + Layer.effect( StoreLock, Effect.gen(function* () { const backingContext = withoutPushAcquire(yield* Layer.build(backingLayer)) @@ -136,7 +136,7 @@ export const makeStoreLockLayerFromBacking = ( * Lock files stored in {basePath}.locks/ directory. */ export const makeStoreLockLayer = (basePath: AbsoluteDirPath) => - Layer.scoped( + Layer.effect( StoreLock, Effect.gen(function* () { const lockDir = `${basePath}.locks` diff --git a/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts b/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts index 4e2b26a581..0d787426e3 100644 --- a/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts +++ b/packages/@overeng/megarepo/src/lib/store-lossless.integration.test.ts @@ -9,8 +9,8 @@ * not. */ -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' @@ -25,8 +25,9 @@ const GIT_USER = ['-c', 'user.email=test@example.com', '-c', 'user.name=Test Use /** Run git in `cwd`, returning trimmed stdout. */ const git = (cwd: string, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...GIT_USER, ...args).pipe(Command.workingDirectory(cwd)) - const result = yield* Command.string(command) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', [...GIT_USER, ...args], { cwd }) + const result = yield* spawner.string(command) return result.trim() }) @@ -39,11 +40,13 @@ const git = (cwd: string, ...args: ReadonlyArray) => */ const createStash = (worktreeCwd: string) => Effect.gen(function* () { - const command = Command.make('git', ...GIT_USER, 'stash').pipe( - Command.workingDirectory(worktreeCwd), - Command.env({ AGENT_POLICY_BYPASS: '1' }), - ) - yield* Command.string(command) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', [...GIT_USER, 'stash'], { + cwd: worktreeCwd, + env: { AGENT_POLICY_BYPASS: '1' }, + extendEnv: true, + }) + yield* spawner.string(command) }) /** diff --git a/packages/@overeng/megarepo/src/lib/store-pr-state.ts b/packages/@overeng/megarepo/src/lib/store-pr-state.ts index 9dd8ec0349..1a2dfb0bc8 100644 --- a/packages/@overeng/megarepo/src/lib/store-pr-state.ts +++ b/packages/@overeng/megarepo/src/lib/store-pr-state.ts @@ -188,14 +188,14 @@ export const makePrStateResolverLayer = ({ }: { limit?: number timeout?: Duration.DurationInput -} = {}): Layer.Layer => +} = {}): Layer.Layer => Layer.effect( PrStateResolver, Effect.gen(function* () { // Capture the executor once at layer build so the service's `resolve` // effects discharge their `CommandExecutor` requirement here (the live // shelling is an implementation detail, not part of the service R-channel). - const executor = yield* CommandExecutor.CommandExecutor + const executor = yield* CommandExecutor.ChildProcessSpawner /** repo `owner/repo` -> decoded PR rows (Option.none ⇒ resolved to no evidence). */ const repoCache = new Map>>() @@ -208,8 +208,7 @@ export const makePrStateResolverLayer = ({ repo: string }): Effect.Effect>> => Effect.gen(function* () { - const command = Command.make( - 'gh', + const command = Command.make('gh', [ 'pr', 'list', '--repo', @@ -220,15 +219,15 @@ export const makePrStateResolverLayer = ({ String(limit), '--json', 'number,state,headRefName,mergedAt,closedAt', - ) - const raw = yield* Command.string(command).pipe( + ]) + const raw = yield* executor.string(command).pipe( Effect.timeoutFail({ duration: timeout, onTimeout: () => new Error('gh pr list timed out'), }), // Any spawn/exec/timeout failure ⇒ no evidence (keep). Effect.option, - Effect.provideService(CommandExecutor.CommandExecutor, executor), + Effect.provideService(CommandExecutor.ChildProcessSpawner, executor), ) return Option.flatMap(raw, decodePrListJson) }) diff --git a/packages/@overeng/megarepo/src/lib/store.ts b/packages/@overeng/megarepo/src/lib/store.ts index d501e45e82..bfc5f9e42e 100644 --- a/packages/@overeng/megarepo/src/lib/store.ts +++ b/packages/@overeng/megarepo/src/lib/store.ts @@ -20,8 +20,8 @@ * ``` */ -import type { Error as PlatformError } from 'effect' -import { FileSystem } from 'effect/FileSystem' +import * as PlatformError from 'effect/PlatformError' +import * as FileSystem from 'effect/FileSystem' import { Context, Effect, Layer, Option } from 'effect' import { EffectPath, type AbsoluteDirPath, type RelativeDirPath } from '@overeng/effect-path' @@ -489,7 +489,7 @@ export const StoreLayer = Layer.effect( ).pipe((storeOnly) => { /* Derive basePath at provision time for the lock layer. * We read the env var again (same as storeOnly) so both use the same path. */ - const lockLayer = Layer.scoped( + const lockLayer = Layer.effect( StoreLock, Effect.gen(function* () { const store = yield* Store diff --git a/packages/@overeng/megarepo/src/lib/sync/member.ts b/packages/@overeng/megarepo/src/lib/sync/member.ts index 5c9acdd75e..47e6a19c7e 100644 --- a/packages/@overeng/megarepo/src/lib/sync/member.ts +++ b/packages/@overeng/megarepo/src/lib/sync/member.ts @@ -6,7 +6,7 @@ import path from 'node:path' -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' @@ -538,12 +538,12 @@ export const syncMember = ({ } yield* Observability.annotateSyncMemberAction('skip-dry-run') } else if (isFetchMode === true && dryRun === false) { - yield* Git.fetchBare({ repoPath: bareRepoPath }).pipe(Effect.catchAll(() => Effect.void)) + yield* Git.fetchBare({ repoPath: bareRepoPath }).pipe(Effect.catch(() => Effect.void)) yield* Observability.annotateSyncMemberAction('fetch') } else if (isApplyMode === true && targetCommit !== undefined && dryRun === false) { const commitExists = yield* Git.refExists({ repoPath: bareRepoPath, ref: targetCommit }) if (commitExists === false) { - yield* Git.fetchBare({ repoPath: bareRepoPath }).pipe(Effect.catchAll(() => Effect.void)) + yield* Git.fetchBare({ repoPath: bareRepoPath }).pipe(Effect.catch(() => Effect.void)) yield* Observability.annotateSyncMemberAction('fetch-missing-commit') } else { yield* Observability.annotateSyncMemberAction('noop') @@ -693,7 +693,7 @@ export const syncMember = ({ repoPath: bareRepoPath, ref: `refs/tags/${targetRef}`, }).pipe( - Effect.catchAll(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), + Effect.catch(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), ) } else if (refInfo.type === 'branch') { resolvedRefType = 'branch' @@ -701,7 +701,7 @@ export const syncMember = ({ repoPath: bareRepoPath, ref: `refs/remotes/origin/${targetRef}`, }).pipe( - Effect.catchAll(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), + Effect.catch(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), ) } else { const heuristicType = classifyRef(targetRef) @@ -711,14 +711,14 @@ export const syncMember = ({ repoPath: bareRepoPath, ref: `refs/tags/${targetRef}`, }).pipe( - Effect.catchAll(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), + Effect.catch(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), ) } else { resolvedCommit = yield* Git.resolveRef({ repoPath: bareRepoPath, ref: `refs/remotes/origin/${targetRef}`, }).pipe( - Effect.catchAll(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), + Effect.catch(() => Git.resolveRef({ repoPath: bareRepoPath, ref: targetRef })), ) } } @@ -844,7 +844,7 @@ export const syncMember = ({ branch: targetRef, createBranch: false, }).pipe( - Effect.catchAll(() => + Effect.catch(() => Git.createWorktree({ repoPath: bareRepoPath, worktreePath, @@ -1019,7 +1019,7 @@ export const syncMember = ({ } satisfies MemberSyncResult }).pipe( Effect.tap((result) => Observability.annotateSyncMemberResult(result.status)), - Effect.catchAll((error) => { + Effect.catch((error) => { // Interpret git errors to provide user-friendly messages if (error instanceof Git.GitCommandError) { const interpreted = Git.interpretGitError(error) diff --git a/packages/@overeng/megarepo/src/test-utils/setup.ts b/packages/@overeng/megarepo/src/test-utils/setup.ts index 8e9abafab7..73148878a5 100644 --- a/packages/@overeng/megarepo/src/test-utils/setup.ts +++ b/packages/@overeng/megarepo/src/test-utils/setup.ts @@ -6,13 +6,14 @@ import os from 'node:os' -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { Effect, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' import { MegarepoConfig } from '../lib/config.ts' +import { encodePrettyJson } from '../lib/json.ts' // ============================================================================= // Types @@ -55,8 +56,9 @@ export interface WorkspaceResult { /** Run a git command in a specific directory */ export const runGitCommand = (cwd: AbsoluteDirPath, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - const result = yield* Command.string(command) + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + const result = yield* spawner.string(command) return result.trim() }) @@ -195,7 +197,7 @@ export const createWorkspace = (fixture?: WorkspaceFixture) => const config: MegarepoConfig = { members: fixture?.members ?? {}, } - const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + const configContent = yield* encodePrettyJson(MegarepoConfig)( config, ) yield* fs.writeFileString( @@ -332,7 +334,7 @@ export const readConfig = (workspacePath: AbsoluteDirPath) => EffectPath.unsafe.relativeFile('megarepo.json'), ) const content = yield* fs.readFileString(configPath) - return yield* Schema.decodeUnknown(Schema.fromJsonString(MegarepoConfig))(content) + return yield* Schema.decodeUnknownEffect(Schema.fromJsonString(MegarepoConfig))(content) }) /** Generate a megarepo.json config object */ diff --git a/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts b/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts index fc6ea376b3..d238f6f90e 100644 --- a/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts +++ b/packages/@overeng/megarepo/src/test-utils/store-setup.integration.test.ts @@ -1,5 +1,5 @@ -import { ChildProcess as Command } from 'effect/unstable/process' -import { FileSystem } from 'effect/FileSystem' +import { ChildProcess as Command, ChildProcessSpawner } from 'effect/unstable/process' +import * as FileSystem from 'effect/FileSystem' import { NodeServices } from '@effect/platform-node' import { describe, it } from '@effect/vitest' import { Effect } from 'effect' @@ -17,8 +17,9 @@ import { const git = (cwd: AbsoluteDirPath, ...args: ReadonlyArray) => Effect.gen(function* () { - const command = Command.make('git', ...args).pipe(Command.workingDirectory(cwd)) - return (yield* Command.string(command)).trim() + const spawner = yield* ChildProcessSpawner.ChildProcessSpawner + const command = Command.make('git', args, { cwd }) + return (yield* spawner.string(command)).trim() }) describe('store-setup fixtures', () => { diff --git a/packages/@overeng/megarepo/src/test-utils/store-setup.ts b/packages/@overeng/megarepo/src/test-utils/store-setup.ts index 4e3e9db8a8..52823fab1e 100644 --- a/packages/@overeng/megarepo/src/test-utils/store-setup.ts +++ b/packages/@overeng/megarepo/src/test-utils/store-setup.ts @@ -4,12 +4,13 @@ * Provides helpers for creating test stores with bare repos and worktrees. */ -import { FileSystem } from 'effect/FileSystem' +import * as FileSystem from 'effect/FileSystem' import { Effect, Option, Schema } from 'effect' import { EffectPath, type AbsoluteDirPath } from '@overeng/effect-path' import { MegarepoConfig } from '../lib/config.ts' +import { encodePrettyJson } from '../lib/json.ts' import * as Git from '../lib/git.ts' import { createLockedMember, @@ -194,7 +195,7 @@ export const createStoreFixture = (repos: ReadonlyArray) => yield* Effect.gen(function* () { yield* runGitCommand(sourceRepoPath, 'remote', 'add', 'origin', pushTargetPath) yield* runGitCommand(sourceRepoPath, 'push', '-u', 'origin', 'main').pipe( - Effect.catchAll(() => + Effect.catch(() => // Try master if main fails runGitCommand(sourceRepoPath, 'push', '-u', 'origin', 'master'), ), @@ -203,10 +204,10 @@ export const createStoreFixture = (repos: ReadonlyArray) => for (const branch of repoFixture.branches ?? []) { if (branch === 'main' || branch === 'master') continue yield* runGitCommand(sourceRepoPath, 'branch', branch, commitSha).pipe( - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), ) yield* runGitCommand(sourceRepoPath, 'push', 'origin', branch).pipe( - Effect.catchAll(() => Effect.void), + Effect.catch(() => Effect.void), ) } @@ -389,7 +390,7 @@ export const createWorkspaceWithLock = (args: { const config: MegarepoConfig = { members: args.members, } - const configContent = yield* Schema.encode(Schema.fromJsonString(MegarepoConfig, { space: 2 }))( + const configContent = yield* encodePrettyJson(MegarepoConfig)( config, ) yield* fs.writeFileString( @@ -463,7 +464,7 @@ export const repinWorkspace = ({ yield* fs.makeDirectory(reposDir, { recursive: true }) const symlinkPath = EffectPath.ops.join(reposDir, EffectPath.unsafe.relativeFile(memberName)) // Replace any existing symlink so the new target is the on-disk truth. - yield* fs.remove(symlinkPath, { force: true }).pipe(Effect.catchAll(() => Effect.void)) + yield* fs.remove(symlinkPath, { force: true }).pipe(Effect.catch(() => Effect.void)) yield* fs.symlink(newTarget.replace(/\/+$/, ''), symlinkPath) // Optionally rewrite the lock entry for this member (ref/commit repin), @@ -516,7 +517,7 @@ export const materializeNonDetachedBranchWorktree = ({ yield* runGitCommand(bareRepoPath, 'worktree', 'remove', '--force', worktreePath) yield* fs .remove(worktreePath, { recursive: true, force: true }) - .pipe(Effect.catchAll(() => Effect.void)) + .pipe(Effect.catch(() => Effect.void)) // Ensure the branch ref points at this fixture commit, then check it out in // a fresh worktree (non-detached). yield* runGitCommand(bareRepoPath, 'branch', '-f', branch, commit)