@graftland/cli 0.3.3 → 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.
Files changed (54) hide show
  1. package/dist/Application.d.ts +27 -0
  2. package/dist/Application.d.ts.map +1 -0
  3. package/dist/Application.js +27 -0
  4. package/dist/Application.js.map +1 -0
  5. package/dist/Commands.d.ts +34 -0
  6. package/dist/Commands.d.ts.map +1 -0
  7. package/dist/Commands.js +31 -0
  8. package/dist/Commands.js.map +1 -0
  9. package/dist/Manage.d.ts +8 -5
  10. package/dist/Manage.d.ts.map +1 -1
  11. package/dist/Manage.js +60 -2
  12. package/dist/Manage.js.map +1 -1
  13. package/dist/Start.d.ts +2 -1
  14. package/dist/Start.d.ts.map +1 -1
  15. package/dist/Start.js +25 -9
  16. package/dist/Start.js.map +1 -1
  17. package/dist/bin.js +38 -11
  18. package/dist/bin.js.map +1 -1
  19. package/dist/index.d.ts +3 -1
  20. package/dist/index.js +3 -1
  21. package/dist/internal/commandConfig.js +62 -0
  22. package/dist/internal/commandConfig.js.map +1 -0
  23. package/dist/internal/commandHost.js +80 -0
  24. package/dist/internal/commandHost.js.map +1 -0
  25. package/dist/internal/commandRegistry.js +59 -0
  26. package/dist/internal/commandRegistry.js.map +1 -0
  27. package/dist/internal/configSchema.d.ts +25 -4
  28. package/dist/internal/configSchema.d.ts.map +1 -1
  29. package/dist/internal/configSchema.js +6 -34
  30. package/dist/internal/configSchema.js.map +1 -1
  31. package/dist/internal/env.js.map +1 -1
  32. package/dist/internal/importer.js +18 -0
  33. package/dist/internal/importer.js.map +1 -0
  34. package/dist/internal/intercepts.js.map +1 -1
  35. package/dist/internal/protocolOutput.js +67 -0
  36. package/dist/internal/protocolOutput.js.map +1 -0
  37. package/dist/internal/watch.js.map +1 -1
  38. package/package.json +10 -7
  39. package/schema.json +2321 -0
  40. package/src/Application.ts +55 -0
  41. package/src/Commands.ts +47 -0
  42. package/src/Manage.ts +81 -2
  43. package/src/Start.ts +100 -28
  44. package/src/bin.ts +80 -12
  45. package/src/index.ts +3 -0
  46. package/src/internal/commandConfig.ts +91 -0
  47. package/src/internal/commandHost.ts +216 -0
  48. package/src/internal/commandRegistry.ts +100 -0
  49. package/src/internal/configSchema.ts +10 -70
  50. package/src/internal/env.ts +3 -2
  51. package/src/internal/importer.ts +28 -0
  52. package/src/internal/intercepts.ts +2 -1
  53. package/src/internal/protocolOutput.ts +101 -0
  54. package/src/internal/watch.ts +4 -2
