Using without Effect
You want the session scaffolding — sessions, sticky ports, managed subprocesses — but you don't write Effect, and you're not about to learn it to run a dev script. devsess/async is the same machinery as Getting Started, behind plain async/await instead of Effect.gen.
It still needs effect and @effect/platform-node sitting in node_modules — devsess/async is built on the same Effect core underneath, it only keeps that out of your own code. Install is otherwise identical to the core install.
The same script, rewritten
With Effect:
// scripts/dev.ts
import { join } from 'node:path'
import { defineDevCli } from 'devsess'
import { Effect } from 'effect'
const main = defineDevCli({
name: 'web',
dir: join(import.meta.dirname, '..'),
run: (ctx) =>
Effect.gen(function* () {
const session = yield* ctx.session
yield* Effect.logInfo(`[dev] session: ${session.name}`)
const port = yield* ctx.getStickyPort()
yield* ctx.runManagedSubprocess('bunx', ['vite'], {
env: { PORT: String(port) },
})
}),
})
main(process.argv)With devsess/async:
// scripts/dev.ts
import { join } from 'node:path'
import { defineDevCli } from 'devsess/async'
const main = defineDevCli({
name: 'web',
dir: join(import.meta.dirname, '..'),
run: async (ctx) => {
const session = await ctx.session()
console.log(`[dev] session: ${session.name}`)
const port = await ctx.getStickyPort()
await ctx.runManagedSubprocess('bunx', ['vite'], {
env: { PORT: String(port) },
})
},
})
main(process.argv)No effect import, no generator function — otherwise the same script.
Differences that actually bite
ctx.sessionis a function, not a value. The Effect API hands you anEffectyou resolve withyield* ctx.session.devsess/asynchands you a function:await ctx.session(). Call it as many times as you want — it's still resolved once per run internally, so repeat calls are free, not repeat work.session.path(rel)is synchronous. Noawait, noyield*— it returns astringdirectly:const dir = session.path('cache'). It still doesn't create the directory, same as the Effect version.createDevSessions(rootDir)replacesDevSessionsandmakeDevSessionsLayer. No service tag, no Layer — for anything you do outsidedefineDevCli. ItsrootDiris the sessions directory itself, not your project root: pass<project>/.data/sessions, or you'll get session directories somewheredefineDevClinever looks.cliandSchemaare both re-exported — but only fromdevsess/async.import { cli, Schema } from 'devsess/async'covers flags (cli.Flag...) andSessionStateschemas (Schema.Struct(...)) without ever importingeffect. The coredevsesspackage re-exportsclitoo, but notSchema— Effect users are already importing fromeffectfor everything else, so it doesn't bother.
A script or a test reaches for createDevSessions directly:
// scripts/seed.ts
import { join } from 'node:path'
import { createDevSessions } from 'devsess/async'
const sessions = createDevSessions(
join(import.meta.dirname, '..', '.data/sessions'),
)
const session = await sessions.getLatestOrCreate()What you give up
devsess/pglite doesn't work with devsess/async — not "no wrapper exists yet," but structurally incompatible.
If your dev script needs a database per session, that piece has to be written against the Effect API. You can still write the rest of the script with devsess/async and drop into Effect.gen/Effect.runPromise for that one piece — but at that point you're back to an effect import, at least in that one file.
Everything else carries over exactly: dev sessions (including SessionState), sticky ports, managed subprocess lifetime, and running signals. Same underlying code, wrapped in Promises instead of Effects.