devsess 0.3.4 → 0.4.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.
@@ -0,0 +1,45 @@
1
+ import { Context, Effect, Layer } from 'effect';
2
+ import { FileSystem } from 'effect/FileSystem';
3
+ import { Path } from 'effect/Path';
4
+ import type { Scope } from 'effect/Scope';
5
+ import { CurrentSession } from '../current-session';
6
+ import type { DevSession } from '../dev-sessions';
7
+ import { type ContainerSpec } from './container';
8
+ import { type ServiceContext, ServiceError } from './core';
9
+ export type { Healthcheck } from './docker-args';
10
+ export { type ServiceContext, ServiceError };
11
+ export type RunningService<Port extends string, A> = (A extends object ? Omit<A, 'ports'> : unknown) & {
12
+ readonly ports: Readonly<Record<Port, number>>;
13
+ };
14
+ type ServiceIdentifier<Name extends string> = {
15
+ readonly devsessService: Name;
16
+ };
17
+ export type ServiceDefinition<Name extends string, Port extends string, A, E, R> = {
18
+ readonly name: Name;
19
+ readonly ports: ReadonlyArray<Port>;
20
+ /** Resolves once the service is ready; its scope closing stops it. */
21
+ readonly start: (ctx: ServiceContext<Port>) => Effect.Effect<A, E, R>;
22
+ /** Yield inside another service's `start` (or any effect) to depend on this one. */
23
+ readonly key: Context.Key<ServiceIdentifier<Name>, RunningService<Port, A>>;
24
+ /** Starts the service once per layer build and shares it with every consumer. */
25
+ readonly layer: Layer.Layer<ServiceIdentifier<Name>, E | ServiceError, Exclude<R, Scope> | CurrentSession | FileSystem | Path>;
26
+ };
27
+ declare const run: <Name extends string, Port extends string, A, E, R>(session: DevSession, def: Pick<ServiceDefinition<Name, Port, A, E, R>, 'name' | 'ports' | 'start'>) => Effect.Effect<RunningService<Port, A>, E | ServiceError, R | FileSystem | Path>;
28
+ declare const make: <const Name extends string, A, E, R, const Port extends string = never>(def: {
29
+ name: Name;
30
+ ports?: ReadonlyArray<Port>;
31
+ start: (ctx: ServiceContext<Port>) => Effect.Effect<A, E, R>;
32
+ }) => ServiceDefinition<Name, Port, A, E, R>;
33
+ export declare const Service: {
34
+ /** Defines a service whose `start` handler you write yourself. */
35
+ make: typeof make;
36
+ /** Defines a service backed by a Docker container, ready once its healthcheck passes. */
37
+ container: <const Name extends string, const Port extends string = never>(def: {
38
+ name: Name;
39
+ } & ContainerSpec<Port>) => ServiceDefinition<Name, Port, {
40
+ container: string;
41
+ }, ServiceError, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | import("..").DevSessions | FileSystem | Path | Scope>;
42
+ /** Starts a service in `session`, outside a layer. */
43
+ run: typeof run;
44
+ };
45
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,312 @@
1
+ import {
2
+ CurrentSession
3
+ } from "../chunk-CBRTTFXG.js";
4
+ import {
5
+ DevSessions,
6
+ getStickyPort
7
+ } from "../chunk-GX4J6JR3.js";
8
+
9
+ // src/services/index.ts
10
+ import { Context, Effect as Effect2, Layer } from "effect";
11
+ import { FileSystem as FileSystem2 } from "effect/FileSystem";
12
+ import { Path as Path2 } from "effect/Path";
13
+
14
+ // src/services/container.ts
15
+ import { Effect, Stream } from "effect";
16
+ import { FileSystem } from "effect/FileSystem";
17
+ import { Path } from "effect/Path";
18
+ import { ChildProcess } from "effect/process";
19
+
20
+ // src/services/core.ts
21
+ import { Data } from "effect";
22
+ var ServiceError = class extends Data.TaggedError("ServiceError") {
23
+ };
24
+ var validName = (name) => /^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name);
25
+
26
+ // src/services/docker-args.ts
27
+ import { Duration } from "effect";
28
+ var dockerDuration = (input) => `${Duration.toMillis(input)}ms`;
29
+ var shellQuote = (part) => `'${part.replaceAll("'", "'\\''")}'`;
30
+ var healthCmd = (test) => {
31
+ if (test[0] === "CMD-SHELL") return test.slice(1).join(" ");
32
+ if (test[0] === "CMD") return test.slice(1).map(shellQuote).join(" ");
33
+ return test.join(" ");
34
+ };
35
+ var buildRunArgs = (input) => {
36
+ const args = [
37
+ "run",
38
+ "-d",
39
+ "--name",
40
+ input.container,
41
+ "--label",
42
+ `devsess.session=${input.session}`,
43
+ "--label",
44
+ `devsess.root=${input.root}`,
45
+ "--user",
46
+ `${input.uid}:${input.gid}`
47
+ ];
48
+ for (const port of input.ports)
49
+ args.push("-p", `127.0.0.1:${port.host}:${port.container}`);
50
+ for (const volume of input.volumes)
51
+ args.push("-v", `${volume.source}:${volume.target}`);
52
+ for (const entry of Object.entries(input.env))
53
+ args.push("-e", `${entry[0]}=${entry[1]}`);
54
+ const healthcheck = input.healthcheck;
55
+ if (healthcheck) {
56
+ args.push("--health-cmd", healthCmd(healthcheck.test));
57
+ if (healthcheck.interval !== void 0)
58
+ args.push("--health-interval", dockerDuration(healthcheck.interval));
59
+ if (healthcheck.timeout !== void 0)
60
+ args.push("--health-timeout", dockerDuration(healthcheck.timeout));
61
+ if (healthcheck.retries !== void 0)
62
+ args.push("--health-retries", String(healthcheck.retries));
63
+ if (healthcheck.startPeriod !== void 0)
64
+ args.push(
65
+ "--health-start-period",
66
+ dockerDuration(healthcheck.startPeriod)
67
+ );
68
+ }
69
+ args.push(input.image);
70
+ return args;
71
+ };
72
+ var selectOrphans = (containers, existingSessions) => containers.filter((container) => !existingSessions.has(container.session)).map((container) => container.id);
73
+
74
+ // src/services/container.ts
75
+ var docker = (args, options) => Effect.gen(function* () {
76
+ const command = ChildProcess.make("docker", args, {
77
+ stdout: options?.progress ? "inherit" : "pipe",
78
+ stderr: "inherit"
79
+ });
80
+ const result = yield* Effect.scoped(
81
+ Effect.gen(function* () {
82
+ const child = yield* command;
83
+ const output = options?.progress ? void 0 : (yield* Stream.mkString(Stream.decodeText(child.stdout))).trim();
84
+ const code = yield* child.exitCode;
85
+ return { output, code };
86
+ })
87
+ ).pipe(
88
+ Effect.mapError(
89
+ (cause) => new ServiceError({
90
+ message: `Docker command failed: docker ${args.join(" ")}`,
91
+ cause
92
+ })
93
+ )
94
+ );
95
+ if (result.code !== 0 && !options?.allowFailure) {
96
+ return yield* new ServiceError({
97
+ message: `Docker command failed (exit ${result.code}): docker ${args.join(" ")}`
98
+ });
99
+ }
100
+ return result;
101
+ });
102
+ var listContainers = (root) => Effect.gen(function* () {
103
+ const listed = yield* docker([
104
+ "ps",
105
+ "-a",
106
+ "--filter",
107
+ `label=devsess.root=${root}`,
108
+ "--format",
109
+ '{{.ID}} {{.Label "devsess.session"}}'
110
+ ]);
111
+ if (listed.output === void 0)
112
+ return yield* new ServiceError({
113
+ message: "Docker container listing returned no output"
114
+ });
115
+ const containers = [];
116
+ for (const line of listed.output.split("\n").filter(Boolean)) {
117
+ const fields = line.split(" ");
118
+ const id = fields[0];
119
+ const session = fields[1];
120
+ if (!id || !session)
121
+ return yield* new ServiceError({
122
+ message: `Invalid devsess container metadata: ${line}`
123
+ });
124
+ containers.push({ id, session });
125
+ }
126
+ return containers;
127
+ });
128
+ var waitHealthy = (name, container) => Effect.gen(function* () {
129
+ while (true) {
130
+ const state = yield* docker([
131
+ "inspect",
132
+ "--format",
133
+ "{{.State.Running}} {{.State.Health.Status}}",
134
+ container
135
+ ]);
136
+ if (state.output === void 0)
137
+ return yield* new ServiceError({
138
+ message: `Docker returned no status for ${container}`
139
+ });
140
+ if (state.output === "true healthy") return;
141
+ if (state.output.startsWith("false "))
142
+ return yield* new ServiceError({
143
+ message: `Service ${name} exited before healthy; services run as your user; this image may require root`
144
+ });
145
+ if (state.output === "true unhealthy")
146
+ return yield* new ServiceError({
147
+ message: `Service ${name} is unhealthy`
148
+ });
149
+ yield* Effect.sleep("500 millis");
150
+ }
151
+ });
152
+ var startContainer = (name, spec) => (ctx) => Effect.gen(function* () {
153
+ const fs = yield* FileSystem;
154
+ const path = yield* Path;
155
+ const sessions = yield* DevSessions;
156
+ const known = new Set(
157
+ (yield* sessions.getSessions).map((existing) => existing.name)
158
+ );
159
+ const containers = yield* listContainers(sessions.dir);
160
+ for (const id of selectOrphans(containers, known))
161
+ yield* docker(["rm", "-f", id]);
162
+ const container = `devsess-${ctx.session.name}-${name}`;
163
+ const portBindings = Object.entries(spec.ports ?? {}).map(
164
+ (entry) => ({
165
+ host: ctx.ports[entry[0]],
166
+ container: entry[1]
167
+ })
168
+ );
169
+ const volumes = [];
170
+ for (const entry of Object.entries(spec.volumes ?? {})) {
171
+ if (!validName(entry[0]))
172
+ return yield* new ServiceError({
173
+ message: `Invalid volume name: ${entry[0]}`
174
+ });
175
+ volumes.push({
176
+ source: path.join(ctx.dataDir, entry[0]),
177
+ target: entry[1]
178
+ });
179
+ }
180
+ const start = Effect.gen(function* () {
181
+ const found = yield* docker(
182
+ [
183
+ "container",
184
+ "inspect",
185
+ "--format",
186
+ '{{.State.Running}} {{index .Config.Labels "devsess.root"}} {{index .Config.Labels "devsess.session"}}',
187
+ container
188
+ ],
189
+ { allowFailure: true }
190
+ );
191
+ const identity = found.output?.split(" ");
192
+ if (found.code === 0 && (identity?.[1] !== sessions.dir || identity[2] !== ctx.session.name))
193
+ return yield* new ServiceError({
194
+ message: `Container name ${container} belongs to another session or project`
195
+ });
196
+ if (found.code === 0 && identity?.[0] === "true") return;
197
+ if (found.code === 0) yield* docker(["rm", "-f", container]);
198
+ if (!process.getuid || !process.getgid)
199
+ return yield* new ServiceError({
200
+ message: "Docker services require a POSIX user and group ID"
201
+ });
202
+ for (const volume of volumes)
203
+ yield* fs.makeDirectory(volume.source, { recursive: true });
204
+ const image = yield* docker(
205
+ ["image", "inspect", "--format", "{{.Id}}", spec.image],
206
+ { allowFailure: true }
207
+ );
208
+ if (image.code !== 0) {
209
+ yield* Effect.logInfo(`[dev] pulling ${spec.image}`);
210
+ yield* docker(["pull", spec.image], { progress: true });
211
+ }
212
+ yield* docker(
213
+ buildRunArgs({
214
+ image: spec.image,
215
+ container,
216
+ root: sessions.dir,
217
+ session: ctx.session.name,
218
+ uid: process.getuid(),
219
+ gid: process.getgid(),
220
+ ports: portBindings,
221
+ volumes,
222
+ env: spec.env ?? {},
223
+ healthcheck: spec.healthcheck
224
+ })
225
+ );
226
+ });
227
+ yield* Effect.acquireRelease(
228
+ start,
229
+ () => docker(["rm", "-f", container]).pipe(
230
+ Effect.catch(
231
+ (error) => Effect.logError(`[dev] failed to remove ${container}: ${error}`)
232
+ )
233
+ )
234
+ );
235
+ if (spec.healthcheck) yield* waitHealthy(name, container);
236
+ return { container };
237
+ }).pipe(
238
+ Effect.mapError(
239
+ (cause) => cause instanceof ServiceError ? cause : new ServiceError({
240
+ message: `Failed to start container for service ${name}`,
241
+ cause
242
+ })
243
+ )
244
+ );
245
+
246
+ // src/services/index.ts
247
+ var run = (session, def) => Effect2.gen(function* () {
248
+ if (!validName(def.name) || !validName(session.name)) {
249
+ return yield* new ServiceError({
250
+ message: "Service and session names must contain only letters, numbers, dots, underscores, or hyphens"
251
+ });
252
+ }
253
+ const fs = yield* FileSystem2;
254
+ const path = yield* Path2;
255
+ const dataDir = path.join(yield* session.path(""), "services", def.name);
256
+ const ports = {};
257
+ for (const port of def.ports)
258
+ ports[port] = yield* getStickyPort(session, {
259
+ name: `${def.name}:${port}`
260
+ });
261
+ yield* fs.makeDirectory(dataDir, { recursive: true });
262
+ return { ports, dataDir };
263
+ }).pipe(
264
+ Effect2.mapError(
265
+ (cause) => cause instanceof ServiceError ? cause : new ServiceError({
266
+ message: `Failed to prepare service ${def.name}`,
267
+ cause
268
+ })
269
+ ),
270
+ Effect2.flatMap(
271
+ (prepared) => def.start({ session, ports: prepared.ports, dataDir: prepared.dataDir }).pipe(
272
+ Effect2.map(
273
+ (value) => ({
274
+ ...typeof value === "object" && value !== null ? value : {},
275
+ ports: prepared.ports
276
+ })
277
+ )
278
+ )
279
+ ),
280
+ Effect2.annotateLogs({ service: def.name })
281
+ );
282
+ var make = (def) => {
283
+ const base = { name: def.name, ports: def.ports ?? [], start: def.start };
284
+ const key = Context.Service(
285
+ `devsess/services/${def.name}`
286
+ );
287
+ const layer = Layer.effect(
288
+ key,
289
+ Effect2.gen(function* () {
290
+ const session = yield* CurrentSession;
291
+ return yield* run(session, base);
292
+ })
293
+ );
294
+ return { ...base, key, layer };
295
+ };
296
+ var Service = {
297
+ /** Defines a service whose `start` handler you write yourself. */
298
+ make,
299
+ /** Defines a service backed by a Docker container, ready once its healthcheck passes. */
300
+ container: (def) => make({
301
+ name: def.name,
302
+ ports: Object.keys(def.ports ?? {}),
303
+ start: startContainer(def.name, def)
304
+ }),
305
+ /** Starts a service in `session`, outside a layer. */
306
+ run
307
+ };
308
+ export {
309
+ Service,
310
+ ServiceError
311
+ };
312
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/services/index.ts","../../src/services/container.ts","../../src/services/core.ts","../../src/services/docker-args.ts"],"sourcesContent":["import { Context, Effect, Layer } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport type { Scope } from 'effect/Scope';\nimport { CurrentSession } from '../current-session';\nimport { getStickyPort } from '../dev/sticky-port';\nimport type { DevSession } from '../dev-sessions';\nimport { type ContainerSpec, startContainer } from './container';\nimport { type ServiceContext, ServiceError, validName } from './core';\n\nexport type { Healthcheck } from './docker-args';\nexport { type ServiceContext, ServiceError };\n\nexport type RunningService<Port extends string, A> = (A extends object\n\t? Omit<A, 'ports'>\n\t: unknown) & { readonly ports: Readonly<Record<Port, number>> };\n\ntype ServiceIdentifier<Name extends string> = {\n\treadonly devsessService: Name;\n};\n\nexport type ServiceDefinition<\n\tName extends string,\n\tPort extends string,\n\tA,\n\tE,\n\tR,\n> = {\n\treadonly name: Name;\n\treadonly ports: ReadonlyArray<Port>;\n\t/** Resolves once the service is ready; its scope closing stops it. */\n\treadonly start: (ctx: ServiceContext<Port>) => Effect.Effect<A, E, R>;\n\t/** Yield inside another service's `start` (or any effect) to depend on this one. */\n\treadonly key: Context.Key<ServiceIdentifier<Name>, RunningService<Port, A>>;\n\t/** Starts the service once per layer build and shares it with every consumer. */\n\treadonly layer: Layer.Layer<\n\t\tServiceIdentifier<Name>,\n\t\tE | ServiceError,\n\t\tExclude<R, Scope> | CurrentSession | FileSystem | Path\n\t>;\n};\n\nconst run = <Name extends string, Port extends string, A, E, R>(\n\tsession: DevSession,\n\tdef: Pick<ServiceDefinition<Name, Port, A, E, R>, 'name' | 'ports' | 'start'>,\n) =>\n\tEffect.gen(function* () {\n\t\tif (!validName(def.name) || !validName(session.name)) {\n\t\t\treturn yield* new ServiceError({\n\t\t\t\tmessage:\n\t\t\t\t\t'Service and session names must contain only letters, numbers, dots, underscores, or hyphens',\n\t\t\t});\n\t\t}\n\t\tconst fs = yield* FileSystem;\n\t\tconst path = yield* Path;\n\t\tconst dataDir = path.join(yield* session.path(''), 'services', def.name);\n\t\tconst ports = {} as Record<Port, number>;\n\t\tfor (const port of def.ports)\n\t\t\tports[port] = yield* getStickyPort(session, {\n\t\t\t\tname: `${def.name}:${port}`,\n\t\t\t});\n\t\tyield* fs.makeDirectory(dataDir, { recursive: true });\n\t\treturn { ports, dataDir };\n\t}).pipe(\n\t\tEffect.mapError((cause) =>\n\t\t\tcause instanceof ServiceError\n\t\t\t\t? cause\n\t\t\t\t: new ServiceError({\n\t\t\t\t\t\tmessage: `Failed to prepare service ${def.name}`,\n\t\t\t\t\t\tcause,\n\t\t\t\t\t}),\n\t\t),\n\t\tEffect.flatMap((prepared) =>\n\t\t\tdef\n\t\t\t\t.start({ session, ports: prepared.ports, dataDir: prepared.dataDir })\n\t\t\t\t.pipe(\n\t\t\t\t\tEffect.map(\n\t\t\t\t\t\t(value) =>\n\t\t\t\t\t\t\t({\n\t\t\t\t\t\t\t\t...(typeof value === 'object' && value !== null ? value : {}),\n\t\t\t\t\t\t\t\tports: prepared.ports,\n\t\t\t\t\t\t\t}) as RunningService<Port, A>,\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t),\n\t\tEffect.annotateLogs({ service: def.name }),\n\t);\n\nconst make = <\n\tconst Name extends string,\n\tA,\n\tE,\n\tR,\n\tconst Port extends string = never,\n>(def: {\n\tname: Name;\n\tports?: ReadonlyArray<Port>;\n\tstart: (ctx: ServiceContext<Port>) => Effect.Effect<A, E, R>;\n}): ServiceDefinition<Name, Port, A, E, R> => {\n\tconst base = { name: def.name, ports: def.ports ?? [], start: def.start };\n\tconst key = Context.Service<ServiceIdentifier<Name>, RunningService<Port, A>>(\n\t\t`devsess/services/${def.name}`,\n\t);\n\tconst layer = Layer.effect(\n\t\tkey,\n\t\tEffect.gen(function* () {\n\t\t\tconst session = yield* CurrentSession;\n\t\t\treturn yield* run(session, base);\n\t\t}),\n\t);\n\treturn { ...base, key, layer };\n};\n\nexport const Service = {\n\t/** Defines a service whose `start` handler you write yourself. */\n\tmake,\n\t/** Defines a service backed by a Docker container, ready once its healthcheck passes. */\n\tcontainer: <const Name extends string, const Port extends string = never>(\n\t\tdef: { name: Name } & ContainerSpec<Port>,\n\t) =>\n\t\tmake({\n\t\t\tname: def.name,\n\t\t\tports: Object.keys(def.ports ?? {}) as Port[],\n\t\t\tstart: startContainer(def.name, def),\n\t\t}),\n\t/** Starts a service in `session`, outside a layer. */\n\trun,\n};\n","import { Effect, Stream } from 'effect';\nimport { FileSystem } from 'effect/FileSystem';\nimport { Path } from 'effect/Path';\nimport { ChildProcess } from 'effect/process';\nimport { DevSessions } from '../dev-sessions';\nimport { type ServiceContext, ServiceError, validName } from './core';\nimport { buildRunArgs, type Healthcheck, selectOrphans } from './docker-args';\n\nexport type ContainerSpec<Port extends string> = {\n\timage: string;\n\tports?: Record<Port, number>;\n\tvolumes?: Record<string, string>;\n\tenv?: Record<string, string>;\n\thealthcheck?: Healthcheck;\n};\n\nconst docker = (\n\targs: string[],\n\toptions?: { progress?: boolean; allowFailure?: boolean },\n) =>\n\tEffect.gen(function* () {\n\t\tconst command = ChildProcess.make('docker', args, {\n\t\t\tstdout: options?.progress ? 'inherit' : 'pipe',\n\t\t\tstderr: 'inherit',\n\t\t});\n\t\tconst result = yield* Effect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst child = yield* command;\n\t\t\t\tconst output = options?.progress\n\t\t\t\t\t? undefined\n\t\t\t\t\t: (yield* Stream.mkString(Stream.decodeText(child.stdout))).trim();\n\t\t\t\tconst code = yield* child.exitCode;\n\t\t\t\treturn { output, code };\n\t\t\t}),\n\t\t).pipe(\n\t\t\tEffect.mapError(\n\t\t\t\t(cause) =>\n\t\t\t\t\tnew ServiceError({\n\t\t\t\t\t\tmessage: `Docker command failed: docker ${args.join(' ')}`,\n\t\t\t\t\t\tcause,\n\t\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tif (result.code !== 0 && !options?.allowFailure) {\n\t\t\treturn yield* new ServiceError({\n\t\t\t\tmessage: `Docker command failed (exit ${result.code}): docker ${args.join(' ')}`,\n\t\t\t});\n\t\t}\n\t\treturn result;\n\t});\n\nconst listContainers = (root: string) =>\n\tEffect.gen(function* () {\n\t\tconst listed = yield* docker([\n\t\t\t'ps',\n\t\t\t'-a',\n\t\t\t'--filter',\n\t\t\t`label=devsess.root=${root}`,\n\t\t\t'--format',\n\t\t\t'{{.ID}}\\t{{.Label \"devsess.session\"}}',\n\t\t]);\n\t\tif (listed.output === undefined)\n\t\t\treturn yield* new ServiceError({\n\t\t\t\tmessage: 'Docker container listing returned no output',\n\t\t\t});\n\t\tconst containers: Array<{ id: string; session: string }> = [];\n\t\tfor (const line of listed.output.split('\\n').filter(Boolean)) {\n\t\t\tconst fields = line.split('\\t');\n\t\t\tconst id = fields[0];\n\t\t\tconst session = fields[1];\n\t\t\tif (!id || !session)\n\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\tmessage: `Invalid devsess container metadata: ${line}`,\n\t\t\t\t});\n\t\t\tcontainers.push({ id, session });\n\t\t}\n\t\treturn containers;\n\t});\n\nconst waitHealthy = (name: string, container: string) =>\n\tEffect.gen(function* () {\n\t\twhile (true) {\n\t\t\tconst state = yield* docker([\n\t\t\t\t'inspect',\n\t\t\t\t'--format',\n\t\t\t\t'{{.State.Running}} {{.State.Health.Status}}',\n\t\t\t\tcontainer,\n\t\t\t]);\n\t\t\tif (state.output === undefined)\n\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\tmessage: `Docker returned no status for ${container}`,\n\t\t\t\t});\n\t\t\tif (state.output === 'true healthy') return;\n\t\t\tif (state.output.startsWith('false '))\n\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\tmessage: `Service ${name} exited before healthy; services run as your user; this image may require root`,\n\t\t\t\t});\n\t\t\tif (state.output === 'true unhealthy')\n\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\tmessage: `Service ${name} is unhealthy`,\n\t\t\t\t});\n\t\t\tyield* Effect.sleep('500 millis');\n\t\t}\n\t});\n\nexport const startContainer =\n\t<Port extends string>(name: string, spec: ContainerSpec<Port>) =>\n\t(ctx: ServiceContext<Port>) =>\n\t\tEffect.gen(function* () {\n\t\t\tconst fs = yield* FileSystem;\n\t\t\tconst path = yield* Path;\n\t\t\tconst sessions = yield* DevSessions;\n\n\t\t\tconst known = new Set(\n\t\t\t\t(yield* sessions.getSessions).map((existing) => existing.name),\n\t\t\t);\n\t\t\tconst containers = yield* listContainers(sessions.dir);\n\t\t\tfor (const id of selectOrphans(containers, known))\n\t\t\t\tyield* docker(['rm', '-f', id]);\n\n\t\t\tconst container = `devsess-${ctx.session.name}-${name}`;\n\t\t\tconst portBindings = Object.entries<number>(spec.ports ?? {}).map(\n\t\t\t\t(entry) => ({\n\t\t\t\t\thost: ctx.ports[entry[0] as Port],\n\t\t\t\t\tcontainer: entry[1],\n\t\t\t\t}),\n\t\t\t);\n\t\t\tconst volumes: Array<{ source: string; target: string }> = [];\n\t\t\tfor (const entry of Object.entries(spec.volumes ?? {})) {\n\t\t\t\tif (!validName(entry[0]))\n\t\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\t\tmessage: `Invalid volume name: ${entry[0]}`,\n\t\t\t\t\t});\n\t\t\t\tvolumes.push({\n\t\t\t\t\tsource: path.join(ctx.dataDir, entry[0]),\n\t\t\t\t\ttarget: entry[1],\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconst start = Effect.gen(function* () {\n\t\t\t\tconst found = yield* docker(\n\t\t\t\t\t[\n\t\t\t\t\t\t'container',\n\t\t\t\t\t\t'inspect',\n\t\t\t\t\t\t'--format',\n\t\t\t\t\t\t'{{.State.Running}}\\t{{index .Config.Labels \"devsess.root\"}}\\t{{index .Config.Labels \"devsess.session\"}}',\n\t\t\t\t\t\tcontainer,\n\t\t\t\t\t],\n\t\t\t\t\t{ allowFailure: true },\n\t\t\t\t);\n\t\t\t\tconst identity = found.output?.split('\\t');\n\t\t\t\tif (\n\t\t\t\t\tfound.code === 0 &&\n\t\t\t\t\t(identity?.[1] !== sessions.dir || identity[2] !== ctx.session.name)\n\t\t\t\t)\n\t\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\t\tmessage: `Container name ${container} belongs to another session or project`,\n\t\t\t\t\t});\n\t\t\t\tif (found.code === 0 && identity?.[0] === 'true') return;\n\t\t\t\tif (found.code === 0) yield* docker(['rm', '-f', container]);\n\t\t\t\tif (!process.getuid || !process.getgid)\n\t\t\t\t\treturn yield* new ServiceError({\n\t\t\t\t\t\tmessage: 'Docker services require a POSIX user and group ID',\n\t\t\t\t\t});\n\t\t\t\tfor (const volume of volumes)\n\t\t\t\t\tyield* fs.makeDirectory(volume.source, { recursive: true });\n\t\t\t\tconst image = yield* docker(\n\t\t\t\t\t['image', 'inspect', '--format', '{{.Id}}', spec.image],\n\t\t\t\t\t{ allowFailure: true },\n\t\t\t\t);\n\t\t\t\tif (image.code !== 0) {\n\t\t\t\t\tyield* Effect.logInfo(`[dev] pulling ${spec.image}`);\n\t\t\t\t\tyield* docker(['pull', spec.image], { progress: true });\n\t\t\t\t}\n\t\t\t\tyield* docker(\n\t\t\t\t\tbuildRunArgs({\n\t\t\t\t\t\timage: spec.image,\n\t\t\t\t\t\tcontainer,\n\t\t\t\t\t\troot: sessions.dir,\n\t\t\t\t\t\tsession: ctx.session.name,\n\t\t\t\t\t\tuid: process.getuid(),\n\t\t\t\t\t\tgid: process.getgid(),\n\t\t\t\t\t\tports: portBindings,\n\t\t\t\t\t\tvolumes,\n\t\t\t\t\t\tenv: spec.env ?? {},\n\t\t\t\t\t\thealthcheck: spec.healthcheck,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t});\n\t\t\tyield* Effect.acquireRelease(start, () =>\n\t\t\t\tdocker(['rm', '-f', container]).pipe(\n\t\t\t\t\tEffect.catch((error) =>\n\t\t\t\t\t\tEffect.logError(`[dev] failed to remove ${container}: ${error}`),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t);\n\t\t\tif (spec.healthcheck) yield* waitHealthy(name, container);\n\t\t\treturn { container };\n\t\t}).pipe(\n\t\t\tEffect.mapError((cause) =>\n\t\t\t\tcause instanceof ServiceError\n\t\t\t\t\t? cause\n\t\t\t\t\t: new ServiceError({\n\t\t\t\t\t\t\tmessage: `Failed to start container for service ${name}`,\n\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t}),\n\t\t\t),\n\t\t);\n","import { Data } from 'effect';\nimport type { DevSession } from '../dev-sessions';\n\nexport class ServiceError extends Data.TaggedError('ServiceError')<{\n\tmessage: string;\n\tcause?: unknown;\n}> {}\n\nexport type ServiceContext<Port extends string> = {\n\treadonly session: DevSession;\n\treadonly ports: Readonly<Record<Port, number>>;\n\t/** `<session>/services/<service>`, created before `start` runs and kept after it stops. */\n\treadonly dataDir: string;\n};\n\nexport const validName = (name: string) =>\n\t/^[a-zA-Z0-9][a-zA-Z0-9_.-]*$/.test(name);\n","import { Duration } from 'effect';\n\nexport type Healthcheck = {\n\ttest: readonly string[];\n\tinterval?: Duration.Input;\n\ttimeout?: Duration.Input;\n\tretries?: number;\n\tstartPeriod?: Duration.Input;\n};\n\nconst dockerDuration = (input: Duration.Input) =>\n\t`${Duration.toMillis(input)}ms`;\n\nconst shellQuote = (part: string) => `'${part.replaceAll(\"'\", \"'\\\\''\")}'`;\n\n// `docker run --health-cmd` always goes through a shell, unlike compose's exec-form `CMD`.\nconst healthCmd = (test: readonly string[]) => {\n\tif (test[0] === 'CMD-SHELL') return test.slice(1).join(' ');\n\tif (test[0] === 'CMD') return test.slice(1).map(shellQuote).join(' ');\n\treturn test.join(' ');\n};\n\nexport const buildRunArgs = (input: {\n\timage: string;\n\tcontainer: string;\n\troot: string;\n\tsession: string;\n\tuid: number;\n\tgid: number;\n\tports: ReadonlyArray<{ host: number; container: number }>;\n\tvolumes: ReadonlyArray<{ source: string; target: string }>;\n\tenv: Record<string, string>;\n\thealthcheck: Healthcheck | undefined;\n}): string[] => {\n\tconst args = [\n\t\t'run',\n\t\t'-d',\n\t\t'--name',\n\t\tinput.container,\n\t\t'--label',\n\t\t`devsess.session=${input.session}`,\n\t\t'--label',\n\t\t`devsess.root=${input.root}`,\n\t\t'--user',\n\t\t`${input.uid}:${input.gid}`,\n\t];\n\tfor (const port of input.ports)\n\t\targs.push('-p', `127.0.0.1:${port.host}:${port.container}`);\n\tfor (const volume of input.volumes)\n\t\targs.push('-v', `${volume.source}:${volume.target}`);\n\tfor (const entry of Object.entries(input.env))\n\t\targs.push('-e', `${entry[0]}=${entry[1]}`);\n\tconst healthcheck = input.healthcheck;\n\tif (healthcheck) {\n\t\targs.push('--health-cmd', healthCmd(healthcheck.test));\n\t\tif (healthcheck.interval !== undefined)\n\t\t\targs.push('--health-interval', dockerDuration(healthcheck.interval));\n\t\tif (healthcheck.timeout !== undefined)\n\t\t\targs.push('--health-timeout', dockerDuration(healthcheck.timeout));\n\t\tif (healthcheck.retries !== undefined)\n\t\t\targs.push('--health-retries', String(healthcheck.retries));\n\t\tif (healthcheck.startPeriod !== undefined)\n\t\t\targs.push(\n\t\t\t\t'--health-start-period',\n\t\t\t\tdockerDuration(healthcheck.startPeriod),\n\t\t\t);\n\t}\n\targs.push(input.image);\n\treturn args;\n};\n\nexport const selectOrphans = (\n\tcontainers: ReadonlyArray<{ id: string; session: string }>,\n\texistingSessions: ReadonlySet<string>,\n) =>\n\tcontainers\n\t\t.filter((container) => !existingSessions.has(container.session))\n\t\t.map((container) => container.id);\n"],"mappings":"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","names":["Effect","FileSystem","Path","Effect","FileSystem","Path"]}
package/package.json CHANGED
@@ -1,89 +1,93 @@
1
1
  {
2
- "name": "devsess",
3
- "version": "0.3.4",
4
- "description": "Isolated, managed dev servers — each worktree gets its own session, with its own port and its own database",
5
- "license": "Apache-2.0",
6
- "homepage": "https://github.com/fdarian/devsess#readme",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/fdarian/devsess.git",
10
- "directory": "packages/devsess"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/fdarian/devsess/issues"
14
- },
15
- "type": "module",
16
- "keywords": [
17
- "effect",
18
- "dev",
19
- "cli",
20
- "dev-server",
21
- "pglite",
22
- "drizzle",
23
- "sqlite",
24
- "postgres"
25
- ],
26
- "sideEffects": false,
27
- "files": [
28
- "dist",
29
- "README.md"
30
- ],
31
- "main": "./dist/index.js",
32
- "module": "./dist/index.js",
33
- "types": "./dist/index.d.ts",
34
- "exports": {
35
- ".": {
36
- "types": "./dist/index.d.ts",
37
- "import": "./dist/index.js"
38
- },
39
- "./pglite": {
40
- "types": "./dist/pglite/index.d.ts",
41
- "import": "./dist/pglite/index.js"
42
- },
43
- "./async": {
44
- "types": "./dist/async/index.d.ts",
45
- "import": "./dist/async/index.js"
46
- },
47
- "./package.json": "./package.json"
48
- },
49
- "publishConfig": {
50
- "access": "public",
51
- "provenance": true
52
- },
53
- "scripts": {
54
- "build": "tsup && tsc -p tsconfig.build.json",
55
- "dev": "tsup --watch",
56
- "check:type": "tsc --noEmit",
57
- "check:lint": "biome check",
58
- "test": "vitest run",
59
- "prepack": "bun run build"
60
- },
61
- "dependencies": {
62
- "get-port": "^7.2.0",
63
- "random-word-slugs": "^0.1.7"
64
- },
65
- "peerDependencies": {
66
- "@electric-sql/pglite": ">=0.2.0",
67
- "drizzle-orm": ">=0.30.0",
68
- "effect": "^4.0.0-beta.101"
69
- },
70
- "peerDependenciesMeta": {
71
- "@electric-sql/pglite": {
72
- "optional": true
73
- },
74
- "drizzle-orm": {
75
- "optional": true
76
- }
77
- },
78
- "devDependencies": {
79
- "@devsess/config": "workspace:*",
80
- "@effect/platform-node": "^4.0.0-beta.101",
81
- "@effect/vitest": "^4.0.0-beta.101",
82
- "@electric-sql/pglite": "^0.5.3",
83
- "@types/node": "^22.0.0",
84
- "drizzle-orm": "^0.45.2",
85
- "effect": "^4.0.0-beta.101",
86
- "tsup": "^8.5.1",
87
- "vitest": "^4.1.10"
88
- }
89
- }
2
+ "name": "devsess",
3
+ "version": "0.4.0",
4
+ "description": "Isolated, managed dev servers — each worktree gets its own session, with its own port and its own database",
5
+ "license": "Apache-2.0",
6
+ "homepage": "https://github.com/fdarian/devsess#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/fdarian/devsess.git",
10
+ "directory": "packages/devsess"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/fdarian/devsess/issues"
14
+ },
15
+ "type": "module",
16
+ "keywords": [
17
+ "effect",
18
+ "dev",
19
+ "cli",
20
+ "dev-server",
21
+ "pglite",
22
+ "drizzle",
23
+ "sqlite",
24
+ "postgres"
25
+ ],
26
+ "sideEffects": false,
27
+ "files": [
28
+ "dist",
29
+ "README.md"
30
+ ],
31
+ "main": "./dist/index.js",
32
+ "module": "./dist/index.js",
33
+ "types": "./dist/index.d.ts",
34
+ "exports": {
35
+ ".": {
36
+ "types": "./dist/index.d.ts",
37
+ "import": "./dist/index.js"
38
+ },
39
+ "./pglite": {
40
+ "types": "./dist/pglite/index.d.ts",
41
+ "import": "./dist/pglite/index.js"
42
+ },
43
+ "./services": {
44
+ "types": "./dist/services/index.d.ts",
45
+ "import": "./dist/services/index.js"
46
+ },
47
+ "./async": {
48
+ "types": "./dist/async/index.d.ts",
49
+ "import": "./dist/async/index.js"
50
+ },
51
+ "./package.json": "./package.json"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public",
55
+ "provenance": true
56
+ },
57
+ "dependencies": {
58
+ "get-port": "^7.2.0",
59
+ "random-word-slugs": "^0.1.7"
60
+ },
61
+ "peerDependencies": {
62
+ "@electric-sql/pglite": ">=0.2.0",
63
+ "drizzle-orm": "^1.0.0-rc.5-5935859",
64
+ "effect": "^4.0.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@electric-sql/pglite": {
68
+ "optional": true
69
+ },
70
+ "drizzle-orm": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@devsess/config": "0.0.0",
76
+ "@effect/platform-node": "^4.0.0",
77
+ "@effect/vitest": "^4.0.0",
78
+ "@electric-sql/pglite": "^0.5.3",
79
+ "@types/node": "^22.0.0",
80
+ "drizzle-orm": "1.0.0-rc.5-5935859",
81
+ "effect": "^4.0.0",
82
+ "tsup": "^8.5.1",
83
+ "typescript": "^7.0.2",
84
+ "vitest": "^5.0.3"
85
+ },
86
+ "scripts": {
87
+ "build": "tsup && tsc -p tsconfig.build.json",
88
+ "dev": "tsup --watch",
89
+ "check:type": "tsc --noEmit",
90
+ "check:lint": "biome check",
91
+ "test": "vitest run"
92
+ }
93
+ }