@@ -0,0 +1,216 @@
1
+ import {
2
+ Array,
3
+ Cause,
4
+ Console,
5
+ Effect,
6
+ Option,
7
+ Path,
8
+ Schema,
9
+ Stream,
10
+ String,
11
+ } from "effect"
12
+ import { CliError, CliOutput, Command } from "effect/cli"
13
+
14
+ import { Graft, Plugin, type Fork } from "@graftland/graft"
15
+
16
+ import { ProtocolOutput } from "../Application"
17
+ import { Commands, type Environment } from "../Commands"
18
+ import * as Configuration from "./commandConfig"
19
+ import * as Registry from "./commandRegistry"
20
+ import * as Importer from "./importer"
21
+ import { reserve } from "./protocolOutput"
22
+
23
+ class CommandPluginError extends Schema.TaggedError<
24
+ CommandPluginError,
25
+ { readonly brand: unique symbol }
26
+ >()("CommandPluginError", {
27
+ plugin: Schema.String,
28
+ detail: Schema.String,
29
+ cause: Schema.optionalKey(Schema.Cause(Schema.Unknown, Schema.Unknown)),
30
+ }) {
31
+ override get message() {
32
+ return `${this.plugin}: ${this.detail}`
33
+ }
34
+ }
35
+ const failure = (name: string, cause: Cause.Cause<unknown>) =>
36
+ CommandPluginError.make({
37
+ plugin: name,
38
+ detail: "Command plugin failed",
39
+ cause,
40
+ })
41
+ const observe = (fork: Fork.Fork, name: string) =>
42
+ fork.state.pipe(
43
+ Stream.runForEach((state) =>
44
+ state._tag === "Failed"
45
+ ? Effect.fail(failure(name, state.cause))
46
+ : Effect.void,
47
+ ),
48
+ Effect.andThen(Effect.never),
49
+ )
50
+ const activate = Effect.fnUntraced(function* (
51
+ entry: Effect.Success<ReturnType<typeof Configuration.discover>>[number],
52
+ registry: Effect.Success<ReturnType<typeof Registry.make>>,
53
+ ) {
54
+ const { ConfigDirectory, Entry, ModuleResolver } = yield* Effect.promise(
55
+ () => import("@graftland/loader"),
56
+ ).pipe(Effect.uninterruptible)
57
+ const path = yield* Path.Path
58
+ const directory = path.dirname(entry.file)
59
+ const importer = yield* Importer.make(directory)
60
+ const resolver = yield* ModuleResolver.make({ base: directory, importer })
61
+ const plugin = yield* resolver.resolve(entry.name)
62
+ const input = yield* Entry.resolveEnv(entry.config)
63
+ const owner = Plugin.make({
64
+ name: `command:${entry.name}`,
65
+ apply: Effect.fnUntraced(function* () {
66
+ yield* Graft.provide(Commands, registry.service(entry.name))
67
+ const child = yield* Graft.plugin(plugin, input)
68
+ const state = yield* child.state.pipe(
69
+ Stream.filter(
70
+ (state) => state._tag === "Active" || state._tag === "Failed",
71
+ ),
72
+ Stream.runHead,
73
+ )
74
+ if (Option.isSome(state) && state.value._tag === "Failed")
75
+ return yield* failure(entry.name, state.value.cause)
76
+ yield* Plugin.fork(observe(child, entry.name))
77
+ }),
78
+ })
79
+ const fork = yield* Graft.plugin(owner, undefined, {
80
+ isolate: [Commands],
81
+ }).pipe(Effect.provideService(ConfigDirectory, Option.some(directory)))
82
+ yield* Effect.addFinalizer(() => fork.dispose.pipe(Effect.orDie))
83
+ yield* fork.await.pipe(
84
+ Effect.timeout("5 seconds"),
85
+ Effect.catchCause((cause) => Effect.fail(failure(entry.name, cause))),
86
+ )
87
+ if (!(yield* registry.has(entry.name)))
88
+ return yield* CommandPluginError.make({
89
+ plugin: entry.name,
90
+ detail: "The plugin did not register a command",
91
+ })
92
+ return fork
93
+ })
94
+
95
+ // Registration checks the error/environment bounds before this heterogeneous tree erases its input types.
96
+ const execute = <Name extends string, Input, ContextInput>(
97
+ command: Command.Command<
98
+ Name,
99
+ Input,
100
+ ContextInput,
101
+ CliError.CliError,
102
+ Environment
103
+ >,
104
+ version: string,
105
+ args: ReadonlyArray<string>,
106
+ ): Effect.Effect<void, CliError.CliError, Environment> =>
107
+ Command.runWith(command, { version })(args)
108
+ export const run = Effect.fnUntraced(function* (
109
+ builtins: ReadonlyArray<Command.Command.Any>,
110
+ root: Command.Command<
111
+ "graft",
112
+ { readonly stdio: boolean; readonly noPlugins: boolean },
113
+ { readonly stdio: boolean; readonly noPlugins: boolean },
114
+ never,
115
+ never
116
+ >,
117
+ version: string,
118
+ args: ReadonlyArray<string>,
119
+ ) {
120
+ const registry = yield* Registry.make(builtins)
121
+ const flags = Array.takeWhile(args, (arg) => arg !== "--")
122
+ const entries =
123
+ flags.includes("--no-plugins") ||
124
+ (args.length === 1 && args[0] === "--version")
125
+ ? []
126
+ : yield* Configuration.discover(
127
+ Option.contains(
128
+ Array.findFirst(flags, (arg) => !String.startsWith("-")(arg)),
129
+ "start",
130
+ ) &&
131
+ (flags.includes("--watch") || flags.includes("-w")),
132
+ )
133
+ const forks = yield* Effect.forEach(
134
+ entries,
135
+ (entry) =>
136
+ activate(entry, registry).pipe(
137
+ Effect.mapError((error) => failure(entry.name, Cause.fail(error))),
138
+ ),
139
+ { concurrency: "unbounded" },
140
+ )
141
+ const commands = yield* registry.freeze
142
+ return yield* execute(
143
+ root.pipe(
144
+ Command.withSubcommands([
145
+ ...Array.map(builtins, Registry.erase),
146
+ ...commands,
147
+ ]),
148
+ ),
149
+ version,
150
+ args,
151
+ ).pipe(
152
+ Effect.raceFirst(
153
+ Effect.forEach(
154
+ forks,
155
+ (fork, index) => observe(fork, entries[index]?.name ?? "command"),
156
+ { concurrency: "unbounded" },
157
+ ).pipe(Effect.andThen(Effect.never)),
158
+ ),
159
+ )
160
+ })
161
+
162
+ const usage = (message: string) =>
163
+ Console.error(message).pipe(
164
+ Effect.andThen(
165
+ CliError.UserError.make({
166
+ cause: "Invalid process flags",
167
+ userMessage: message,
168
+ }),
169
+ ),
170
+ )
171
+
172
+ /** Select output policy before discovery or CLI action flags can produce text. */
173
+ export const output = <A, E, R>(
174
+ effect: Effect.Effect<A, E, R>,
175
+ args: ReadonlyArray<string>,
176
+ ) =>
177
+ Effect.gen(function* () {
178
+ const tokens = Array.takeWhile(args, (arg) => arg !== "--")
179
+ const stdio = tokens.includes("--stdio")
180
+ const program = Effect.gen(function* () {
181
+ if (
182
+ stdio &&
183
+ Array.some(
184
+ tokens,
185
+ (arg) =>
186
+ arg === "--wizard" ||
187
+ String.startsWith("--wizard=")(arg) ||
188
+ arg === "--completions" ||
189
+ String.startsWith("--completions=")(arg),
190
+ )
191
+ )
192
+ return yield* usage(
193
+ "--stdio cannot be combined with wizard or completions",
194
+ )
195
+ if (
196
+ Array.some(
197
+ tokens,
198
+ (arg) =>
199
+ String.startsWith("--stdio=")(arg) ||
200
+ arg === "--no-stdio" ||
201
+ String.startsWith("--no-plugins=")(arg),
202
+ )
203
+ )
204
+ return yield* usage("Use --stdio and --no-plugins as standalone flags")
205
+ return yield* effect
206
+ }).pipe(Effect.provideService(ProtocolOutput, stdio))
207
+ return yield* stdio
208
+ ? program.pipe(
209
+ reserve,
210
+ Effect.provideService(
211
+ CliOutput.Formatter,
212
+ CliOutput.defaultFormatter({ colors: false }),
213
+ ),
214
+ )
215
+ : program
216
+ })
@@ -0,0 +1,100 @@
1
+ import { Array, Effect, HashMap, Option, TxRef } from "effect"
2
+ import { Command, type CliError } from "effect/cli"
3
+
4
+ import {
5
+ CommandRegistrationError,
6
+ Commands,
7
+ type Environment,
8
+ } from "../Commands"
9
+
10
+ export type Registered = Command.Command<
11
+ string,
12
+ never,
13
+ {},
14
+ CliError.CliError,
15
+ Environment
16
+ >
17
+ /** The public register method checks E and R before input/name erasure. */
18
+ export const erase = (command: Command.Command.Any): Registered => command
19
+
20
+ const names = (command: Command.Command.Any) =>
21
+ command.alias === undefined ? [command.name] : [command.name, command.alias]
22
+ const validate = (
23
+ commands: ReadonlyArray<Command.Command.Any>,
24
+ ): Effect.Effect<void, CommandRegistrationError> => {
25
+ const all = Array.flatMap(commands, names)
26
+ const duplicate = Array.findFirst(
27
+ all,
28
+ (name, index) => all.indexOf(name) !== index,
29
+ )
30
+ return Option.isSome(duplicate)
31
+ ? Effect.fail(
32
+ CommandRegistrationError.make({
33
+ command: duplicate.value,
34
+ detail: "Duplicate command name or alias",
35
+ }),
36
+ )
37
+ : Effect.forEach(
38
+ commands,
39
+ (command) =>
40
+ validate(
41
+ Array.flatMap(command.subcommands, (group) => group.commands),
42
+ ),
43
+ { discard: true },
44
+ )
45
+ }
46
+ export const make = Effect.fnUntraced(function* (
47
+ builtins: ReadonlyArray<Command.Command.Any>,
48
+ ) {
49
+ const state = yield* TxRef.make({
50
+ frozen: false,
51
+ entries: HashMap.empty<string, Registered>(),
52
+ })
53
+ const service = (owner: string) =>
54
+ Commands.of({
55
+ register: (command) =>
56
+ Effect.acquireRelease(
57
+ Effect.gen(function* () {
58
+ const current = yield* TxRef.get(state)
59
+ if (current.frozen)
60
+ return yield* CommandRegistrationError.make({
61
+ command: command.name,
62
+ detail: "Registration is closed",
63
+ })
64
+ if (HashMap.has(current.entries, owner))
65
+ return yield* CommandRegistrationError.make({
66
+ command: command.name,
67
+ detail: "A plugin must register exactly one root command",
68
+ })
69
+ yield* validate([
70
+ ...builtins,
71
+ ...HashMap.values(current.entries),
72
+ command,
73
+ ])
74
+ yield* TxRef.set(state, {
75
+ ...current,
76
+ entries: HashMap.set(current.entries, owner, erase(command)),
77
+ })
78
+ }).pipe(Effect.tx),
79
+ () =>
80
+ TxRef.update(state, (current) => ({
81
+ ...current,
82
+ entries: HashMap.remove(current.entries, owner),
83
+ })).pipe(Effect.tx),
84
+ ),
85
+ })
86
+ const freeze = Effect.gen(function* () {
87
+ const current = yield* TxRef.get(state)
88
+ yield* TxRef.set(state, { ...current, frozen: true })
89
+ return Array.fromIterable(HashMap.values(current.entries))
90
+ }).pipe(Effect.tx)
91
+ return {
92
+ service,
93
+ freeze,
94
+ has: (owner: string) =>
95
+ TxRef.get(state).pipe(
96
+ Effect.map((current) => HashMap.has(current.entries, owner)),
97
+ Effect.tx,
98
+ ),
99
+ }
100
+ })
@@ -1,13 +1,9 @@
1
- /**
2
- * The JSON Schema of the config file that graft start reads, derived from the
3
- * loader's entry fields and the intercepts the CLI registers. The build writes
4
- * it to the package's schema.json, so each release ships its own version.
5
- */
6
- import { Entry } from "@graftland/loader"
7
- import { Array, JsonSchema, Record, Schema } from "effect"
8
- import { registrations } from "./intercepts"
1
+ /** The CLI adds its intercepts to the loader's recursive editor schema. */
2
+ import { Array, Record, Schema } from "effect"
3
+
4
+ import { ConfigSchema } from "@graftland/loader"
9
5
 
