devsess 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +109 -0
  2. package/dist/async/define-cli.d.ts +20 -0
  3. package/dist/async/define-cli.d.ts.map +1 -0
  4. package/dist/async/dev-sessions.d.ts +16 -0
  5. package/dist/async/dev-sessions.d.ts.map +1 -0
  6. package/dist/async/index.d.ts +8 -0
  7. package/dist/async/index.d.ts.map +1 -0
  8. package/dist/async/index.js +127 -0
  9. package/dist/async/index.js.map +1 -0
  10. package/dist/async/run.d.ts +5 -0
  11. package/dist/async/run.d.ts.map +1 -0
  12. package/dist/async/session-state.d.ts +11 -0
  13. package/dist/async/session-state.d.ts.map +1 -0
  14. package/dist/async/session.d.ts +16 -0
  15. package/dist/async/session.d.ts.map +1 -0
  16. package/dist/chunk-IEQZ3AJH.js +282 -0
  17. package/dist/chunk-IEQZ3AJH.js.map +1 -0
  18. package/dist/dev/define-cli.d.ts +25 -0
  19. package/dist/dev/define-cli.d.ts.map +1 -0
  20. package/dist/dev/run-dev-cli.d.ts +20 -0
  21. package/dist/dev/run-dev-cli.d.ts.map +1 -0
  22. package/dist/dev/running-signal.d.ts +18 -0
  23. package/dist/dev/running-signal.d.ts.map +1 -0
  24. package/dist/dev/session-state.d.ts +23 -0
  25. package/dist/dev/session-state.d.ts.map +1 -0
  26. package/dist/dev/sticky-port.d.ts +4 -0
  27. package/dist/dev/sticky-port.d.ts.map +1 -0
  28. package/dist/dev/subprocess.d.ts +5 -0
  29. package/dist/dev/subprocess.d.ts.map +1 -0
  30. package/dist/dev-sessions.d.ts +32 -0
  31. package/dist/dev-sessions.d.ts.map +1 -0
  32. package/dist/index.d.ts +5 -0
  33. package/dist/index.d.ts.map +1 -0
  34. package/dist/index.js +50 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/pglite/index.d.ts +50 -0
  37. package/dist/pglite/index.d.ts.map +1 -0
  38. package/dist/pglite/index.js +218 -0
  39. package/dist/pglite/index.js.map +1 -0
  40. package/package.json +87 -1
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # devsess
2
+
3
+ Scaffold dev scripts with reusable **dev sessions** and a per-session **PGlite + Drizzle** adapter — built on [Effect](https://effect.website).
4
+
5
+ `devsess` gives your local `scripts/dev.ts` two things:
6
+
7
+ - **Dev sessions** — each run reuses (or creates) a named, slug-based working directory under `.data/sessions`. Sticky dev-server ports, managed subprocesses that die with your script, and cross-service "running" signal files come built in.
8
+ - **A PGlite adapter** (`devsess/pglite`) — prepare a local Postgres-compatible database from your Drizzle migrations, scoped to the current dev session. Perfect for a `--lite` offline dev mode.
9
+
10
+ It targets **Node** (`@effect/platform-node`). The examples below use the [Effect](https://effect.website) API (`devsess`); if you'd rather not deal with Effect directly, `devsess/async` exposes the same dev-session scaffolding as a plain, Promise-based API — see [API](#api).
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ bun add devsess effect @effect/platform-node
16
+ ```
17
+
18
+ For the PGlite adapter (`devsess/pglite`), also add:
19
+
20
+ ```bash
21
+ bun add @electric-sql/pglite drizzle-orm
22
+ ```
23
+
24
+ ## Quickstart
25
+
26
+ ```ts
27
+ // scripts/dev.ts
28
+ import { join } from 'node:path';
29
+ import { cli, defineDevCli } from 'devsess';
30
+ import { Effect } from 'effect';
31
+
32
+ const main = defineDevCli({
33
+ name: 'web',
34
+ dir: join(import.meta.dirname, '..'),
35
+ options: {
36
+ lite: cli.Flag.boolean('lite').pipe(
37
+ cli.Flag.withDescription('Use a per-session PGlite database'),
38
+ ),
39
+ },
40
+ run: (ctx, opts) =>
41
+ Effect.gen(function* () {
42
+ const session = yield* ctx.session;
43
+ yield* Effect.logInfo(`[dev] session: ${session.name}`);
44
+
45
+ const port = yield* ctx.getStickyPort();
46
+ const env: Record<string, string> = { PORT: String(port) };
47
+
48
+ if (opts.lite) {
49
+ const litePath = yield* session.path('pglite');
50
+ env.DATABASE_LITE = 'true';
51
+ env.DATABASE_LITE_PATH = litePath;
52
+ }
53
+
54
+ yield* ctx.runManagedSubprocess('bunx', ['vite'], { env });
55
+ }),
56
+ });
57
+
58
+ main(process.argv);
59
+ ```
60
+
61
+ ```bash
62
+ bun scripts/dev.ts --lite
63
+ ```
64
+
65
+ ## Per-session PGlite
66
+
67
+ `prepareSessionPglite` builds a migration dump (if missing), hydrates a PGlite
68
+ client, validates and applies migrations — all scoped to the active dev session:
69
+
70
+ ```ts
71
+ import { prepareSessionPglite } from 'devsess/pglite';
72
+
73
+ // inside run():
74
+ const session = yield* ctx.session;
75
+ const db = yield* prepareSessionPglite(session, {
76
+ migrationsFolder: join(import.meta.dirname, '../drizzle'),
77
+ });
78
+ // db.client — a ready PGlite client
79
+ // db.dataDir — its on-disk path (e.g. pass as DATABASE_LITE_PATH)
80
+ ```
81
+
82
+ ## API
83
+
84
+ From `devsess` (the Effect API):
85
+
86
+ - `defineDevCli({ name, dir, options?, run })` → `(argv) => void`
87
+ - `cli` — re-export of `effect/unstable/cli`
88
+ - `DevSessions`, `makeDevSessionsLayer(rootDir)`, `DevSession`
89
+ - `SessionState.slot(schema)` — typed per-session JSON state
90
+
91
+ From `devsess/pglite`:
92
+
93
+ - `prepareSessionPglite(session, { migrationsFolder })`
94
+ - `openLitePglite({ dataDir, dumpPath, migrationsFolder })`
95
+ - `buildPgliteDump`, `ensurePgliteDump`, `migratePglite`,
96
+ `dumpPgliteToFile`, `createPgliteFromDump`, `getDbMigrationCount`,
97
+ `getExpectedMigrationCount`, and the `PgliteError` tagged error.
98
+
99
+ From `devsess/async` (a plain, Promise-based mirror of the Effect API — no `effect` imports required):
100
+
101
+ - `defineDevCli({ name, dir, options?, run })` — `run` is `async (ctx, opts) => void`, and `ctx`'s helpers return Promises instead of Effects
102
+ - `createDevSessions(rootDir)` — async session manager (`getSessions`, `createSession`, `getLatestOrCreate`)
103
+ - `SessionState`, `DevSession`, plus `cli` and `Schema` re-exports
104
+
105
+ See the [full documentation](https://github.com/fdarian/devsess/tree/main/apps/docs) for details.
106
+
107
+ ## License
108
+
109
+ Apache-2.0
@@ -0,0 +1,20 @@
1
+ import { Effect } from 'effect';
2
+ import { type CommandConfig } from '../dev/run-dev-cli';
3
+ import { runManagedSubprocess } from '../dev/subprocess';
4
+ import { type DevSession } from './session';
5
+ /** Promise-returning mirror of the Effect core's `RunContext` (see `src/dev/define-cli.ts`). */
6
+ type AsyncRunContext = {
7
+ session: () => Promise<DevSession>;
8
+ getStickyPort: () => Promise<number>;
9
+ runManagedSubprocess: (...args: Parameters<typeof runManagedSubprocess>) => Promise<Effect.Success<ReturnType<typeof runManagedSubprocess>>>;
10
+ publishRunning: (data: unknown) => Promise<void>;
11
+ awaitRunning: <T>(pkg: string) => Promise<T>;
12
+ };
13
+ export declare const defineDevCli: (config: {
14
+ name: string;
15
+ dir: string;
16
+ options?: CommandConfig;
17
+ run: (ctx: AsyncRunContext, opts: Record<string, unknown>) => Promise<void> | void;
18
+ }) => ((argv: string[]) => void);
19
+ export {};
20
+ //# sourceMappingURL=define-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-cli.d.ts","sourceRoot":"","sources":["../../src/async/define-cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAI5C,OAAO,EAAE,KAAK,aAAa,EAAa,MAAM,oBAAoB,CAAC;AAQnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD,OAAO,EAAE,KAAK,UAAU,EAAkB,MAAM,WAAW,CAAC;AAE5D,gGAAgG;AAChG,KAAK,eAAe,GAAG;IACtB,OAAO,EAAE,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;IACnC,aAAa,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,oBAAoB,EAAE,CACrB,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,KAC5C,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC,CAAC;IACtE,cAAc,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjD,YAAY,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7C,CAAC;AAEF,eAAO,MAAM,YAAY,WAAY;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,GAAG,EAAE,CACJ,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACzB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC1B,KAAG,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CA4C1B,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type DevSession } from './session';
2
+ export type DevSessionManager = {
3
+ readonly dir: string;
4
+ getSessions(): Promise<Array<DevSession>>;
5
+ createSession(): Promise<DevSession>;
6
+ getLatestOrCreate(): Promise<DevSession>;
7
+ };
8
+ /**
9
+ * Creates a manager for slug-named dev session directories under `rootDir`.
10
+ *
11
+ * `makeDevSessionsLayer` only wires lazy service methods when built (no directory
12
+ * IO happens at build time — see `src/dev-sessions.ts`), so it's cheap to re-provide
13
+ * the same `layer` value on every call rather than resolving a shared runtime once.
14
+ */
15
+ export declare const createDevSessions: (rootDir: string) => DevSessionManager;
16
+ //# sourceMappingURL=dev-sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-sessions.d.ts","sourceRoot":"","sources":["../../src/async/dev-sessions.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAkB,MAAM,WAAW,CAAC;AAE5D,MAAM,MAAM,iBAAiB,GAAG;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1C,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;IACrC,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CACzC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,YAAa,MAAM,KAAG,iBAwBnD,CAAC"}
@@ -0,0 +1,8 @@
1
+ export { Schema } from 'effect';
2
+ export * as cli from 'effect/unstable/cli';
3
+ export { SessionStateError } from '../dev/session-state';
4
+ export { defineDevCli } from './define-cli';
5
+ export { createDevSessions, type DevSessionManager } from './dev-sessions';
6
+ export type { DevSession } from './session';
7
+ export { SessionState } from './session-state';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/async/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,KAAK,GAAG,MAAM,qBAAqB,CAAC;AAE3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC3E,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,127 @@
1
+ import {
2
+ DevSessions,
3
+ SessionState,
4
+ SessionStateError,
5
+ awaitRunningSignal,
6
+ getStickyPort,
7
+ makeDevSessionsLayer,
8
+ publishRunningSignal,
9
+ resolveSiblingDir,
10
+ runDevCli,
11
+ runManagedSubprocess,
12
+ runningSignalPath
13
+ } from "../chunk-IEQZ3AJH.js";
14
+
15
+ // src/async/index.ts
16
+ import { Schema } from "effect";
17
+ import * as cli from "effect/unstable/cli";
18
+
19
+ // src/async/define-cli.ts
20
+ import { Effect as Effect2 } from "effect";
21
+
22
+ // src/async/session.ts
23
+ import { Effect } from "effect";
24
+ var EFFECT_SESSION = /* @__PURE__ */ Symbol("devsess/async/DevSession#effectSession");
25
+ var toAsyncSession = (session) => {
26
+ const asyncSession = {
27
+ name: session.name,
28
+ lastModifiedAt: session.lastModifiedAt,
29
+ // Safe to run synchronously: the core `path` is `Effect.succeed(pathJoin(rel))`, pure.
30
+ path: (relativePath) => Effect.runSync(session.path(relativePath)),
31
+ toString: () => session.toString()
32
+ };
33
+ Object.defineProperty(asyncSession, EFFECT_SESSION, {
34
+ value: session,
35
+ enumerable: false
36
+ });
37
+ return asyncSession;
38
+ };
39
+ var unwrapSession = (session) => {
40
+ const effectSession = session[EFFECT_SESSION];
41
+ if (effectSession === void 0) {
42
+ throw new Error(
43
+ "devsess/async: expected a DevSession created by this package, but the internal Effect session handle is missing"
44
+ );
45
+ }
46
+ return effectSession;
47
+ };
48
+
49
+ // src/async/define-cli.ts
50
+ var defineDevCli = (config) => runDevCli({
51
+ name: config.name,
52
+ dir: config.dir,
53
+ options: config.options,
54
+ makeHandler: (opts) => Effect2.gen(function* () {
55
+ const sessions = yield* DevSessions;
56
+ const session = yield* Effect2.cached(sessions.getLatestOrCreate);
57
+ const services = yield* Effect2.context();
58
+ const ctx = {
59
+ session: () => Effect2.runPromiseWith(services)(session).then(toAsyncSession),
60
+ getStickyPort: () => Effect2.runPromiseWith(services)(
61
+ Effect2.flatMap(session, getStickyPort)
62
+ ),
63
+ runManagedSubprocess: (...args) => Effect2.runPromiseWith(services)(runManagedSubprocess(...args)),
64
+ publishRunning: (data) => Effect2.runPromiseWith(services)(
65
+ publishRunningSignal(runningSignalPath(config.dir), data)
66
+ ),
67
+ awaitRunning: (pkg) => Effect2.runPromiseWith(services)(
68
+ awaitRunningSignal(
69
+ runningSignalPath(resolveSiblingDir(pkg, config.dir)),
70
+ { parse: (raw) => JSON.parse(raw) }
71
+ )
72
+ )
73
+ };
74
+ yield* Effect2.promise(() => Promise.resolve(config.run(ctx, opts)));
75
+ })
76
+ });
77
+
78
+ // src/async/dev-sessions.ts
79
+ import { NodeServices } from "@effect/platform-node";
80
+ import { Effect as Effect3, Layer } from "effect";
81
+ var createDevSessions = (rootDir) => {
82
+ const layer = makeDevSessionsLayer(rootDir).pipe(
83
+ Layer.provide(NodeServices.layer)
84
+ );
85
+ const run = (effect) => Effect3.runPromise(Effect3.provide(effect, layer));
86
+ return {
87
+ // Matches the service's own `dir`, which is `rootDir` verbatim — safe to read synchronously.
88
+ dir: rootDir,
89
+ getSessions: () => run(DevSessions.use((sessions) => sessions.getSessions)).then(
90
+ (sessions) => sessions.map(toAsyncSession)
91
+ ),
92
+ createSession: () => run(DevSessions.use((sessions) => sessions.createSession)).then(
93
+ toAsyncSession
94
+ ),
95
+ getLatestOrCreate: () => run(DevSessions.use((sessions) => sessions.getLatestOrCreate)).then(
96
+ toAsyncSession
97
+ )
98
+ };
99
+ };
100
+
101
+ // src/async/run.ts
102
+ import { NodeServices as NodeServices2 } from "@effect/platform-node";
103
+ import { Effect as Effect4 } from "effect";
104
+ var nodeLayer = NodeServices2.layer;
105
+ var runNode = (effect) => Effect4.runPromise(Effect4.scoped(Effect4.provide(effect, nodeLayer)));
106
+
107
+ // src/async/session-state.ts
108
+ var SessionState2;
109
+ ((SessionState3) => {
110
+ function slot(schema) {
111
+ const effectSlot = SessionState.slot(schema);
112
+ return {
113
+ read: (session) => runNode(effectSlot.read(unwrapSession(session))),
114
+ write: (session, data) => runNode(effectSlot.write(unwrapSession(session), data))
115
+ };
116
+ }
117
+ SessionState3.slot = slot;
118
+ })(SessionState2 || (SessionState2 = {}));
119
+ export {
120
+ Schema,
121
+ SessionState2 as SessionState,
122
+ SessionStateError,
123
+ cli,
124
+ createDevSessions,
125
+ defineDevCli
126
+ };
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/async/index.ts","../../src/async/define-cli.ts","../../src/async/session.ts","../../src/async/dev-sessions.ts","../../src/async/run.ts","../../src/async/session-state.ts"],"sourcesContent":["// Re-exported so consumers can declare CLI flags/arguments without a direct\n// dependency on `effect/unstable/cli`, e.g. `cli.Flag.string('local')`.\n\nexport { Schema } from 'effect';\nexport * as cli from 'effect/unstable/cli';\n\nexport { SessionStateError } from '../dev/session-state';\nexport { defineDevCli } from './define-cli';\nexport { createDevSessions, type DevSessionManager } from './dev-sessions';\nexport type { DevSession } from './session';\nexport { SessionState } from './session-state';\n","import { Effect, type Scope } from 'effect';\nimport type { FileSystem } from 'effect/FileSystem';\nimport type { Path } from 'effect/Path';\nimport type { ChildProcessSpawner } from 'effect/unstable/process/ChildProcessSpawner';\nimport { type CommandConfig, runDevCli } from '../dev/run-dev-cli';\nimport {\n\tawaitRunningSignal,\n\tpublishRunningSignal,\n\tresolveSiblingDir,\n\trunningSignalPath,\n} from '../dev/running-signal';\nimport { getStickyPort } from '../dev/sticky-port';\nimport { runManagedSubprocess } from '../dev/subprocess';\nimport { DevSessions } from '../dev-sessions';\nimport { type DevSession, toAsyncSession } from './session';\n\n/** Promise-returning mirror of the Effect core's `RunContext` (see `src/dev/define-cli.ts`). */\ntype AsyncRunContext = {\n\tsession: () => Promise<DevSession>;\n\tgetStickyPort: () => Promise<number>;\n\trunManagedSubprocess: (\n\t\t...args: Parameters<typeof runManagedSubprocess>\n\t) => Promise<Effect.Success<ReturnType<typeof runManagedSubprocess>>>;\n\tpublishRunning: (data: unknown) => Promise<void>;\n\tawaitRunning: <T>(pkg: string) => Promise<T>;\n};\n\nexport const defineDevCli = (config: {\n\tname: string;\n\tdir: string;\n\toptions?: CommandConfig;\n\trun: (\n\t\tctx: AsyncRunContext,\n\t\topts: Record<string, unknown>,\n\t) => Promise<void> | void;\n}): ((argv: string[]) => void) =>\n\trunDevCli({\n\t\tname: config.name,\n\t\tdir: config.dir,\n\t\toptions: config.options,\n\t\tmakeHandler: (opts) =>\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst sessions = yield* DevSessions;\n\t\t\t\tconst session = yield* Effect.cached(sessions.getLatestOrCreate);\n\n\t\t\t\t// Capture the process-lifetime context (the Scope opened by `runDevCli`'s\n\t\t\t\t// outer `Effect.scoped`, plus DevSessions | FileSystem | Path | ChildProcessSpawner).\n\t\t\t\t// Running each ctx helper with this captured context — instead of a fresh,\n\t\t\t\t// transient `Effect.runPromise` per call — attaches its scoped resources\n\t\t\t\t// (managed subprocess, running-signal file) to that process-lifetime scope,\n\t\t\t\t// so they only release when the CLI process ends, not right after the Promise settles.\n\t\t\t\tconst services = yield* Effect.context<\n\t\t\t\t\tDevSessions | FileSystem | Path | ChildProcessSpawner | Scope.Scope\n\t\t\t\t>();\n\n\t\t\t\tconst ctx: AsyncRunContext = {\n\t\t\t\t\tsession: () =>\n\t\t\t\t\t\tEffect.runPromiseWith(services)(session).then(toAsyncSession),\n\t\t\t\t\tgetStickyPort: () =>\n\t\t\t\t\t\tEffect.runPromiseWith(services)(\n\t\t\t\t\t\t\tEffect.flatMap(session, getStickyPort),\n\t\t\t\t\t\t),\n\t\t\t\t\trunManagedSubprocess: (...args) =>\n\t\t\t\t\t\tEffect.runPromiseWith(services)(runManagedSubprocess(...args)),\n\t\t\t\t\tpublishRunning: (data) =>\n\t\t\t\t\t\tEffect.runPromiseWith(services)(\n\t\t\t\t\t\t\tpublishRunningSignal(runningSignalPath(config.dir), data),\n\t\t\t\t\t\t),\n\t\t\t\t\tawaitRunning: <T>(pkg: string) =>\n\t\t\t\t\t\tEffect.runPromiseWith(services)(\n\t\t\t\t\t\t\tawaitRunningSignal<T>(\n\t\t\t\t\t\t\t\trunningSignalPath(resolveSiblingDir(pkg, config.dir)),\n\t\t\t\t\t\t\t\t{ parse: (raw) => JSON.parse(raw) as T },\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t),\n\t\t\t\t};\n\n\t\t\t\tyield* Effect.promise(() => Promise.resolve(config.run(ctx, opts)));\n\t\t\t}),\n\t});\n","import { Effect } from 'effect';\nimport type { DevSession as EffectDevSession } from '../dev-sessions';\n\n/** Non-enumerable, module-private key used to stash the Effect session on its async wrapper. */\nconst EFFECT_SESSION = Symbol('devsess/async/DevSession#effectSession');\n\nexport type DevSession = {\n\treadonly name: string;\n\treadonly lastModifiedAt: Date | null;\n\tpath(relativePath: string): string;\n\ttoString(): string;\n};\n\n/**\n * Wraps an Effect `DevSession` as a plain-async `DevSession`. The original Effect\n * session is stashed under `EFFECT_SESSION` so other async ops (e.g. `SessionState`)\n * can reuse the Effect core via `unwrapSession`.\n */\nexport const toAsyncSession = (session: EffectDevSession): DevSession => {\n\tconst asyncSession: DevSession = {\n\t\tname: session.name,\n\t\tlastModifiedAt: session.lastModifiedAt,\n\t\t// Safe to run synchronously: the core `path` is `Effect.succeed(pathJoin(rel))`, pure.\n\t\tpath: (relativePath) => Effect.runSync(session.path(relativePath)),\n\t\ttoString: () => session.toString(),\n\t};\n\n\tObject.defineProperty(asyncSession, EFFECT_SESSION, {\n\t\tvalue: session,\n\t\tenumerable: false,\n\t});\n\n\treturn asyncSession;\n};\n\n/** Recovers the Effect `DevSession` stashed by `toAsyncSession`. */\nexport const unwrapSession = (session: DevSession): EffectDevSession => {\n\tconst effectSession = (\n\t\tsession as unknown as Record<\n\t\t\ttypeof EFFECT_SESSION,\n\t\t\tEffectDevSession | undefined\n\t\t>\n\t)[EFFECT_SESSION];\n\tif (effectSession === undefined) {\n\t\tthrow new Error(\n\t\t\t'devsess/async: expected a DevSession created by this package, but the internal Effect session handle is missing',\n\t\t);\n\t}\n\treturn effectSession;\n};\n","import { NodeServices } from '@effect/platform-node';\nimport { Effect, Layer } from 'effect';\nimport { DevSessions, makeDevSessionsLayer } from '../dev-sessions';\nimport { type DevSession, toAsyncSession } from './session';\n\nexport type DevSessionManager = {\n\treadonly dir: string;\n\tgetSessions(): Promise<Array<DevSession>>;\n\tcreateSession(): Promise<DevSession>;\n\tgetLatestOrCreate(): Promise<DevSession>;\n};\n\n/**\n * Creates a manager for slug-named dev session directories under `rootDir`.\n *\n * `makeDevSessionsLayer` only wires lazy service methods when built (no directory\n * IO happens at build time — see `src/dev-sessions.ts`), so it's cheap to re-provide\n * the same `layer` value on every call rather than resolving a shared runtime once.\n */\nexport const createDevSessions = (rootDir: string): DevSessionManager => {\n\tconst layer = makeDevSessionsLayer(rootDir).pipe(\n\t\tLayer.provide(NodeServices.layer),\n\t);\n\n\tconst run = <A, E>(effect: Effect.Effect<A, E, DevSessions>): Promise<A> =>\n\t\tEffect.runPromise(Effect.provide(effect, layer));\n\n\treturn {\n\t\t// Matches the service's own `dir`, which is `rootDir` verbatim — safe to read synchronously.\n\t\tdir: rootDir,\n\t\tgetSessions: () =>\n\t\t\trun(DevSessions.use((sessions) => sessions.getSessions)).then(\n\t\t\t\t(sessions) => sessions.map(toAsyncSession),\n\t\t\t),\n\t\tcreateSession: () =>\n\t\t\trun(DevSessions.use((sessions) => sessions.createSession)).then(\n\t\t\t\ttoAsyncSession,\n\t\t\t),\n\t\tgetLatestOrCreate: () =>\n\t\t\trun(DevSessions.use((sessions) => sessions.getLatestOrCreate)).then(\n\t\t\t\ttoAsyncSession,\n\t\t\t),\n\t};\n};\n","import { NodeServices } from '@effect/platform-node';\nimport { Effect, type Scope } from 'effect';\n\nconst nodeLayer = NodeServices.layer;\n\n/** Runs an Effect that only needs the shared Node platform services (+ an optional Scope) as a Promise. */\nexport const runNode = <A, E>(\n\teffect: Effect.Effect<A, E, NodeServices.NodeServices | Scope.Scope>,\n): Promise<A> =>\n\tEffect.runPromise(Effect.scoped(Effect.provide(effect, nodeLayer)));\n","import type * as S from 'effect/Schema';\nimport { SessionState as EffectSessionState } from '../dev/session-state';\nimport { runNode } from './run';\nimport { type DevSession, unwrapSession } from './session';\n\nexport namespace SessionState {\n\t// Decoding must run without extra context (only `NodeServices | Scope` are\n\t// available via `runNode`), so schemas cannot require decoding services.\n\ttype Shape = S.Codec<unknown, unknown, never>;\n\n\texport function slot<T extends Shape>(schema: T) {\n\t\tconst effectSlot = EffectSessionState.slot(schema);\n\t\treturn {\n\t\t\tread: (session: DevSession): Promise<T['Type'] | null> =>\n\t\t\t\trunNode(effectSlot.read(unwrapSession(session))),\n\t\t\twrite: (session: DevSession, data: T['Type']): Promise<void> =>\n\t\t\t\trunNode(effectSlot.write(unwrapSession(session), data)),\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AAGA,SAAS,cAAc;AACvB,YAAY,SAAS;;;ACJrB,SAAS,UAAAA,eAA0B;;;ACAnC,SAAS,cAAc;AAIvB,IAAM,iBAAiB,uBAAO,wCAAwC;AAc/D,IAAM,iBAAiB,CAAC,YAA0C;AACxE,QAAM,eAA2B;AAAA,IAChC,MAAM,QAAQ;AAAA,IACd,gBAAgB,QAAQ;AAAA;AAAA,IAExB,MAAM,CAAC,iBAAiB,OAAO,QAAQ,QAAQ,KAAK,YAAY,CAAC;AAAA,IACjE,UAAU,MAAM,QAAQ,SAAS;AAAA,EAClC;AAEA,SAAO,eAAe,cAAc,gBAAgB;AAAA,IACnD,OAAO;AAAA,IACP,YAAY;AAAA,EACb,CAAC;AAED,SAAO;AACR;AAGO,IAAM,gBAAgB,CAAC,YAA0C;AACvE,QAAM,gBACL,QAIC,cAAc;AAChB,MAAI,kBAAkB,QAAW;AAChC,UAAM,IAAI;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACA,SAAO;AACR;;;ADtBO,IAAM,eAAe,CAAC,WAS5B,UAAU;AAAA,EACT,MAAM,OAAO;AAAA,EACb,KAAK,OAAO;AAAA,EACZ,SAAS,OAAO;AAAA,EAChB,aAAa,CAAC,SACbC,QAAO,IAAI,aAAa;AACvB,UAAM,WAAW,OAAO;AACxB,UAAM,UAAU,OAAOA,QAAO,OAAO,SAAS,iBAAiB;AAQ/D,UAAM,WAAW,OAAOA,QAAO,QAE7B;AAEF,UAAM,MAAuB;AAAA,MAC5B,SAAS,MACRA,QAAO,eAAe,QAAQ,EAAE,OAAO,EAAE,KAAK,cAAc;AAAA,MAC7D,eAAe,MACdA,QAAO,eAAe,QAAQ;AAAA,QAC7BA,QAAO,QAAQ,SAAS,aAAa;AAAA,MACtC;AAAA,MACD,sBAAsB,IAAI,SACzBA,QAAO,eAAe,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;AAAA,MAC9D,gBAAgB,CAAC,SAChBA,QAAO,eAAe,QAAQ;AAAA,QAC7B,qBAAqB,kBAAkB,OAAO,GAAG,GAAG,IAAI;AAAA,MACzD;AAAA,MACD,cAAc,CAAI,QACjBA,QAAO,eAAe,QAAQ;AAAA,QAC7B;AAAA,UACC,kBAAkB,kBAAkB,KAAK,OAAO,GAAG,CAAC;AAAA,UACpD,EAAE,OAAO,CAAC,QAAQ,KAAK,MAAM,GAAG,EAAO;AAAA,QACxC;AAAA,MACD;AAAA,IACF;AAEA,WAAOA,QAAO,QAAQ,MAAM,QAAQ,QAAQ,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC;AAAA,EACnE,CAAC;AACH,CAAC;;;AE/EF,SAAS,oBAAoB;AAC7B,SAAS,UAAAC,SAAQ,aAAa;AAkBvB,IAAM,oBAAoB,CAAC,YAAuC;AACxE,QAAM,QAAQ,qBAAqB,OAAO,EAAE;AAAA,IAC3C,MAAM,QAAQ,aAAa,KAAK;AAAA,EACjC;AAEA,QAAM,MAAM,CAAO,WAClBC,QAAO,WAAWA,QAAO,QAAQ,QAAQ,KAAK,CAAC;AAEhD,SAAO;AAAA;AAAA,IAEN,KAAK;AAAA,IACL,aAAa,MACZ,IAAI,YAAY,IAAI,CAAC,aAAa,SAAS,WAAW,CAAC,EAAE;AAAA,MACxD,CAAC,aAAa,SAAS,IAAI,cAAc;AAAA,IAC1C;AAAA,IACD,eAAe,MACd,IAAI,YAAY,IAAI,CAAC,aAAa,SAAS,aAAa,CAAC,EAAE;AAAA,MAC1D;AAAA,IACD;AAAA,IACD,mBAAmB,MAClB,IAAI,YAAY,IAAI,CAAC,aAAa,SAAS,iBAAiB,CAAC,EAAE;AAAA,MAC9D;AAAA,IACD;AAAA,EACF;AACD;;;AC3CA,SAAS,gBAAAC,qBAAoB;AAC7B,SAAS,UAAAC,eAA0B;AAEnC,IAAM,YAAYD,cAAa;AAGxB,IAAM,UAAU,CACtB,WAEAC,QAAO,WAAWA,QAAO,OAAOA,QAAO,QAAQ,QAAQ,SAAS,CAAC,CAAC;;;ACJ5D,IAAUC;AAAA,CAAV,CAAUA,kBAAV;AAKC,WAAS,KAAsB,QAAW;AAChD,UAAM,aAAa,aAAmB,KAAK,MAAM;AACjD,WAAO;AAAA,MACN,MAAM,CAAC,YACN,QAAQ,WAAW,KAAK,cAAc,OAAO,CAAC,CAAC;AAAA,MAChD,OAAO,CAAC,SAAqB,SAC5B,QAAQ,WAAW,MAAM,cAAc,OAAO,GAAG,IAAI,CAAC;AAAA,IACxD;AAAA,EACD;AARO,EAAAA,cAAS;AAAA,GALAA,kCAAA;","names":["Effect","Effect","Effect","Effect","NodeServices","Effect","SessionState"]}
@@ -0,0 +1,5 @@
1
+ import { NodeServices } from '@effect/platform-node';
2
+ import { Effect, type Scope } from 'effect';
3
+ /** Runs an Effect that only needs the shared Node platform services (+ an optional Scope) as a Promise. */
4
+ export declare const runNode: <A, E>(effect: Effect.Effect<A, E, NodeServices.NodeServices | Scope.Scope>) => Promise<A>;
5
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../src/async/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAI5C,2GAA2G;AAC3G,eAAO,MAAM,OAAO,GAAI,CAAC,EAAE,CAAC,UACnB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,CAAC,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,KAClE,OAAO,CAAC,CAAC,CACwD,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type * as S from 'effect/Schema';
2
+ import { type DevSession } from './session';
3
+ export declare namespace SessionState {
4
+ type Shape = S.Codec<unknown, unknown, never>;
5
+ export function slot<T extends Shape>(schema: T): {
6
+ read: (session: DevSession) => Promise<T['Type'] | null>;
7
+ write: (session: DevSession, data: T['Type']) => Promise<void>;
8
+ };
9
+ export {};
10
+ }
11
+ //# sourceMappingURL=session-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-state.d.ts","sourceRoot":"","sources":["../../src/async/session-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,CAAC,MAAM,eAAe,CAAC;AAGxC,OAAO,EAAE,KAAK,UAAU,EAAiB,MAAM,WAAW,CAAC;AAE3D,yBAAiB,YAAY,CAAC;IAG7B,KAAK,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAE9C,MAAM,UAAU,IAAI,CAAC,CAAC,SAAS,KAAK,EAAE,MAAM,EAAE,CAAC;QAG7C,IAAI,YAAY,UAAU,KAAG,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;QAEtD,KAAK,YAAY,UAAU,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAG,OAAO,CAAC,IAAI,CAAC;MAG7D;;CACD"}
@@ -0,0 +1,16 @@
1
+ import type { DevSession as EffectDevSession } from '../dev-sessions';
2
+ export type DevSession = {
3
+ readonly name: string;
4
+ readonly lastModifiedAt: Date | null;
5
+ path(relativePath: string): string;
6
+ toString(): string;
7
+ };
8
+ /**
9
+ * Wraps an Effect `DevSession` as a plain-async `DevSession`. The original Effect
10
+ * session is stashed under `EFFECT_SESSION` so other async ops (e.g. `SessionState`)
11
+ * can reuse the Effect core via `unwrapSession`.
12
+ */
13
+ export declare const toAsyncSession: (session: EffectDevSession) => DevSession;
14
+ /** Recovers the Effect `DevSession` stashed by `toAsyncSession`. */
15
+ export declare const unwrapSession: (session: DevSession) => EffectDevSession;
16
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/async/session.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAKtE,MAAM,MAAM,UAAU,GAAG;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC;IACnC,QAAQ,IAAI,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,cAAc,YAAa,gBAAgB,KAAG,UAe1D,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,aAAa,YAAa,UAAU,KAAG,gBAanD,CAAC"}
@@ -0,0 +1,282 @@
1
+ // src/dev-sessions.ts
2
+ import { Array as A, Context, Effect, Layer, Option } from "effect";
3
+ import { FileSystem } from "effect/FileSystem";
4
+ import { Path } from "effect/Path";
5
+ import { generateSlug } from "random-word-slugs";
6
+ var DevSession;
7
+ ((DevSession2) => {
8
+ function make(input) {
9
+ return {
10
+ name: input.name,
11
+ lastModifiedAt: input.lastModifiedAt,
12
+ path: (relativePath) => Effect.succeed(input.pathJoin(relativePath)),
13
+ toString: () => input.name
14
+ };
15
+ }
16
+ DevSession2.make = make;
17
+ })(DevSession || (DevSession = {}));
18
+ var DevSessions = class extends Context.Service()("devsess/DevSessions") {
19
+ };
20
+ var makeDevSessionsLayer = (rootDir) => Layer.effect(
21
+ DevSessions,
22
+ Effect.gen(function* () {
23
+ const fs = yield* FileSystem;
24
+ const path = yield* Path;
25
+ yield* Effect.logDebug(`DevSessions dir: ${rootDir}`);
26
+ const getPath2 = (relativePath) => path.join(rootDir, relativePath);
27
+ const getSessions = Effect.gen(function* () {
28
+ const entries = yield* fs.readDirectory(rootDir).pipe(Effect.catch(() => Effect.succeed([])));
29
+ const possibleSessions = yield* Effect.all(
30
+ entries.map(
31
+ (entry) => Effect.gen(function* () {
32
+ const stat = yield* fs.stat(getPath2(entry));
33
+ if (stat.type !== "Directory") return Option.none();
34
+ return Option.some(
35
+ DevSession.make({
36
+ name: entry,
37
+ lastModifiedAt: Option.getOrNull(stat.mtime),
38
+ rootDir,
39
+ pathJoin: (relativePath) => path.join(rootDir, entry, relativePath)
40
+ })
41
+ );
42
+ })
43
+ ),
44
+ { concurrency: "unbounded" }
45
+ );
46
+ return A.getSomes(possibleSessions);
47
+ });
48
+ const createSession = Effect.gen(function* () {
49
+ const slug = generateSlug(1, { partsOfSpeech: ["noun"] });
50
+ yield* fs.makeDirectory(getPath2(slug), { recursive: true });
51
+ return DevSession.make({
52
+ name: slug,
53
+ lastModifiedAt: null,
54
+ rootDir,
55
+ pathJoin: (relativePath) => path.join(rootDir, slug, relativePath)
56
+ });
57
+ });
58
+ return {
59
+ dir: rootDir,
60
+ path: getPath2,
61
+ getSessions,
62
+ createSession,
63
+ getLatestOrCreate: Effect.gen(function* () {
64
+ yield* fs.makeDirectory(rootDir, { recursive: true });
65
+ const sessions = yield* getSessions;
66
+ if (sessions.length > 0) {
67
+ sessions.sort(
68
+ (a, b) => (b.lastModifiedAt?.getTime() ?? 0) - (a.lastModifiedAt?.getTime() ?? 0)
69
+ );
70
+ const latest = sessions[0];
71
+ return latest;
72
+ }
73
+ return yield* createSession;
74
+ })
75
+ };
76
+ })
77
+ );
78
+
79
+ // src/dev/session-state.ts
80
+ import { Data, Effect as Effect2 } from "effect";
81
+ import { FileSystem as FileSystem2 } from "effect/FileSystem";
82
+ import { Path as Path2 } from "effect/Path";
83
+ import * as S from "effect/Schema";
84
+ var SessionStateError = class extends Data.TaggedError("SessionStateError") {
85
+ };
86
+ function getPath(session) {
87
+ return session.path("sess.json");
88
+ }
89
+ function read(filePath) {
90
+ return Effect2.gen(function* () {
91
+ const fs = yield* FileSystem2;
92
+ if (!(yield* fs.exists(filePath))) {
93
+ return null;
94
+ }
95
+ return yield* fs.readFileString(filePath);
96
+ });
97
+ }
98
+ var SessionState;
99
+ ((SessionState2) => {
100
+ function slot(schema) {
101
+ return {
102
+ read: (session) => Effect2.gen(function* () {
103
+ const filePath = yield* getPath(session);
104
+ const content = yield* read(filePath);
105
+ if (content == null) return null;
106
+ return yield* S.decodeEffect(S.fromJsonString(schema))(content).pipe(
107
+ Effect2.catchTag("SchemaError", () => Effect2.succeed(null))
108
+ );
109
+ }),
110
+ write: (session, data) => Effect2.gen(function* () {
111
+ const fs = yield* FileSystem2;
112
+ const path = yield* Path2;
113
+ const filePath = yield* getPath(session);
114
+ yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
115
+ const existing = yield* read(filePath);
116
+ const parsed = yield* Effect2.try({
117
+ try: () => JSON.parse(existing ?? "{}"),
118
+ catch: (cause) => new SessionStateError({
119
+ message: `Failed to parse ${filePath} as JSON`,
120
+ cause
121
+ })
122
+ });
123
+ const merged = { ...parsed, ...data };
124
+ yield* fs.writeFileString(filePath, JSON.stringify(merged));
125
+ })
126
+ };
127
+ }
128
+ SessionState2.slot = slot;
129
+ })(SessionState || (SessionState = {}));
130
+
131
+ // src/dev/run-dev-cli.ts
132
+ import { join } from "path";
133
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
134
+ import { Effect as Effect3, Layer as Layer2 } from "effect";
135
+ import * as cli from "effect/unstable/cli";
136
+ var runDevCli = (config) => {
137
+ const command = cli.Command.make(
138
+ config.name,
139
+ config.options ?? {},
140
+ (opts) => config.makeHandler(opts)
141
+ );
142
+ return (argv) => {
143
+ const layer = makeDevSessionsLayer(join(config.dir, ".data/sessions")).pipe(
144
+ Layer2.provideMerge(NodeServices.layer)
145
+ );
146
+ const program = cli.Command.runWith(command, {
147
+ version: "0.0.0"
148
+ })(argv);
149
+ NodeRuntime.runMain(Effect3.scoped(Effect3.provide(program, layer)));
150
+ };
151
+ };
152
+
153
+ // src/dev/running-signal.ts
154
+ import { watch as fsWatch, realpathSync } from "fs";
155
+ import { createRequire } from "module";
156
+ import { dirname, join as join2, resolve } from "path";
157
+ import { Deferred, Effect as Effect4 } from "effect";
158
+ import { FileSystem as FileSystem3 } from "effect/FileSystem";
159
+ import { Path as Path3 } from "effect/Path";
160
+ var RUNNING_SIGNAL_FILE = ".data/running.json";
161
+ var runningSignalPath = (dir) => join2(dir, RUNNING_SIGNAL_FILE);
162
+ var resolveSiblingDir = (spec, fromDir) => {
163
+ if (spec.startsWith(".") || spec.startsWith("/")) {
164
+ return resolve(fromDir, spec);
165
+ }
166
+ const require2 = createRequire(join2(fromDir, "package.json"));
167
+ const pkgJsonPath = require2.resolve(`${spec}/package.json`);
168
+ return dirname(realpathSync(pkgJsonPath));
169
+ };
170
+ var publishRunningSignal = (filePath, value) => Effect4.acquireRelease(
171
+ Effect4.gen(function* () {
172
+ const fs = yield* FileSystem3;
173
+ const path = yield* Path3;
174
+ const tmp = `${filePath}.tmp`;
175
+ yield* fs.makeDirectory(path.dirname(filePath), { recursive: true });
176
+ yield* fs.writeFileString(tmp, JSON.stringify(value));
177
+ yield* fs.rename(tmp, filePath);
178
+ }),
179
+ () => Effect4.gen(function* () {
180
+ const fs = yield* FileSystem3;
181
+ yield* fs.remove(filePath).pipe(Effect4.catch(() => Effect4.void));
182
+ })
183
+ );
184
+ var awaitRunningSignal = (filePath, opts) => Effect4.scoped(
185
+ Effect4.gen(function* () {
186
+ const fs = yield* FileSystem3;
187
+ const path = yield* Path3;
188
+ const dir = path.dirname(filePath);
189
+ const filename = path.basename(filePath);
190
+ const deferred = yield* Deferred.make();
191
+ const tryRead = Effect4.gen(function* () {
192
+ if (!(yield* fs.exists(filePath))) return false;
193
+ const raw = yield* fs.readFileString(filePath).pipe(Effect4.catch(() => Effect4.succeed(null)));
194
+ if (raw == null) return false;
195
+ const parsed = yield* Effect4.try(() => opts.parse(raw)).pipe(
196
+ Effect4.catch(() => Effect4.succeed(null))
197
+ );
198
+ if (parsed == null) return false;
199
+ yield* Deferred.succeed(deferred, parsed);
200
+ return true;
201
+ });
202
+ yield* fs.makeDirectory(dir, { recursive: true });
203
+ yield* Effect4.acquireRelease(
204
+ Effect4.sync(
205
+ () => (
206
+ // Watch parent dir — the file may not exist yet, and watching a missing file errors.
207
+ fsWatch(dir, { recursive: false }, (eventType, eventFilename) => {
208
+ if (eventFilename !== filename) return;
209
+ if (eventType !== "rename" && eventType !== "change") return;
210
+ Effect4.runFork(tryRead);
211
+ })
212
+ )
213
+ ),
214
+ (watcher) => Effect4.sync(() => watcher.close())
215
+ );
216
+ yield* tryRead;
217
+ return yield* Deferred.await(deferred);
218
+ })
219
+ );
220
+
221
+ // src/dev/sticky-port.ts
222
+ import { Effect as Effect5 } from "effect";
223
+ import * as S2 from "effect/Schema";
224
+ import getPort from "get-port";
225
+ var StickyPort = SessionState.slot(S2.Struct({ port: S2.Number }));
226
+ var getStickyPort = (session) => Effect5.gen(function* () {
227
+ const file = yield* StickyPort.read(session);
228
+ const preferred = file?.port;
229
+ const port = yield* Effect5.promise(
230
+ async () => getPort({
231
+ port: preferred !== void 0 ? [preferred] : void 0
232
+ })
233
+ );
234
+ yield* StickyPort.write(session, { port });
235
+ return port;
236
+ });
237
+
238
+ // src/dev/subprocess.ts
239
+ import { Effect as Effect6 } from "effect";
240
+ import { ChildProcess } from "effect/unstable/process";
241
+ var runManagedSubprocess = (cmd, args, opts) => Effect6.gen(function* () {
242
+ const command = ChildProcess.make(cmd, args, {
243
+ stdin: "inherit",
244
+ stdout: "inherit",
245
+ stderr: "inherit",
246
+ env: { ...process.env, ...opts?.env ?? {} }
247
+ });
248
+ const label = [cmd, ...args].join(" ");
249
+ const child = yield* Effect6.acquireRelease(
250
+ Effect6.gen(function* () {
251
+ const proc = yield* command;
252
+ yield* Effect6.logInfo(`[dev] started: ${label} (pid=${proc.pid})`);
253
+ return proc;
254
+ }),
255
+ (proc) => Effect6.gen(function* () {
256
+ yield* Effect6.logInfo(`[dev] stopping: ${label} (pid=${proc.pid})`);
257
+ yield* proc.kill().pipe(
258
+ Effect6.catch(
259
+ (err) => Effect6.logError(
260
+ `[dev] failed to stop ${label} (pid=${proc.pid}): ${err}`
261
+ )
262
+ )
263
+ );
264
+ })
265
+ );
266
+ return yield* child.exitCode;
267
+ });
268
+
269
+ export {
270
+ DevSessions,
271
+ makeDevSessionsLayer,
272
+ runDevCli,
273
+ runningSignalPath,
274
+ resolveSiblingDir,
275
+ publishRunningSignal,
276
+ awaitRunningSignal,
277
+ SessionStateError,
278
+ SessionState,
279
+ getStickyPort,
280
+ runManagedSubprocess
281
+ };
282
+ //# sourceMappingURL=chunk-IEQZ3AJH.js.map