10
- const fields = Entry.Entry.schema.fields
6
+ import { registrations } from "./intercepts"
11
7
 
12
8
  const Intercept = Schema.Struct(
13
9
  Record.fromEntries(
@@ -22,64 +18,8 @@ const Intercept = Schema.Struct(
22
18
  "Patches for references, applying to this entry and its descendants.",
23
19
  })
24
20
 
25
- const Metadata = [Schema.Record(Schema.String, Schema.Unknown)] as const
26
-
27
- const shared = {
28
- ...fields,
29
- id: Schema.optionalKey(
30
- fields.id.annotate({
31
- description:
32
- "Identifies the entry. Graft adds one, and writes it to the file, if an entry has none.",
33
- }),
34
- ),
35
- intercept: Schema.optionalKey(Intercept),
36
- }
37
-
38
- // Two shapes instead of one, so that only a group's config is checked as a
39
- // list of entries; a plugin's config is checked by the plugin's own Schema.
40
- const FileEntry: Schema.Top = Schema.Union([
41
- Schema.StructWithRest(
42
- Schema.Struct({
43
- ...shared,
44
- group: Schema.Literal(true).annotate({
45
- description: "true makes the entry a group of other entries.",
46
- }),
47
- config: Schema.optionalKey(
48
- Schema.Array(Schema.suspend(() => FileEntry)).annotate({
49
- description: "The entries of this group.",
50
- }),
51
- ),
52
- }),
53
- Metadata,
54
- ),
55
- Schema.StructWithRest(
56
- Schema.Struct({
57
- ...shared,
58
- group: Schema.optionalKey(
59
- Schema.Literal(false).annotate({
60
- description: "true makes the entry a group of other entries.",
61
- }),
62
- ),
63
- }),
64
- Metadata,
65
- ),
66
- ]).annotate({ identifier: "Entry" })
67
-
68
- const File = Schema.NullOr(Schema.Array(FileEntry)).annotate({
69
- title: "Graft config file",
70
- description:
71
- "The entries that graft start loads. Each entry loads one plugin, or groups other entries.",
72
- })
73
-
74
- /** Draft-07, which YAML language servers support most widely. */
75
- export const make = () => {
76
- const document = JsonSchema.toDocumentDraft07(
77
- // Only the intercept struct is closed: entries keep a rest record for metadata.
78
- Schema.toJsonSchemaDocument(File, { onExcessProperty: "error" }),
79
- )
80
- return {
81
- $schema: JsonSchema.META_SCHEMA_URI_DRAFT_07,
82
- ...document.schema,
83
- definitions: document.definitions,
84
- }
85
- }
21
+ export const make = (entries?: ReadonlyArray<ConfigSchema.Registration>) =>
22
+ ConfigSchema.make({
23
+ ...(entries === undefined ? {} : { entries }),
24
+ intercept: Intercept,
25
+ })
@@ -2,8 +2,6 @@
2
2
  * Where `!env` values come from: the process environment, then an optional
3
3
  * `.env` file that watch mode reads again when it changes.
4
4
  */
5
- import { observeShutdown } from "./observeShutdown"
6
- import type { PlatformError } from "effect"
7
5
  import {
8
6
  Cause,
9
7
  ConfigProvider,
@@ -17,8 +15,11 @@ import {
17
15
  Queue,
18
16
  Ref,
19
17
  Stream,
18
+ type PlatformError,
20
19
  } from "effect"
21
20
 
21
+ import { observeShutdown } from "./observeShutdown"
22
+
22
23
  // An empty variable is still set: it overrides the file, and the plugin's
23
24
  // schema decides whether it is valid.
24
25
  const options = { preserveEmptyStrings: true } as const
@@ -0,0 +1,28 @@
1
+ import { registerHooks } from "node:module"
2
+
3
+ import { Effect, Path, String } from "effect"
4
+ import { SecureRandom } from "effect/crypto"
5
+
6
+ /** Bare names resolve from the application; imported dependencies keep their own parent URLs. */
7
+ export const make = Effect.fnUntraced(function* (directory: string) {
8
+ const path = yield* Path.Path
9
+ const base = yield* path.toFileUrl(path.join(directory, "graft.yml"))
10
+ const random = yield* SecureRandom.SecureRandom
11
+ const id = yield* random.uuidV4()
12
+ const prefix = `graft-module-${id}:`
13
+ yield* Effect.acquireRelease(
14
+ Effect.sync(() =>
15
+ registerHooks({
16
+ resolve: (specifier, context, next) =>
17
+ String.startsWith(prefix)(specifier)
18
+ ? next(String.slice(prefix.length)(specifier), {
19
+ ...context,
20
+ parentURL: base.href,
21
+ })
22
+ : next(specifier, context),
23
+ }),
24
+ ),
25
+ (hooks) => Effect.sync(() => hooks.deregister()),
26
+ )
27
+ return (specifier: string): Promise<unknown> => import(prefix + specifier)
28
+ })
@@ -1,6 +1,7 @@
1
- import { Intercept } from "@graftland/loader"
2
1
  import { LogLevel, References, Schema } from "effect"
3
2
 
3
+ import { Intercept } from "@graftland/loader"
4
+
4
5
  /** References that can be configured by stock CLI entries. */
5
6
  export const registrations = [
6
7
  Intercept.make(
@@ -0,0 +1,101 @@
1
+ import process from "node:process"
2
+ // @effect-diagnostics-next-line nodeBuiltinImport:off -- a native Writable retains the protocol writer while Node forwards all diagnostic output to stderr.
3
+ import { finished, Writable } from "node:stream"
4
+
5
+ import { Context, Effect, Logger, Option, PlatformError, Stdio } from "effect"
6
+ import { NodeSink } from "@effect/platform-node"
7
+
8
+ // A peer that stops reading but keeps the pipe open must not hold shutdown forever.
9
+ const flushTimeout = "2 seconds"
10
+ const flushTimedOut = new Error(
11
+ "The protocol peer stopped reading during shutdown",
12
+ )
13
+
14
+ export const ProtocolStdio = Context.Reference(
15
+ "@graftland/cli/internal/ProtocolStdio",
16
+ { defaultValue: (): Option.Option<Stdio.Stdio> => Option.none() },
17
+ )
18
+
19
+ /** Reserve the original stdout writer for Stdio; all process diagnostics use stderr. */
20
+ export const reserve = <A, E, R>(program: Effect.Effect<A, E, R>) =>
21
+ Effect.scoped(
22
+ Effect.gen(function* () {
23
+ const stdio = yield* Stdio.Stdio
24
+ const protocol = yield* Effect.acquireRelease(
25
+ Effect.sync(() => {
26
+ const original = process.stdout.write
27
+ const write = original.bind(process.stdout)
28
+ const stream = new Writable({
29
+ write: (chunk, encoding, done) => {
30
+ write(chunk, encoding, done)
31
+ },
32
+ })
33
+ // Default worker forwarding waits for stdout's drain after write returns false.
34
+ const drain = () => {
35
+ process.stdout.emit("drain")
36
+ }
37
+ const error = (cause: Error) => {
38
+ stream.destroy(cause)
39
+ }
40
+ process.stdout.on("error", error)
41
+ process.stderr.on("drain", drain)
42
+ // Node's console and default worker forwarding use this same writer.
43
+ process.stdout.write = process.stderr.write.bind(process.stderr)
44
+ return { original, stream, drain, error }
45
+ }),
46
+ ({ original, stream, drain, error }) =>
47
+ Effect.raceFirst(
48
+ Effect.callback<void>((resume) => {
49
+ const cleanup = finished(stream, (cause) => {
50
+ cleanup()
51
+ const timedOut = cause === flushTimedOut
52
+ const restore = () => {
53
+ // After a timeout, part of a frame may still be queued on stdout, so later
54
+ // output keeps going to stderr instead of corrupting the abandoned stream.
55
+ if (!timedOut) {
56
+ process.stdout.off("error", error)
57
+ process.stderr.off("drain", drain)
58
+ process.stdout.write = original
59
+ }
60
+ resume(
61
+ cause == null || timedOut ? Effect.void : Effect.die(cause),
62
+ )
63
+ }
64
+ // Keep the native error listener until every accepted write and delayed error has settled.
65
+ if (process.stdout.destroyed && !process.stdout.closed)
66
+ process.stdout.once("close", restore)
67
+ else restore()
68
+ })
69
+ stream.end()
70
+ }),
71
+ // Unflushed bytes for a peer that stopped reading are dropped, not a cleanup failure.
72
+ // Node cannot close stdio, so a clean exit still waits for the stalled peer; signals and failures exit.
73
+ Effect.sleep(flushTimeout).pipe(
74
+ Effect.andThen(Effect.sync(() => stream.destroy(flushTimedOut))),
75
+ Effect.andThen(Effect.never),
76
+ Effect.interruptible,
77
+ ),
78
+ ),
79
+ )
80
+ const reserved = {
81
+ ...stdio,
82
+ stdout: (options?: { readonly endOnDone?: boolean }) =>
83
+ NodeSink.fromWritable({
84
+ evaluate: () => protocol.stream,
85
+ onError: (cause) =>
86
+ PlatformError.systemError({
87
+ module: "Stdio",
88
+ method: "stdout",
89
+ _tag: "Unknown",
90
+ cause,
91
+ }),
92
+ endOnDone: options?.endOnDone ?? false,
93
+ }),
94
+ }
95
+ return yield* program.pipe(
96
+ Effect.provideService(Logger.LogToStderr, true),
97
+ Effect.provideService(Stdio.Stdio, reserved),
98
+ Effect.provideService(ProtocolStdio, Option.some(reserved)),
99
+ )
100
+ }),
101
+ )
@@ -1,7 +1,9 @@
1
- import { observeShutdown } from "./observeShutdown"
2
- import type * as Hmr from "@graftland/hmr"
3
1
  import { Array, Cause, Effect, Exit, Stream, String } from "effect"
4
2
 
3
+ import type * as Hmr from "@graftland/hmr"
4
+
5
+ import { observeShutdown } from "./observeShutdown"
6
+
5
7
  const moduleExtensions = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"]
6
8
 
7
9
  /** Whether a change to this file should reload plugin modules. */