@graftland/cli 0.4.1 → 0.4.2

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.
package/dist/Start.d.ts CHANGED
@@ -15,6 +15,12 @@ export interface Options {
15
15
  * a variable. With `watch`, changes to it are read again.
16
16
  */
17
17
  readonly envFile?: string | undefined;
18
+ /**
19
+ * A directory of secret files that `!env` reads when neither the process
20
+ * environment nor `envFile` sets a variable. Each regular file is one
21
+ * variable, named after the file. With `watch`, changes are read again.
22
+ */
23
+ readonly envDir?: string | undefined;
18
24
  }
19
25
  /**
20
26
  * Loads the entries of the config file and runs until interrupted. Plugin
@@ -1 +1 @@
1
- {"version":3,"file":"Start.d.ts","names":[],"sources":["../src/Start.ts"],"mappings":";;;;;;;iBAoCiB;;WAEP;;WAEA;;;;;WAKA;;;;;;;qBAQG,MAAG,SACN,YACP,OAAO,aAEP,mBACA,MAAM,kBACN,cAAc,cACd,cAAc,gBACd,OAAO,cACP,OAAO,aACP,KAAK"}
1
+ {"version":3,"file":"Start.d.ts","names":[],"sources":["../src/Start.ts"],"mappings":";;;;;;;iBA0CiB;;WAEP;;WAEA;;;;;WAKA;;;;;;WAMA;;;;;;;qBAQG,MAAG,SACN,YACP,OAAO,aAEP,mBACA,MAAM,kBACN,cAAc,cACd,cAAc,gBACd,OAAO,cACP,OAAO,aACP,KAAK"}
package/dist/Start.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { Application, ApplicationError, platform } from "./Application.js";
3
- import { EnvFile, layer, watchEnvFile } from "./internal/env.js";
3
+ import { EnvDir, EnvFile, layer, watchEnvDir, watchEnvFile } from "./internal/env.js";
4
4
  import { layer as layer$1 } from "./internal/intercepts.js";
5
5
  import { watchModules } from "./internal/watch.js";
6
6
  import { Array, Cause, Deferred, Effect, Exit, FileSystem, Layer, Logger, Option, Path, Scope, Semaphore } from "effect";
@@ -58,7 +58,8 @@ const run = (options) => Effect.gen(function* () {
58
58
  }).pipe(Effect.provideService(ModuleResolver.Watch, options.watch ? Option.some(observe) : Option.none()))).await;
59
59
  if (options.watch) {
60
60
  const { reload } = yield* EnvFile;
61
- const envFile = options.envFile;
61
+ const { reload: reloadDir } = yield* EnvDir;
62
+ const { envFile, envDir } = options;
62
63
  const reloader = Plugin.make({
63
64
  name: "reloader",
64
65
  inject: [
@@ -70,13 +71,17 @@ const run = (options) => Effect.gen(function* () {
70
71
  const loader = yield* Loader.Loader;
71
72
  yield* watchModules(hmr, directory, loader.retry).pipe(Effect.forkScoped);
72
73
  if (envFile !== void 0) yield* watchEnvFile(envFile, reload, loader.retry).pipe(Effect.forkScoped);
74
+ if (envDir !== void 0) yield* watchEnvDir(envDir, reloadDir, loader.retry).pipe(Effect.forkScoped);
73
75
  })
74
76
  });
75
77
  yield* Graft.plugin(reloader);
76
78
  }
77
79
  yield* Effect.logInfo(`Running ${path.relative(process.cwd(), file) || file}`);
78
80
  return yield* Deferred.await(done);
79
- }).pipe(Effect.scoped, Effect.provideService(Entry.Select, (entry) => entry["cli"] !== true), Effect.provide(Layer.provideMerge(Graft.layer, Layer.mergeAll(platform, layer$1, layer(options.envFile).pipe(Layer.provide(NodeServices.layer)), Logger.layer([Logger.consolePretty()])))));
81
+ }).pipe(Effect.scoped, Effect.provideService(Entry.Select, (entry) => entry["cli"] !== true), Effect.provide(Layer.provideMerge(Graft.layer, Layer.mergeAll(platform, layer$1, layer({
82
+ file: options.envFile,
83
+ dir: options.envDir
84
+ }).pipe(Layer.provide(NodeServices.layer)), Logger.layer([Logger.consolePretty()])))));
80
85
  export { Start_exports, run };
81
86
 
82
87
  //# sourceMappingURL=Start.js.map
package/dist/Start.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"Start.js","names":["intercepts","env"],"sources":["../src/Start.ts"],"sourcesContent":["/**\n * Runs a Graft application from its config file.\n */\nimport {\n\tArray,\n\tCause,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tLogger,\n\tOption,\n\tPath,\n\tScope,\n\tSemaphore,\n\ttype PlatformError,\n\ttype Schema,\n} from \"effect\"\nimport { NodeServices } from \"@effect/platform-node\"\n\nimport { Graft, Plugin } from \"@graftland/graft\"\nimport * as Hmr from \"@graftland/hmr\"\nimport {\n\tEntry,\n\tInclude,\n\tLoader,\n\tModuleResolver,\n\ttype Tree,\n} from \"@graftland/loader\"\n\nimport { Application, ApplicationError, platform } from \"./Application\"\nimport { layer as env, EnvFile, watchEnvFile } from \"./internal/env\"\nimport { layer as intercepts } from \"./internal/intercepts\"\nimport { watchModules } from \"./internal/watch\"\n\nexport interface Options {\n\t/** Path of the config file. */\n\treadonly config: string\n\t/** Reload plugin modules and the config file when they change. */\n\treadonly watch: boolean\n\t/**\n\t * A `.env` file that `!env` reads when the process environment does not set\n\t * a variable. With `watch`, changes to it are read again.\n\t */\n\treadonly envFile?: string | undefined\n}\n\n/**\n * Loads the entries of the config file and runs until interrupted. Plugin\n * modules are imported through the hot reloader, so bare specifiers resolve\n * from the application directory whether or not `watch` is on.\n */\nexport const run = (\n\toptions: Options,\n): Effect.Effect<\n\tvoid,\n\t| ApplicationError\n\t| Graft.ServiceConflict\n\t| PlatformError.BadArgument\n\t| PlatformError.PlatformError\n\t| Schema.SchemaError\n\t| Loader.StoreError\n\t| Tree.InvalidOperation\n> =>\n\tEffect.gen(function* () {\n\t\tconst scope = yield* Effect.scope\n\t\tconst done = yield* Deferred.make<void, ApplicationError>()\n\t\tconst protocol = yield* Semaphore.make(1)\n\t\tyield* Graft.provide(Application, {\n\t\t\tclaimProtocol: Effect.acquireRelease(\n\t\t\t\tprotocol.takeIfAvailable(1).pipe(\n\t\t\t\t\tEffect.flatMap((acquired) =>\n\t\t\t\t\t\tacquired\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.fail(\n\t\t\t\t\t\t\t\t\tApplicationError.make({\n\t\t\t\t\t\t\t\t\t\tcause: Cause.fail(\n\t\t\t\t\t\t\t\t\t\t\t\"Only one runner may own the process stdio channel\",\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t() => protocol.release(1),\n\t\t\t),\n\t\t\tfinish: (exit) =>\n\t\t\t\tDeferred.done(\n\t\t\t\t\tdone,\n\t\t\t\t\tExit.isFailure(exit)\n\t\t\t\t\t\t? Exit.fail(ApplicationError.make({ cause: exit.cause }))\n\t\t\t\t\t\t: Exit.void,\n\t\t\t\t).pipe(Effect.asVoid),\n\t\t\tonExit: (release) => Scope.addFinalizer(scope, release),\n\t\t})\n\t\tconst path = yield* Path.Path\n\t\tconst file = path.resolve(options.config)\n\t\tconst directory = path.dirname(file)\n\t\tconst base = yield* path.toFileUrl(file)\n\t\tconst hmr = yield* Hmr.make({ base: base.href })\n\t\tconst resolver = yield* ModuleResolver.make({\n\t\t\tbase: directory,\n\t\t\timporter: hmr.importerScoped,\n\t\t})\n\t\tconst modules = yield* ModuleResolver.make({\n\t\t\tbase: directory,\n\t\t\timporter: options.watch ? hmr.importModuleScoped : hmr.importModule,\n\t\t})\n\t\tyield* Graft.provide(ModuleResolver.ModuleResolver, {\n\t\t\tresolve: resolver.resolve,\n\t\t\timportModule: modules.importModule,\n\t\t})\n\t\tconst fs = yield* FileSystem.FileSystem\n\t\tconst observe = hmr.observation.pipe(\n\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\tEffect.map((source) => ({\n\t\t\t\t...source,\n\t\t\t\twatch: (files: ReadonlyArray<string>, activation: Scope.Scope) =>\n\t\t\t\t\tsource.watch(files, activation).pipe(\n\t\t\t\t\t\tEffect.mapError((cause) =>\n\t\t\t\t\t\t\tModuleResolver.ModuleInvalid.make({\n\t\t\t\t\t\t\t\tspecifier: Array.join(files, \", \"),\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t})),\n\t\t)\n\t\tconst included = yield* Graft.plugin(Include.Include, {\n\t\t\tpath: file,\n\t\t\twatch: options.watch,\n\t\t}).pipe(\n\t\t\tEffect.provideService(\n\t\t\t\tModuleResolver.Watch,\n\t\t\t\toptions.watch ? Option.some(observe) : Option.none(),\n\t\t\t),\n\t\t)\n\t\tyield* included.await\n\t\tif (options.watch) {\n\t\t\tconst { reload } = yield* EnvFile\n\t\t\tconst envFile = options.envFile\n\t\t\tconst reloader = Plugin.make({\n\t\t\t\tname: \"reloader\",\n\t\t\t\tinject: [Loader.Loader, FileSystem.FileSystem, Path.Path],\n\t\t\t\tapply: () =>\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tconst loader = yield* Loader.Loader\n\t\t\t\t\t\tyield* watchModules(hmr, directory, loader.retry).pipe(\n\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif (envFile !== undefined)\n\t\t\t\t\t\t\tyield* watchEnvFile(envFile, reload, loader.retry).pipe(\n\t\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\tyield* Graft.plugin(reloader)\n\t\t}\n\n\t\tyield* Effect.logInfo(\n\t\t\t`Running ${path.relative(process.cwd(), file) || file}`,\n\t\t)\n\t\treturn yield* Deferred.await(done)\n\t}).pipe(\n\t\tEffect.scoped,\n\t\tEffect.provideService(Entry.Select, (entry) => entry[\"cli\"] !== true),\n\t\t// @effect-diagnostics-next-line strictEffectProvide:off -- this runs the whole application, so it is an entry point\n\t\tEffect.provide(\n\t\t\tLayer.provideMerge(\n\t\t\t\tGraft.layer,\n\t\t\t\tLayer.mergeAll(\n\t\t\t\t\tplatform,\n\t\t\t\t\tintercepts,\n\t\t\t\t\tenv(options.envFile).pipe(Layer.provide(NodeServices.layer)),\n\t\t\t\t\tLogger.layer([Logger.consolePretty()]),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqDA,MAAa,OACZ,YAWA,OAAO,IAAI,aAAa;CACvB,MAAM,QAAQ,OAAO,OAAO;CAC5B,MAAM,OAAO,OAAO,SAAS,KAA6B;CAC1D,MAAM,WAAW,OAAO,UAAU,KAAK,CAAC;CACxC,OAAO,MAAM,QAAQ,aAAa;EACjC,eAAe,OAAO,eACrB,SAAS,gBAAgB,CAAC,CAAC,CAAC,KAC3B,OAAO,SAAS,aACf,WACG,OAAO,OACP,OAAO,KACP,iBAAiB,KAAK,EACrB,OAAO,MAAM,KACZ,mDACD,EACD,CAAC,CACF,CACH,CACD,SACM,SAAS,QAAQ,CAAC,CACzB;EACA,SAAS,SACR,SAAS,KACR,MACA,KAAK,UAAU,IAAI,IAChB,KAAK,KAAK,iBAAiB,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,CAAC,IACtD,KAAK,IACT,CAAC,CAAC,KAAK,OAAO,MAAM;EACrB,SAAS,YAAY,MAAM,aAAa,OAAO,OAAO;CACvD,CAAC;CACD,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,OAAO,KAAK,QAAQ,QAAQ,MAAM;CACxC,MAAM,YAAY,KAAK,QAAQ,IAAI;CACnC,MAAM,OAAO,OAAO,KAAK,UAAU,IAAI;CACvC,MAAM,MAAM,OAAO,IAAI,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;CAC/C,MAAM,WAAW,OAAO,eAAe,KAAK;EAC3C,MAAM;EACN,UAAU,IAAI;CACf,CAAC;CACD,MAAM,UAAU,OAAO,eAAe,KAAK;EAC1C,MAAM;EACN,UAAU,QAAQ,QAAQ,IAAI,qBAAqB,IAAI;CACxD,CAAC;CACD,OAAO,MAAM,QAAQ,eAAe,gBAAgB;EACnD,SAAS,SAAS;EAClB,cAAc,QAAQ;CACvB,CAAC;CACD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,UAAU,IAAI,YAAY,KAC/B,OAAO,eAAe,WAAW,YAAY,EAAE,GAC/C,OAAO,KAAK,YAAY;EACvB,GAAG;EACH,QAAQ,OAA8B,eACrC,OAAO,MAAM,OAAO,UAAU,CAAC,CAAC,KAC/B,OAAO,UAAU,UAChB,eAAe,cAAc,KAAK;GACjC,WAAW,MAAM,KAAK,OAAO,IAAI;GACjC;EACD,CAAC,CACF,CACD;CACF,EAAE,CACH;CAUA,QAAO,OATiB,MAAM,OAAO,QAAQ,SAAS;EACrD,MAAM;EACN,OAAO,QAAQ;CAChB,CAAC,CAAC,CAAC,KACF,OAAO,eACN,eAAe,OACf,QAAQ,QAAQ,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,CACpD,CACD,EAAA,CACgB;CAChB,IAAI,QAAQ,OAAO;EAClB,MAAM,EAAE,WAAW,OAAO;EAC1B,MAAM,UAAU,QAAQ;EACxB,MAAM,WAAW,OAAO,KAAK;GAC5B,MAAM;GACN,QAAQ;IAAC,OAAO;IAAQ,WAAW;IAAY,KAAK;GAAI;GACxD,aACC,OAAO,IAAI,aAAa;IACvB,MAAM,SAAS,OAAO,OAAO;IAC7B,OAAO,aAAa,KAAK,WAAW,OAAO,KAAK,CAAC,CAAC,KACjD,OAAO,UACR;IACA,IAAI,YAAY,KAAA,GACf,OAAO,aAAa,SAAS,QAAQ,OAAO,KAAK,CAAC,CAAC,KAClD,OAAO,UACR;GACF,CAAC;EACH,CAAC;EACD,OAAO,MAAM,OAAO,QAAQ;CAC7B;CAEA,OAAO,OAAO,QACb,WAAW,KAAK,SAAS,QAAQ,IAAI,GAAG,IAAI,KAAK,MAClD;CACA,OAAO,OAAO,SAAS,MAAM,IAAI;AAClC,CAAC,CAAC,CAAC,KACF,OAAO,QACP,OAAO,eAAe,MAAM,SAAS,UAAU,MAAM,WAAW,IAAI,GAEpE,OAAO,QACN,MAAM,aACL,MAAM,OACN,MAAM,SACL,UACAA,SACAC,MAAI,QAAQ,OAAO,CAAC,CAAC,KAAK,MAAM,QAAQ,aAAa,KAAK,CAAC,GAC3D,OAAO,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC,CACtC,CACD,CACD,CACD"}
1
+ {"version":3,"file":"Start.js","names":["intercepts","env"],"sources":["../src/Start.ts"],"sourcesContent":["/**\n * Runs a Graft application from its config file.\n */\nimport {\n\tArray,\n\tCause,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tLogger,\n\tOption,\n\tPath,\n\tScope,\n\tSemaphore,\n\ttype PlatformError,\n\ttype Schema,\n} from \"effect\"\nimport { NodeServices } from \"@effect/platform-node\"\n\nimport { Graft, Plugin } from \"@graftland/graft\"\nimport * as Hmr from \"@graftland/hmr\"\nimport {\n\tEntry,\n\tInclude,\n\tLoader,\n\tModuleResolver,\n\ttype Tree,\n} from \"@graftland/loader\"\n\nimport { Application, ApplicationError, platform } from \"./Application\"\nimport {\n\tlayer as env,\n\tEnvDir,\n\tEnvFile,\n\twatchEnvDir,\n\twatchEnvFile,\n} from \"./internal/env\"\nimport { layer as intercepts } from \"./internal/intercepts\"\nimport { watchModules } from \"./internal/watch\"\n\nexport interface Options {\n\t/** Path of the config file. */\n\treadonly config: string\n\t/** Reload plugin modules and the config file when they change. */\n\treadonly watch: boolean\n\t/**\n\t * A `.env` file that `!env` reads when the process environment does not set\n\t * a variable. With `watch`, changes to it are read again.\n\t */\n\treadonly envFile?: string | undefined\n\t/**\n\t * A directory of secret files that `!env` reads when neither the process\n\t * environment nor `envFile` sets a variable. Each regular file is one\n\t * variable, named after the file. With `watch`, changes are read again.\n\t */\n\treadonly envDir?: string | undefined\n}\n\n/**\n * Loads the entries of the config file and runs until interrupted. Plugin\n * modules are imported through the hot reloader, so bare specifiers resolve\n * from the application directory whether or not `watch` is on.\n */\nexport const run = (\n\toptions: Options,\n): Effect.Effect<\n\tvoid,\n\t| ApplicationError\n\t| Graft.ServiceConflict\n\t| PlatformError.BadArgument\n\t| PlatformError.PlatformError\n\t| Schema.SchemaError\n\t| Loader.StoreError\n\t| Tree.InvalidOperation\n> =>\n\tEffect.gen(function* () {\n\t\tconst scope = yield* Effect.scope\n\t\tconst done = yield* Deferred.make<void, ApplicationError>()\n\t\tconst protocol = yield* Semaphore.make(1)\n\t\tyield* Graft.provide(Application, {\n\t\t\tclaimProtocol: Effect.acquireRelease(\n\t\t\t\tprotocol.takeIfAvailable(1).pipe(\n\t\t\t\t\tEffect.flatMap((acquired) =>\n\t\t\t\t\t\tacquired\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.fail(\n\t\t\t\t\t\t\t\t\tApplicationError.make({\n\t\t\t\t\t\t\t\t\t\tcause: Cause.fail(\n\t\t\t\t\t\t\t\t\t\t\t\"Only one runner may own the process stdio channel\",\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t\t() => protocol.release(1),\n\t\t\t),\n\t\t\tfinish: (exit) =>\n\t\t\t\tDeferred.done(\n\t\t\t\t\tdone,\n\t\t\t\t\tExit.isFailure(exit)\n\t\t\t\t\t\t? Exit.fail(ApplicationError.make({ cause: exit.cause }))\n\t\t\t\t\t\t: Exit.void,\n\t\t\t\t).pipe(Effect.asVoid),\n\t\t\tonExit: (release) => Scope.addFinalizer(scope, release),\n\t\t})\n\t\tconst path = yield* Path.Path\n\t\tconst file = path.resolve(options.config)\n\t\tconst directory = path.dirname(file)\n\t\tconst base = yield* path.toFileUrl(file)\n\t\tconst hmr = yield* Hmr.make({ base: base.href })\n\t\tconst resolver = yield* ModuleResolver.make({\n\t\t\tbase: directory,\n\t\t\timporter: hmr.importerScoped,\n\t\t})\n\t\tconst modules = yield* ModuleResolver.make({\n\t\t\tbase: directory,\n\t\t\timporter: options.watch ? hmr.importModuleScoped : hmr.importModule,\n\t\t})\n\t\tyield* Graft.provide(ModuleResolver.ModuleResolver, {\n\t\t\tresolve: resolver.resolve,\n\t\t\timportModule: modules.importModule,\n\t\t})\n\t\tconst fs = yield* FileSystem.FileSystem\n\t\tconst observe = hmr.observation.pipe(\n\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\tEffect.map((source) => ({\n\t\t\t\t...source,\n\t\t\t\twatch: (files: ReadonlyArray<string>, activation: Scope.Scope) =>\n\t\t\t\t\tsource.watch(files, activation).pipe(\n\t\t\t\t\t\tEffect.mapError((cause) =>\n\t\t\t\t\t\t\tModuleResolver.ModuleInvalid.make({\n\t\t\t\t\t\t\t\tspecifier: Array.join(files, \", \"),\n\t\t\t\t\t\t\t\tcause,\n\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t})),\n\t\t)\n\t\tconst included = yield* Graft.plugin(Include.Include, {\n\t\t\tpath: file,\n\t\t\twatch: options.watch,\n\t\t}).pipe(\n\t\t\tEffect.provideService(\n\t\t\t\tModuleResolver.Watch,\n\t\t\t\toptions.watch ? Option.some(observe) : Option.none(),\n\t\t\t),\n\t\t)\n\t\tyield* included.await\n\t\tif (options.watch) {\n\t\t\tconst { reload } = yield* EnvFile\n\t\t\tconst { reload: reloadDir } = yield* EnvDir\n\t\t\tconst { envFile, envDir } = options\n\t\t\tconst reloader = Plugin.make({\n\t\t\t\tname: \"reloader\",\n\t\t\t\tinject: [Loader.Loader, FileSystem.FileSystem, Path.Path],\n\t\t\t\tapply: () =>\n\t\t\t\t\tEffect.gen(function* () {\n\t\t\t\t\t\tconst loader = yield* Loader.Loader\n\t\t\t\t\t\tyield* watchModules(hmr, directory, loader.retry).pipe(\n\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif (envFile !== undefined)\n\t\t\t\t\t\t\tyield* watchEnvFile(envFile, reload, loader.retry).pipe(\n\t\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\tif (envDir !== undefined)\n\t\t\t\t\t\t\tyield* watchEnvDir(envDir, reloadDir, loader.retry).pipe(\n\t\t\t\t\t\t\t\tEffect.forkScoped,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t}),\n\t\t\t})\n\t\t\tyield* Graft.plugin(reloader)\n\t\t}\n\n\t\tyield* Effect.logInfo(\n\t\t\t`Running ${path.relative(process.cwd(), file) || file}`,\n\t\t)\n\t\treturn yield* Deferred.await(done)\n\t}).pipe(\n\t\tEffect.scoped,\n\t\tEffect.provideService(Entry.Select, (entry) => entry[\"cli\"] !== true),\n\t\t// @effect-diagnostics-next-line strictEffectProvide:off -- this runs the whole application, so it is an entry point\n\t\tEffect.provide(\n\t\t\tLayer.provideMerge(\n\t\t\t\tGraft.layer,\n\t\t\t\tLayer.mergeAll(\n\t\t\t\t\tplatform,\n\t\t\t\t\tintercepts,\n\t\t\t\t\tenv({ file: options.envFile, dir: options.envDir }).pipe(\n\t\t\t\t\t\tLayer.provide(NodeServices.layer),\n\t\t\t\t\t),\n\t\t\t\t\tLogger.layer([Logger.consolePretty()]),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n"],"mappings":"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"}
package/dist/bin.js CHANGED
@@ -14,10 +14,12 @@ import { command } from "@graftland/create/Command";
14
14
  const start = Command.make("start", {
15
15
  config: Argument.String("config").pipe(Argument.withDescription("Path of the config file"), Argument.withDefault("graft.yml")),
16
16
  watch: Flag.Boolean("watch").pipe(Flag.withAlias("w"), Flag.withDescription("Reload plugins and config when files change"), Flag.withDefault(false)),
17
- envFile: Flag.String("env-file").pipe(Flag.withDescription("Read variables for !env from a .env file; the process environment takes precedence"), Flag.optional)
17
+ envFile: Flag.String("env-file").pipe(Flag.withDescription("Read variables for !env from a .env file; the process environment takes precedence"), Flag.optional),
18
+ envDir: Flag.String("env-dir").pipe(Flag.withDescription("Read variables for !env from a directory with one file per variable, such as /run/secrets"), Flag.optional)
18
19
  }, (options) => Effect.promise(() => import("./Start.js")).pipe(Effect.uninterruptible, Effect.flatMap((Start) => Start.run({
19
20
  ...options,
20
- envFile: Option.getOrUndefined(options.envFile)
21
+ envFile: Option.getOrUndefined(options.envFile),
22
+ envDir: Option.getOrUndefined(options.envDir)
21
23
  })))).pipe(Command.withDescription("Run the application described by a config file"));
22
24
  const add = Command.make("add", {
23
25
  packages: Argument.variadic(Argument.String("package")),
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","names":["Host.output","Host.run","create"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `graft` executable. Running this module starts the program.\n */\nimport { Array, Effect, Layer, Logger, Option, Stdio } from \"effect\"\nimport { Argument, CliError, Command, Flag } from \"effect/cli\"\nimport { NodeRuntime } from \"@effect/platform-node\"\n\nimport { command as create } from \"@graftland/create/Command\"\nimport { Graft } from \"@graftland/graft\"\n\nimport { version } from \"../package.json\" with { type: \"json\" }\nimport { platform } from \"./Application\"\nimport * as Host from \"./internal/commandHost\"\n\nconst start = Command.make(\n\t\"start\",\n\t{\n\t\tconfig: Argument.String(\"config\").pipe(\n\t\t\tArgument.withDescription(\"Path of the config file\"),\n\t\t\tArgument.withDefault(\"graft.yml\"),\n\t\t),\n\t\twatch: Flag.Boolean(\"watch\").pipe(\n\t\t\tFlag.withAlias(\"w\"),\n\t\t\tFlag.withDescription(\"Reload plugins and config when files change\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t\tenvFile: Flag.String(\"env-file\").pipe(\n\t\t\tFlag.withDescription(\n\t\t\t\t\"Read variables for !env from a .env file; the process environment takes precedence\",\n\t\t\t),\n\t\t\tFlag.optional,\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Start\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Start) =>\n\t\t\t\tStart.run({\n\t\t\t\t\t...options,\n\t\t\t\t\tenvFile: Option.getOrUndefined(options.envFile),\n\t\t\t\t}),\n\t\t\t),\n\t\t),\n).pipe(\n\tCommand.withDescription(\"Run the application described by a config file\"),\n)\n\nconst add = Command.make(\n\t\"add\",\n\t{\n\t\tpackages: Argument.variadic(Argument.String(\"package\")),\n\t\tplugin: Flag.String(\"plugin\").pipe(Flag.optional),\n\t\tglobal: Flag.Boolean(\"global\").pipe(Flag.withDefault(false)),\n\t},\n\tEffect.fnUntraced(function* ({ packages, plugin, global }) {\n\t\tif (\n\t\t\t(global && Option.isNone(plugin)) ||\n\t\t\t(Option.isSome(plugin) && packages.length > 0)\n\t\t)\n\t\t\treturn yield* CliError.UserError.make({\n\t\t\t\tcause: \"Conflicting install options\",\n\t\t\t\tuserMessage:\n\t\t\t\t\t\"Use add --plugin <name> [--global] without package arguments\",\n\t\t\t})\n\t\tconst Manage = yield* Effect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t)\n\t\treturn yield* Option.isSome(plugin)\n\t\t\t? Manage.addPlugin(plugin.value, global)\n\t\t\t: Manage.add(packages)\n\t}),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Add dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst remove = Command.make(\n\t\"remove\",\n\t{ packages: Argument.variadic(Argument.String(\"package\")) },\n\t({ packages }) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Manage) => Manage.remove(packages)),\n\t\t),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Remove dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst graft = Command.make(\"graft\").pipe(\n\tCommand.withDescription(\"Run and manage Graft applications\"),\n\tCommand.withSharedFlags({\n\t\tstdio: Flag.Boolean(\"stdio\").pipe(Flag.withDefault(false)),\n\t\tnoPlugins: Flag.Boolean(\"no-plugins\").pipe(Flag.withDefault(false)),\n\t}),\n)\n\nconst program = Effect.gen(function* () {\n\tconst stdio = yield* Stdio.Stdio\n\tconst args = yield* stdio.args\n\treturn yield* Host.output(\n\t\tEffect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst context = yield* Layer.build(Graft.layer)\n\t\t\t\treturn yield* Host.run(\n\t\t\t\t\t[start, create, add, remove],\n\t\t\t\t\tgraft,\n\t\t\t\t\tversion,\n\t\t\t\t\targs,\n\t\t\t\t).pipe(Effect.provideContext(context))\n\t\t\t}),\n\t\t),\n\t\targs,\n\t)\n})\nNodeRuntime.runMain(\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tconst context = yield* Layer.build(platform)\n\t\t\treturn yield* program.pipe(Effect.provideContext(context))\n\t\t}),\n\t).pipe(\n\t\t(effect) =>\n\t\t\tEffect.withFiber((main) =>\n\t\t\t\teffect.pipe(\n\t\t\t\t\tEffect.tapCause((cause) =>\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Interrupt\" && reason.fiberId === main.id,\n\t\t\t\t\t\t) ||\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Fail\" && CliError.isCliError(reason.error),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.logError(cause),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\tEffect.provideService(Logger.LogToStderr, true),\n\t),\n\t{ disableErrorReporting: true },\n)\n"],"mappings":"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"}
1
+ {"version":3,"file":"bin.js","names":["Host.output","Host.run","create"],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `graft` executable. Running this module starts the program.\n */\nimport { Array, Effect, Layer, Logger, Option, Stdio } from \"effect\"\nimport { Argument, CliError, Command, Flag } from \"effect/cli\"\nimport { NodeRuntime } from \"@effect/platform-node\"\n\nimport { command as create } from \"@graftland/create/Command\"\nimport { Graft } from \"@graftland/graft\"\n\nimport { version } from \"../package.json\" with { type: \"json\" }\nimport { platform } from \"./Application\"\nimport * as Host from \"./internal/commandHost\"\n\nconst start = Command.make(\n\t\"start\",\n\t{\n\t\tconfig: Argument.String(\"config\").pipe(\n\t\t\tArgument.withDescription(\"Path of the config file\"),\n\t\t\tArgument.withDefault(\"graft.yml\"),\n\t\t),\n\t\twatch: Flag.Boolean(\"watch\").pipe(\n\t\t\tFlag.withAlias(\"w\"),\n\t\t\tFlag.withDescription(\"Reload plugins and config when files change\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t\tenvFile: Flag.String(\"env-file\").pipe(\n\t\t\tFlag.withDescription(\n\t\t\t\t\"Read variables for !env from a .env file; the process environment takes precedence\",\n\t\t\t),\n\t\t\tFlag.optional,\n\t\t),\n\t\tenvDir: Flag.String(\"env-dir\").pipe(\n\t\t\tFlag.withDescription(\n\t\t\t\t\"Read variables for !env from a directory with one file per variable, such as /run/secrets\",\n\t\t\t),\n\t\t\tFlag.optional,\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Start\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Start) =>\n\t\t\t\tStart.run({\n\t\t\t\t\t...options,\n\t\t\t\t\tenvFile: Option.getOrUndefined(options.envFile),\n\t\t\t\t\tenvDir: Option.getOrUndefined(options.envDir),\n\t\t\t\t}),\n\t\t\t),\n\t\t),\n).pipe(\n\tCommand.withDescription(\"Run the application described by a config file\"),\n)\n\nconst add = Command.make(\n\t\"add\",\n\t{\n\t\tpackages: Argument.variadic(Argument.String(\"package\")),\n\t\tplugin: Flag.String(\"plugin\").pipe(Flag.optional),\n\t\tglobal: Flag.Boolean(\"global\").pipe(Flag.withDefault(false)),\n\t},\n\tEffect.fnUntraced(function* ({ packages, plugin, global }) {\n\t\tif (\n\t\t\t(global && Option.isNone(plugin)) ||\n\t\t\t(Option.isSome(plugin) && packages.length > 0)\n\t\t)\n\t\t\treturn yield* CliError.UserError.make({\n\t\t\t\tcause: \"Conflicting install options\",\n\t\t\t\tuserMessage:\n\t\t\t\t\t\"Use add --plugin <name> [--global] without package arguments\",\n\t\t\t})\n\t\tconst Manage = yield* Effect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t)\n\t\treturn yield* Option.isSome(plugin)\n\t\t\t? Manage.addPlugin(plugin.value, global)\n\t\t\t: Manage.add(packages)\n\t}),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Add dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst remove = Command.make(\n\t\"remove\",\n\t{ packages: Argument.variadic(Argument.String(\"package\")) },\n\t({ packages }) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Manage) => Manage.remove(packages)),\n\t\t),\n).pipe(\n\tCommand.withDescription(\n\t\t\"Remove dependencies, restoring the project if installing fails\",\n\t),\n)\n\nconst graft = Command.make(\"graft\").pipe(\n\tCommand.withDescription(\"Run and manage Graft applications\"),\n\tCommand.withSharedFlags({\n\t\tstdio: Flag.Boolean(\"stdio\").pipe(Flag.withDefault(false)),\n\t\tnoPlugins: Flag.Boolean(\"no-plugins\").pipe(Flag.withDefault(false)),\n\t}),\n)\n\nconst program = Effect.gen(function* () {\n\tconst stdio = yield* Stdio.Stdio\n\tconst args = yield* stdio.args\n\treturn yield* Host.output(\n\t\tEffect.scoped(\n\t\t\tEffect.gen(function* () {\n\t\t\t\tconst context = yield* Layer.build(Graft.layer)\n\t\t\t\treturn yield* Host.run(\n\t\t\t\t\t[start, create, add, remove],\n\t\t\t\t\tgraft,\n\t\t\t\t\tversion,\n\t\t\t\t\targs,\n\t\t\t\t).pipe(Effect.provideContext(context))\n\t\t\t}),\n\t\t),\n\t\targs,\n\t)\n})\nNodeRuntime.runMain(\n\tEffect.scoped(\n\t\tEffect.gen(function* () {\n\t\t\tconst context = yield* Layer.build(platform)\n\t\t\treturn yield* program.pipe(Effect.provideContext(context))\n\t\t}),\n\t).pipe(\n\t\t(effect) =>\n\t\t\tEffect.withFiber((main) =>\n\t\t\t\teffect.pipe(\n\t\t\t\t\tEffect.tapCause((cause) =>\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Interrupt\" && reason.fiberId === main.id,\n\t\t\t\t\t\t) ||\n\t\t\t\t\t\tArray.every(\n\t\t\t\t\t\t\tcause.reasons,\n\t\t\t\t\t\t\t(reason) =>\n\t\t\t\t\t\t\t\treason._tag === \"Fail\" && CliError.isCliError(reason.error),\n\t\t\t\t\t\t)\n\t\t\t\t\t\t\t? Effect.void\n\t\t\t\t\t\t\t: Effect.logError(cause),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\tEffect.provideService(Logger.LogToStderr, true),\n\t),\n\t{ disableErrorReporting: true },\n)\n"],"mappings":"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"}
@@ -1,34 +1,70 @@
1
1
  import { observeShutdown } from "./observeShutdown.js";
2
- import { Cause, ConfigProvider, Context, Deferred, Effect, Exit, FileSystem, Layer, Path, Queue, Ref, Stream } from "effect";
2
+ import { Array, Cause, ConfigProvider, Context, Deferred, Effect, Exit, FileSystem, Layer, Option, Path, Queue, Record, Ref, Stream, String } from "effect";
3
3
  // src/internal/env.ts
4
4
  /**
5
5
  * Where `!env` values come from: the process environment, then an optional
6
- * `.env` file that watch mode reads again when it changes.
6
+ * `.env` file, then an optional directory of secret files. Watch mode reads
7
+ * the file and the directory again when they change.
7
8
  */
8
9
  const options = { preserveEmptyStrings: true };
9
10
  var EnvFile = class extends Context.Service()("@graftland/cli/internal/env/EnvFile") {};
10
- /** Provides `ConfigProvider` and `EnvFile`; a missing file fails the layer. */
11
- const layer = (file) => Layer.unwrap(Effect.gen(function* () {
12
- const environment = ConfigProvider.fromEnv(options);
13
- if (file === void 0) return Layer.merge(ConfigProvider.layer(environment), Layer.succeed(EnvFile, { reload: Effect.succeed(false) }));
14
- const fs = yield* FileSystem.FileSystem;
15
- const read = Effect.gen(function* () {
16
- const target = yield* fs.realPath(file);
17
- const text = yield* fs.readFileString(target);
18
- return {
19
- target,
20
- text,
21
- provider: ConfigProvider.fromDotEnvContents(text, options)
22
- };
23
- });
11
+ var EnvDir = class extends Context.Service()("@graftland/cli/internal/env/EnvDir") {};
12
+ const unchanged = { reload: Effect.succeed(false) };
13
+ /**
14
+ * A source that keeps the provider read from its latest snapshot. A failed
15
+ * read keeps the previous snapshot.
16
+ */
17
+ const source = Effect.fnUntraced(function* (read, provider, same) {
24
18
  const initial = yield* read;
25
- const loaded = yield* Ref.make(initial);
26
- const fromFile = ConfigProvider.make((path) => Effect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)));
27
- return Layer.merge(ConfigProvider.layer(ConfigProvider.orElse(environment, fromFile)), Layer.succeed(EnvFile, { reload: Effect.gen(function* () {
28
- const next = yield* read;
29
- const previous = yield* Ref.getAndSet(loaded, next);
30
- return next.text !== previous.text;
31
- }) }));
19
+ const loaded = yield* Ref.make({
20
+ snapshot: initial,
21
+ provider: provider(initial)
22
+ });
23
+ return {
24
+ provider: ConfigProvider.make((path) => Effect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path))),
25
+ reload: Effect.gen(function* () {
26
+ const snapshot = yield* read;
27
+ return !same(snapshot, (yield* Ref.getAndSet(loaded, {
28
+ snapshot,
29
+ provider: provider(snapshot)
30
+ })).snapshot);
31
+ })
32
+ };
33
+ });
34
+ const readFile = Effect.fnUntraced(function* (file) {
35
+ const fs = yield* FileSystem.FileSystem;
36
+ const target = yield* fs.realPath(file);
37
+ return yield* fs.readFileString(target);
38
+ });
39
+ const isNotFound = (error) => error.reason._tag === "NotFound";
40
+ /**
41
+ * One variable per regular file, named after the file. Hidden entries are
42
+ * skipped, which also skips the bookkeeping links of Kubernetes volumes.
43
+ * Trailing line breaks are not part of the value.
44
+ */
45
+ const readDir = Effect.fnUntraced(function* (directory) {
46
+ const fs = yield* FileSystem.FileSystem;
47
+ const path = yield* Path.Path;
48
+ const names = yield* fs.readDirectory(directory);
49
+ const values = yield* Effect.forEach(Array.filter(names, (name) => !String.startsWith(".")(name)), (name) => {
50
+ const file = path.join(directory, name);
51
+ return fs.stat(file).pipe(Effect.flatMap((info) => info.type === "File" ? fs.readFileString(file).pipe(Effect.map((text) => Option.some([name, String.replace(/[\r\n]+$/, "")(text)]))) : Effect.succeedNone), Effect.catchIf(isNotFound, () => Effect.succeedNone));
52
+ }, { concurrency: 16 });
53
+ return Record.fromEntries(Array.getSomes(values));
54
+ });
55
+ const sameValues = Record.makeEquivalence(String.Equivalence);
56
+ /**
57
+ * Provides `ConfigProvider`, `EnvFile` and `EnvDir`. A missing file or
58
+ * directory fails the layer.
59
+ */
60
+ const layer = (sources) => Layer.unwrap(Effect.gen(function* () {
61
+ const fs = yield* FileSystem.FileSystem;
62
+ const path = yield* Path.Path;
63
+ const provide = (read) => read.pipe(Effect.provideService(FileSystem.FileSystem, fs), Effect.provideService(Path.Path, path));
64
+ const file = sources.file === void 0 ? void 0 : yield* source(provide(readFile(sources.file)), (text) => ConfigProvider.fromDotEnvContents(text, options), (a, b) => a === b);
65
+ const dir = sources.dir === void 0 ? void 0 : yield* source(provide(readDir(sources.dir)), (values) => ConfigProvider.fromEnvRecord(values, options), sameValues);
66
+ const provider = Array.reduce([file, dir], ConfigProvider.fromEnv(options), (first, next) => next === void 0 ? first : ConfigProvider.orElse(first, next.provider));
67
+ return Layer.mergeAll(ConfigProvider.layer(provider), Layer.succeed(EnvFile, file ?? unchanged), Layer.succeed(EnvDir, dir ?? unchanged));
32
68
  }));
33
69
  /**
34
70
  * The handlers below let interruption through, and with it any failure raised
@@ -40,16 +76,25 @@ const logStopped = (file) => (exit) => Exit.isFailure(exit) && Cause.hasInterrup
40
76
  * reads `!env` again. A file that cannot be read keeps its previous values.
41
77
  */
42
78
  const watchEnvFile = Effect.fnUntraced(function* (file, reload, retry) {
43
- const fs = yield* FileSystem.FileSystem;
44
79
  const path = yield* Path.Path;
80
+ const name = path.basename(file);
81
+ return yield* watchSource(file, path.dirname(file), (changed) => path.basename(changed) === name, reload, retry);
82
+ });
83
+ /**
84
+ * Reloads the directory when anything in it changes and retries the loader's
85
+ * entries. A directory that cannot be read keeps its previous values.
86
+ */
87
+ const watchEnvDir = (directory, reload, retry) => watchSource(directory, directory, () => true, reload, retry);
88
+ const watchSource = Effect.fnUntraced(function* (file, directory, matches, reload, retry) {
89
+ const fs = yield* FileSystem.FileSystem;
45
90
  const ready = yield* Deferred.make();
46
- const events = Stream.callback((queue) => fs.watch(path.dirname(file), { onReady: () => Deferred.doneUnsafe(ready, Effect.void) }).pipe(Stream.filter((event) => path.basename(event.path) === path.basename(file)), Stream.runForEachArray((events) => Queue.offerAll(queue, events)), Effect.scoped, (effect) => observeShutdown(effect, `Watching ${file} stopped with interrupted cleanup`), Effect.onExit(logStopped(file)), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Watching ${file} failed`, cause)), Effect.andThen(Effect.sleep("1 second")), Effect.forever), { bufferSize: 16 });
91
+ const events = Stream.callback((queue) => fs.watch(directory, { onReady: () => Deferred.doneUnsafe(ready, Effect.void) }).pipe(Stream.filter((event) => matches(event.path)), Stream.runForEachArray((events) => Queue.offerAll(queue, events)), Effect.scoped, (effect) => observeShutdown(effect, `Watching ${file} stopped with interrupted cleanup`), Effect.onExit(logStopped(file)), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Watching ${file} failed`, cause)), Effect.andThen(Effect.sleep("1 second")), Effect.forever), { bufferSize: 16 });
47
92
  yield* Stream.mergeAll([
48
93
  Stream.as(events, void 0),
49
94
  Stream.fromEffect(Deferred.await(ready)),
50
95
  Stream.tick("1 second").pipe(Stream.drop(1))
51
96
  ], { concurrency: "unbounded" }).pipe(Stream.runForEach(() => Effect.sleep("100 millis").pipe(Effect.andThen(reload), Effect.flatMap((changed) => changed ? retry : Effect.void), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Reloading ${file} failed`, cause)))), Effect.catchCauseIf((cause) => !Cause.hasInterrupts(cause), (cause) => Effect.logError(`Stopped watching ${file}`, cause)), Effect.onExit(logStopped(file)));
52
97
  });
53
- export { EnvFile, layer, watchEnvFile };
98
+ export { EnvDir, EnvFile, layer, watchEnvDir, watchEnvFile };
54
99
 
55
100
  //# sourceMappingURL=env.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["/**\n * Where `!env` values come from: the process environment, then an optional\n * `.env` file that watch mode reads again when it changes.\n */\nimport {\n\tCause,\n\tConfigProvider,\n\tContext,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tPath,\n\tQueue,\n\tRef,\n\tStream,\n\ttype PlatformError,\n} from \"effect\"\n\nimport { observeShutdown } from \"./observeShutdown\"\n\n// An empty variable is still set: it overrides the file, and the plugin's\n// schema decides whether it is valid.\nconst options = { preserveEmptyStrings: true } as const\n\ninterface Loaded {\n\treadonly target: string\n\treadonly text: string\n\treadonly provider: ConfigProvider.ConfigProvider\n}\n\nexport class EnvFile extends Context.Service<\n\tEnvFile,\n\t{\n\t\t/** Reads the file again; reports whether its contents changed. */\n\t\treadonly reload: Effect.Effect<boolean, PlatformError.PlatformError>\n\t}\n>()(\"@graftland/cli/internal/env/EnvFile\") {}\n\n/** Provides `ConfigProvider` and `EnvFile`; a missing file fails the layer. */\nexport const layer = (file: string | undefined) =>\n\tLayer.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst environment = ConfigProvider.fromEnv(options)\n\t\t\tif (file === undefined)\n\t\t\t\treturn Layer.merge(\n\t\t\t\t\tConfigProvider.layer(environment),\n\t\t\t\t\tLayer.succeed(EnvFile, { reload: Effect.succeed(false) }),\n\t\t\t\t)\n\t\t\tconst fs = yield* FileSystem.FileSystem\n\t\t\t// Resolve the request on every read, including polling. Never pin a\n\t\t\t// symlink to its initial target. The provider and its baseline are\n\t\t\t// committed together from the bytes actually read.\n\t\t\tconst read = Effect.gen(function* () {\n\t\t\t\tconst target = yield* fs.realPath(file)\n\t\t\t\tconst text = yield* fs.readFileString(target)\n\t\t\t\treturn {\n\t\t\t\t\ttarget,\n\t\t\t\t\ttext,\n\t\t\t\t\tprovider: ConfigProvider.fromDotEnvContents(text, options),\n\t\t\t\t}\n\t\t\t})\n\t\t\tconst initial: Loaded = yield* read\n\t\t\tconst loaded = yield* Ref.make(initial)\n\t\t\tconst fromFile = ConfigProvider.make((path) =>\n\t\t\t\tEffect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),\n\t\t\t)\n\t\t\treturn Layer.merge(\n\t\t\t\tConfigProvider.layer(ConfigProvider.orElse(environment, fromFile)),\n\t\t\t\tLayer.succeed(EnvFile, {\n\t\t\t\t\treload: Effect.gen(function* () {\n\t\t\t\t\t\tconst next = yield* read\n\t\t\t\t\t\tconst previous = yield* Ref.getAndSet(loaded, next)\n\t\t\t\t\t\treturn next.text !== previous.text\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t)\n\t\t}),\n\t)\n\n/**\n * The handlers below let interruption through, and with it any failure raised\n * while stopping. Log those; failures without interruption are handled there.\n */\nconst logStopped =\n\t(file: string) =>\n\t<A, E>(exit: Exit.Exit<A, E>) =>\n\t\tExit.isFailure(exit) &&\n\t\tCause.hasInterrupts(exit.cause) &&\n\t\t(Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause))\n\t\t\t? Effect.logError(`Watching ${file} stopped with a failure`, exit.cause)\n\t\t\t: Effect.void\n\n/**\n * Reloads the file when it changes and retries the loader's entries, which\n * reads `!env` again. A file that cannot be read keeps its previous values.\n */\nexport const watchEnvFile = Effect.fnUntraced(function* <E, R>(\n\tfile: string,\n\treload: Effect.Effect<boolean, PlatformError.PlatformError>,\n\tretry: Effect.Effect<void, E, R>,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tconst ready = yield* Deferred.make<void>()\n\t// As in HMR, own the source scope inside the producer. Observe its\n\t// complete Exit after cleanup, before merge can discard it.\n\tconst events = Stream.callback<\n\t\tFileSystem.WatchEvent,\n\t\tPlatformError.PlatformError\n\t>(\n\t\t(queue) =>\n\t\t\tfs\n\t\t\t\t.watch(path.dirname(file), {\n\t\t\t\t\tonReady: () => Deferred.doneUnsafe(ready, Effect.void),\n\t\t\t\t})\n\t\t\t\t.pipe(\n\t\t\t\t\tStream.filter(\n\t\t\t\t\t\t(event) => path.basename(event.path) === path.basename(file),\n\t\t\t\t\t),\n\t\t\t\t\tStream.runForEachArray((events) => Queue.offerAll(queue, events)),\n\t\t\t\t\tEffect.scoped,\n\t\t\t\t\t(effect) =>\n\t\t\t\t\t\tobserveShutdown(\n\t\t\t\t\t\t\teffect,\n\t\t\t\t\t\t\t`Watching ${file} stopped with interrupted cleanup`,\n\t\t\t\t\t\t),\n\t\t\t\t\tEffect.onExit(logStopped(file)),\n\t\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t\t(cause) => Effect.logError(`Watching ${file} failed`, cause),\n\t\t\t\t\t),\n\t\t\t\t\t// A failed native backend must not stop content reconciliation.\n\t\t\t\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\t\t\t\tEffect.forever,\n\t\t\t\t),\n\t\t{ bufferSize: 16 },\n\t)\n\t// Native events accelerate updates. Poll the request too: its target\n\t// may live outside this directory, change, or lose a notification.\n\tyield* Stream.mergeAll(\n\t\t[\n\t\t\tStream.as(events, undefined),\n\t\t\tStream.fromEffect(Deferred.await(ready)),\n\t\t\tStream.tick(\"1 second\").pipe(Stream.drop(1)),\n\t\t],\n\t\t{ concurrency: \"unbounded\" },\n\t).pipe(\n\t\tStream.runForEach(() =>\n\t\t\t// Delay to let writes settle.\n\t\t\tEffect.sleep(\"100 millis\").pipe(\n\t\t\t\tEffect.andThen(reload),\n\t\t\t\tEffect.flatMap((changed) => (changed ? retry : Effect.void)),\n\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t(cause) => Effect.logError(`Reloading ${file} failed`, cause),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t\tEffect.catchCauseIf(\n\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t(cause) => Effect.logError(`Stopped watching ${file}`, cause),\n\t\t),\n\t\t// Observe the whole watcher before forkScoped owns it. The steps\n\t\t// before the stream cannot fail, so this covers every failure.\n\t\tEffect.onExit(logStopped(file)),\n\t)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"env.js","names":[],"sources":["../../src/internal/env.ts"],"sourcesContent":["/**\n * Where `!env` values come from: the process environment, then an optional\n * `.env` file, then an optional directory of secret files. Watch mode reads\n * the file and the directory again when they change.\n */\nimport {\n\tArray,\n\tCause,\n\tConfigProvider,\n\tContext,\n\tDeferred,\n\tEffect,\n\tExit,\n\tFileSystem,\n\tLayer,\n\tOption,\n\tPath,\n\tQueue,\n\tRecord,\n\tRef,\n\tStream,\n\tString,\n\ttype PlatformError,\n} from \"effect\"\n\nimport { observeShutdown } from \"./observeShutdown\"\n\n// An empty variable is still set: it overrides the file, and the plugin's\n// schema decides whether it is valid.\nconst options = { preserveEmptyStrings: true } as const\n\ntype Reload = Effect.Effect<boolean, PlatformError.PlatformError>\n\nexport class EnvFile extends Context.Service<\n\tEnvFile,\n\t{\n\t\t/** Reads the file again; reports whether its contents changed. */\n\t\treadonly reload: Reload\n\t}\n>()(\"@graftland/cli/internal/env/EnvFile\") {}\n\nexport class EnvDir extends Context.Service<\n\tEnvDir,\n\t{\n\t\t/** Reads the directory again; reports whether any value changed. */\n\t\treadonly reload: Reload\n\t}\n>()(\"@graftland/cli/internal/env/EnvDir\") {}\n\nconst unchanged = { reload: Effect.succeed(false) }\n\n/**\n * A source that keeps the provider read from its latest snapshot. A failed\n * read keeps the previous snapshot.\n */\nconst source = Effect.fnUntraced(function* <A>(\n\tread: Effect.Effect<A, PlatformError.PlatformError>,\n\tprovider: (snapshot: A) => ConfigProvider.ConfigProvider,\n\tsame: (a: A, b: A) => boolean,\n) {\n\tconst initial = yield* read\n\tconst loaded = yield* Ref.make({\n\t\tsnapshot: initial,\n\t\tprovider: provider(initial),\n\t})\n\treturn {\n\t\tprovider: ConfigProvider.make((path) =>\n\t\t\tEffect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),\n\t\t),\n\t\treload: Effect.gen(function* () {\n\t\t\tconst snapshot = yield* read\n\t\t\tconst previous = yield* Ref.getAndSet(loaded, {\n\t\t\t\tsnapshot,\n\t\t\t\tprovider: provider(snapshot),\n\t\t\t})\n\t\t\treturn !same(snapshot, previous.snapshot)\n\t\t}),\n\t}\n})\n\nconst readFile = Effect.fnUntraced(function* (file: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\t// Resolve the request on every read, including polling. Never pin a\n\t// symlink to its initial target.\n\tconst target = yield* fs.realPath(file)\n\treturn yield* fs.readFileString(target)\n})\n\nconst isNotFound = (error: PlatformError.PlatformError) =>\n\terror.reason._tag === \"NotFound\"\n\n/**\n * One variable per regular file, named after the file. Hidden entries are\n * skipped, which also skips the bookkeeping links of Kubernetes volumes.\n * Trailing line breaks are not part of the value.\n */\nconst readDir = Effect.fnUntraced(function* (directory: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tconst names = yield* fs.readDirectory(directory)\n\tconst values = yield* Effect.forEach(\n\t\tArray.filter(names, (name) => !String.startsWith(\".\")(name)),\n\t\t(name) => {\n\t\t\tconst file = path.join(directory, name)\n\t\t\treturn fs.stat(file).pipe(\n\t\t\t\tEffect.flatMap((info) =>\n\t\t\t\t\tinfo.type === \"File\"\n\t\t\t\t\t\t? fs\n\t\t\t\t\t\t\t\t.readFileString(file)\n\t\t\t\t\t\t\t\t.pipe(\n\t\t\t\t\t\t\t\t\tEffect.map((text) =>\n\t\t\t\t\t\t\t\t\t\tOption.some([\n\t\t\t\t\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\t\t\t\t\tString.replace(/[\\r\\n]+$/, \"\")(text),\n\t\t\t\t\t\t\t\t\t\t] as const),\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: Effect.succeedNone,\n\t\t\t\t),\n\t\t\t\t// A file removed since the listing is no longer a variable.\n\t\t\t\tEffect.catchIf(isNotFound, () => Effect.succeedNone),\n\t\t\t)\n\t\t},\n\t\t{ concurrency: 16 },\n\t)\n\treturn Record.fromEntries(Array.getSomes(values))\n})\n\nconst sameValues = Record.makeEquivalence(String.Equivalence)\n\n/**\n * Provides `ConfigProvider`, `EnvFile` and `EnvDir`. A missing file or\n * directory fails the layer.\n */\nexport const layer = (sources: {\n\treadonly file?: string | undefined\n\treadonly dir?: string | undefined\n}) =>\n\tLayer.unwrap(\n\t\tEffect.gen(function* () {\n\t\t\tconst fs = yield* FileSystem.FileSystem\n\t\t\tconst path = yield* Path.Path\n\t\t\tconst provide = <A>(\n\t\t\t\tread: Effect.Effect<\n\t\t\t\t\tA,\n\t\t\t\t\tPlatformError.PlatformError,\n\t\t\t\t\tFileSystem.FileSystem | Path.Path\n\t\t\t\t>,\n\t\t\t) =>\n\t\t\t\tread.pipe(\n\t\t\t\t\tEffect.provideService(FileSystem.FileSystem, fs),\n\t\t\t\t\tEffect.provideService(Path.Path, path),\n\t\t\t\t)\n\t\t\tconst file =\n\t\t\t\tsources.file === undefined\n\t\t\t\t\t? undefined\n\t\t\t\t\t: yield* source(\n\t\t\t\t\t\t\tprovide(readFile(sources.file)),\n\t\t\t\t\t\t\t(text) => ConfigProvider.fromDotEnvContents(text, options),\n\t\t\t\t\t\t\t(a, b) => a === b,\n\t\t\t\t\t\t)\n\t\t\tconst dir =\n\t\t\t\tsources.dir === undefined\n\t\t\t\t\t? undefined\n\t\t\t\t\t: yield* source(\n\t\t\t\t\t\t\tprovide(readDir(sources.dir)),\n\t\t\t\t\t\t\t(values) => ConfigProvider.fromEnvRecord(values, options),\n\t\t\t\t\t\t\tsameValues,\n\t\t\t\t\t\t)\n\t\t\tconst provider = Array.reduce(\n\t\t\t\t[file, dir],\n\t\t\t\tConfigProvider.fromEnv(options),\n\t\t\t\t(first, next) =>\n\t\t\t\t\tnext === undefined\n\t\t\t\t\t\t? first\n\t\t\t\t\t\t: ConfigProvider.orElse(first, next.provider),\n\t\t\t)\n\t\t\treturn Layer.mergeAll(\n\t\t\t\tConfigProvider.layer(provider),\n\t\t\t\tLayer.succeed(EnvFile, file ?? unchanged),\n\t\t\t\tLayer.succeed(EnvDir, dir ?? unchanged),\n\t\t\t)\n\t\t}),\n\t)\n\n/**\n * The handlers below let interruption through, and with it any failure raised\n * while stopping. Log those; failures without interruption are handled there.\n */\nconst logStopped =\n\t(file: string) =>\n\t<A, E>(exit: Exit.Exit<A, E>) =>\n\t\tExit.isFailure(exit) &&\n\t\tCause.hasInterrupts(exit.cause) &&\n\t\t(Cause.hasFails(exit.cause) || Cause.hasDies(exit.cause))\n\t\t\t? Effect.logError(`Watching ${file} stopped with a failure`, exit.cause)\n\t\t\t: Effect.void\n\n/**\n * Reloads the file when it changes and retries the loader's entries, which\n * reads `!env` again. A file that cannot be read keeps its previous values.\n */\nexport const watchEnvFile = Effect.fnUntraced(function* <E, R>(\n\tfile: string,\n\treload: Reload,\n\tretry: Effect.Effect<void, E, R>,\n) {\n\tconst path = yield* Path.Path\n\tconst name = path.basename(file)\n\treturn yield* watchSource(\n\t\tfile,\n\t\tpath.dirname(file),\n\t\t(changed) => path.basename(changed) === name,\n\t\treload,\n\t\tretry,\n\t)\n})\n\n/**\n * Reloads the directory when anything in it changes and retries the loader's\n * entries. A directory that cannot be read keeps its previous values.\n */\nexport const watchEnvDir = <E, R>(\n\tdirectory: string,\n\treload: Reload,\n\tretry: Effect.Effect<void, E, R>,\n) => watchSource(directory, directory, () => true, reload, retry)\n\nconst watchSource = Effect.fnUntraced(function* <E, R>(\n\tfile: string,\n\tdirectory: string,\n\tmatches: (path: string) => boolean,\n\treload: Reload,\n\tretry: Effect.Effect<void, E, R>,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst ready = yield* Deferred.make<void>()\n\t// As in HMR, own the source scope inside the producer. Observe its\n\t// complete Exit after cleanup, before merge can discard it.\n\tconst events = Stream.callback<\n\t\tFileSystem.WatchEvent,\n\t\tPlatformError.PlatformError\n\t>(\n\t\t(queue) =>\n\t\t\tfs\n\t\t\t\t.watch(directory, {\n\t\t\t\t\tonReady: () => Deferred.doneUnsafe(ready, Effect.void),\n\t\t\t\t})\n\t\t\t\t.pipe(\n\t\t\t\t\tStream.filter((event) => matches(event.path)),\n\t\t\t\t\tStream.runForEachArray((events) => Queue.offerAll(queue, events)),\n\t\t\t\t\tEffect.scoped,\n\t\t\t\t\t(effect) =>\n\t\t\t\t\t\tobserveShutdown(\n\t\t\t\t\t\t\teffect,\n\t\t\t\t\t\t\t`Watching ${file} stopped with interrupted cleanup`,\n\t\t\t\t\t\t),\n\t\t\t\t\tEffect.onExit(logStopped(file)),\n\t\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t\t(cause) => Effect.logError(`Watching ${file} failed`, cause),\n\t\t\t\t\t),\n\t\t\t\t\t// A failed native backend must not stop content reconciliation.\n\t\t\t\t\tEffect.andThen(Effect.sleep(\"1 second\")),\n\t\t\t\t\tEffect.forever,\n\t\t\t\t),\n\t\t{ bufferSize: 16 },\n\t)\n\t// Native events accelerate updates. Poll the request too: its target\n\t// may live outside this directory, change, or lose a notification.\n\tyield* Stream.mergeAll(\n\t\t[\n\t\t\tStream.as(events, undefined),\n\t\t\tStream.fromEffect(Deferred.await(ready)),\n\t\t\tStream.tick(\"1 second\").pipe(Stream.drop(1)),\n\t\t],\n\t\t{ concurrency: \"unbounded\" },\n\t).pipe(\n\t\tStream.runForEach(() =>\n\t\t\t// Delay to let writes settle.\n\t\t\tEffect.sleep(\"100 millis\").pipe(\n\t\t\t\tEffect.andThen(reload),\n\t\t\t\tEffect.flatMap((changed) => (changed ? retry : Effect.void)),\n\t\t\t\tEffect.catchCauseIf(\n\t\t\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t\t\t(cause) => Effect.logError(`Reloading ${file} failed`, cause),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t\tEffect.catchCauseIf(\n\t\t\t(cause) => !Cause.hasInterrupts(cause),\n\t\t\t(cause) => Effect.logError(`Stopped watching ${file}`, cause),\n\t\t),\n\t\t// Observe the whole watcher before forkScoped owns it. The steps\n\t\t// before the stream cannot fail, so this covers every failure.\n\t\tEffect.onExit(logStopped(file)),\n\t)\n})\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graftland/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22.19.0"
@@ -37,11 +37,11 @@
37
37
  "vitest": "5.1.0"
38
38
  },
39
39
  "dependencies": {
40
- "@graftland/graft": "0.4.1",
41
- "@graftland/loader": "0.4.1",
42
- "@graftland/hmr": "0.4.1",
43
- "@graftland/create": "0.4.1",
44
- "@graftland/package-manager": "0.4.1",
40
+ "@graftland/graft": "0.4.2",
41
+ "@graftland/loader": "0.4.2",
42
+ "@graftland/hmr": "0.4.2",
43
+ "@graftland/create": "0.4.2",
44
+ "@graftland/package-manager": "0.4.2",
45
45
  "@effect/platform-node": "4.1.2",
46
46
  "yaml": "2.9.1"
47
47
  },
package/src/Start.ts CHANGED
@@ -30,7 +30,13 @@ import {
30
30
  } from "@graftland/loader"
31
31
 
32
32
  import { Application, ApplicationError, platform } from "./Application"
33
- import { layer as env, EnvFile, watchEnvFile } from "./internal/env"
33
+ import {
34
+ layer as env,
35
+ EnvDir,
36
+ EnvFile,
37
+ watchEnvDir,
38
+ watchEnvFile,
39
+ } from "./internal/env"
34
40
  import { layer as intercepts } from "./internal/intercepts"
35
41
  import { watchModules } from "./internal/watch"
36
42
 
@@ -44,6 +50,12 @@ export interface Options {
44
50
  * a variable. With `watch`, changes to it are read again.
45
51
  */
46
52
  readonly envFile?: string | undefined
53
+ /**
54
+ * A directory of secret files that `!env` reads when neither the process
55
+ * environment nor `envFile` sets a variable. Each regular file is one
56
+ * variable, named after the file. With `watch`, changes are read again.
57
+ */
58
+ readonly envDir?: string | undefined
47
59
  }
48
60
 
49
61
  /**
@@ -138,7 +150,8 @@ export const run = (
138
150
  yield* included.await
139
151
  if (options.watch) {
140
152
  const { reload } = yield* EnvFile
141
- const envFile = options.envFile
153
+ const { reload: reloadDir } = yield* EnvDir
154
+ const { envFile, envDir } = options
142
155
  const reloader = Plugin.make({
143
156
  name: "reloader",
144
157
  inject: [Loader.Loader, FileSystem.FileSystem, Path.Path],
@@ -152,6 +165,10 @@ export const run = (
152
165
  yield* watchEnvFile(envFile, reload, loader.retry).pipe(
153
166
  Effect.forkScoped,
154
167
  )
168
+ if (envDir !== undefined)
169
+ yield* watchEnvDir(envDir, reloadDir, loader.retry).pipe(
170
+ Effect.forkScoped,
171
+ )
155
172
  }),
156
173
  })
157
174
  yield* Graft.plugin(reloader)
@@ -171,7 +188,9 @@ export const run = (
171
188
  Layer.mergeAll(
172
189
  platform,
173
190
  intercepts,
174
- env(options.envFile).pipe(Layer.provide(NodeServices.layer)),
191
+ env({ file: options.envFile, dir: options.envDir }).pipe(
192
+ Layer.provide(NodeServices.layer),
193
+ ),
175
194
  Logger.layer([Logger.consolePretty()]),
176
195
  ),
177
196
  ),
package/src/bin.ts CHANGED
@@ -31,6 +31,12 @@ const start = Command.make(
31
31
  ),
32
32
  Flag.optional,
33
33
  ),
34
+ envDir: Flag.String("env-dir").pipe(
35
+ Flag.withDescription(
36
+ "Read variables for !env from a directory with one file per variable, such as /run/secrets",
37
+ ),
38
+ Flag.optional,
39
+ ),
34
40
  },
35
41
  (options) =>
36
42
  Effect.promise(() => import("./Start")).pipe(
@@ -39,6 +45,7 @@ const start = Command.make(
39
45
  Start.run({
40
46
  ...options,
41
47
  envFile: Option.getOrUndefined(options.envFile),
48
+ envDir: Option.getOrUndefined(options.envDir),
42
49
  }),
43
50
  ),
44
51
  ),
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Where `!env` values come from: the process environment, then an optional
3
- * `.env` file that watch mode reads again when it changes.
3
+ * `.env` file, then an optional directory of secret files. Watch mode reads
4
+ * the file and the directory again when they change.
4
5
  */
5
6
  import {
7
+ Array,
6
8
  Cause,
7
9
  ConfigProvider,
8
10
  Context,
@@ -11,10 +13,13 @@ import {
11
13
  Exit,
12
14
  FileSystem,
13
15
  Layer,
16
+ Option,
14
17
  Path,
15
18
  Queue,
19
+ Record,
16
20
  Ref,
17
21
  Stream,
22
+ String,
18
23
  type PlatformError,
19
24
  } from "effect"
20
25
 
@@ -24,57 +29,156 @@ import { observeShutdown } from "./observeShutdown"
24
29
  // schema decides whether it is valid.
25
30
  const options = { preserveEmptyStrings: true } as const
26
31
 
27
- interface Loaded {
28
- readonly target: string
29
- readonly text: string
30
- readonly provider: ConfigProvider.ConfigProvider
31
- }
32
+ type Reload = Effect.Effect<boolean, PlatformError.PlatformError>
32
33
 
33
34
  export class EnvFile extends Context.Service<
34
35
  EnvFile,
35
36
  {
36
37
  /** Reads the file again; reports whether its contents changed. */
37
- readonly reload: Effect.Effect<boolean, PlatformError.PlatformError>
38
+ readonly reload: Reload
38
39
  }
39
40
  >()("@graftland/cli/internal/env/EnvFile") {}
40
41
 
41
- /** Provides `ConfigProvider` and `EnvFile`; a missing file fails the layer. */
42
- export const layer = (file: string | undefined) =>
42
+ export class EnvDir extends Context.Service<
43
+ EnvDir,
44
+ {
45
+ /** Reads the directory again; reports whether any value changed. */
46
+ readonly reload: Reload
47
+ }
48
+ >()("@graftland/cli/internal/env/EnvDir") {}
49
+
50
+ const unchanged = { reload: Effect.succeed(false) }
51
+
52
+ /**
53
+ * A source that keeps the provider read from its latest snapshot. A failed
54
+ * read keeps the previous snapshot.
55
+ */
56
+ const source = Effect.fnUntraced(function* <A>(
57
+ read: Effect.Effect<A, PlatformError.PlatformError>,
58
+ provider: (snapshot: A) => ConfigProvider.ConfigProvider,
59
+ same: (a: A, b: A) => boolean,
60
+ ) {
61
+ const initial = yield* read
62
+ const loaded = yield* Ref.make({
63
+ snapshot: initial,
64
+ provider: provider(initial),
65
+ })
66
+ return {
67
+ provider: ConfigProvider.make((path) =>
68
+ Effect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),
69
+ ),
70
+ reload: Effect.gen(function* () {
71
+ const snapshot = yield* read
72
+ const previous = yield* Ref.getAndSet(loaded, {
73
+ snapshot,
74
+ provider: provider(snapshot),
75
+ })
76
+ return !same(snapshot, previous.snapshot)
77
+ }),
78
+ }
79
+ })
80
+
81
+ const readFile = Effect.fnUntraced(function* (file: string) {
82
+ const fs = yield* FileSystem.FileSystem
83
+ // Resolve the request on every read, including polling. Never pin a
84
+ // symlink to its initial target.
85
+ const target = yield* fs.realPath(file)
86
+ return yield* fs.readFileString(target)
87
+ })
88
+
89
+ const isNotFound = (error: PlatformError.PlatformError) =>
90
+ error.reason._tag === "NotFound"
91
+
92
+ /**
93
+ * One variable per regular file, named after the file. Hidden entries are
94
+ * skipped, which also skips the bookkeeping links of Kubernetes volumes.
95
+ * Trailing line breaks are not part of the value.
96
+ */
97
+ const readDir = Effect.fnUntraced(function* (directory: string) {
98
+ const fs = yield* FileSystem.FileSystem
99
+ const path = yield* Path.Path
100
+ const names = yield* fs.readDirectory(directory)
101
+ const values = yield* Effect.forEach(
102
+ Array.filter(names, (name) => !String.startsWith(".")(name)),
103
+ (name) => {
104
+ const file = path.join(directory, name)
105
+ return fs.stat(file).pipe(
106
+ Effect.flatMap((info) =>
107
+ info.type === "File"
108
+ ? fs
109
+ .readFileString(file)
110
+ .pipe(
111
+ Effect.map((text) =>
112
+ Option.some([
113
+ name,
114
+ String.replace(/[\r\n]+$/, "")(text),
115
+ ] as const),
116
+ ),
117
+ )
118
+ : Effect.succeedNone,
119
+ ),
120
+ // A file removed since the listing is no longer a variable.
121
+ Effect.catchIf(isNotFound, () => Effect.succeedNone),
122
+ )
123
+ },
124
+ { concurrency: 16 },
125
+ )
126
+ return Record.fromEntries(Array.getSomes(values))
127
+ })
128
+
129
+ const sameValues = Record.makeEquivalence(String.Equivalence)
130
+
131
+ /**
132
+ * Provides `ConfigProvider`, `EnvFile` and `EnvDir`. A missing file or
133
+ * directory fails the layer.
134
+ */
135
+ export const layer = (sources: {
136
+ readonly file?: string | undefined
137
+ readonly dir?: string | undefined
138
+ }) =>
43
139
  Layer.unwrap(
44
140
  Effect.gen(function* () {
45
- const environment = ConfigProvider.fromEnv(options)
46
- if (file === undefined)
47
- return Layer.merge(
48
- ConfigProvider.layer(environment),
49
- Layer.succeed(EnvFile, { reload: Effect.succeed(false) }),
50
- )
51
141
  const fs = yield* FileSystem.FileSystem
52
- // Resolve the request on every read, including polling. Never pin a
53
- // symlink to its initial target. The provider and its baseline are
54
- // committed together from the bytes actually read.
55
- const read = Effect.gen(function* () {
56
- const target = yield* fs.realPath(file)
57
- const text = yield* fs.readFileString(target)
58
- return {
59
- target,
60
- text,
61
- provider: ConfigProvider.fromDotEnvContents(text, options),
62
- }
63
- })
64
- const initial: Loaded = yield* read
65
- const loaded = yield* Ref.make(initial)
66
- const fromFile = ConfigProvider.make((path) =>
67
- Effect.flatMap(Ref.get(loaded), ({ provider }) => provider.load(path)),
142
+ const path = yield* Path.Path
143
+ const provide = <A>(
144
+ read: Effect.Effect<
145
+ A,
146
+ PlatformError.PlatformError,
147
+ FileSystem.FileSystem | Path.Path
148
+ >,
149
+ ) =>
150
+ read.pipe(
151
+ Effect.provideService(FileSystem.FileSystem, fs),
152
+ Effect.provideService(Path.Path, path),
153
+ )
154
+ const file =
155
+ sources.file === undefined
156
+ ? undefined
157
+ : yield* source(
158
+ provide(readFile(sources.file)),
159
+ (text) => ConfigProvider.fromDotEnvContents(text, options),
160
+ (a, b) => a === b,
161
+ )
162
+ const dir =
163
+ sources.dir === undefined
164
+ ? undefined
165
+ : yield* source(
166
+ provide(readDir(sources.dir)),
167
+ (values) => ConfigProvider.fromEnvRecord(values, options),
168
+ sameValues,
169
+ )
170
+ const provider = Array.reduce(
171
+ [file, dir],
172
+ ConfigProvider.fromEnv(options),
173
+ (first, next) =>
174
+ next === undefined
175
+ ? first
176
+ : ConfigProvider.orElse(first, next.provider),
68
177
  )
69
- return Layer.merge(
70
- ConfigProvider.layer(ConfigProvider.orElse(environment, fromFile)),
71
- Layer.succeed(EnvFile, {
72
- reload: Effect.gen(function* () {
73
- const next = yield* read
74
- const previous = yield* Ref.getAndSet(loaded, next)
75
- return next.text !== previous.text
76
- }),
77
- }),
178
+ return Layer.mergeAll(
179
+ ConfigProvider.layer(provider),
180
+ Layer.succeed(EnvFile, file ?? unchanged),
181
+ Layer.succeed(EnvDir, dir ?? unchanged),
78
182
  )
79
183
  }),
80
184
  )
@@ -98,11 +202,38 @@ const logStopped =
98
202
  */
99
203
  export const watchEnvFile = Effect.fnUntraced(function* <E, R>(
100
204
  file: string,
101
- reload: Effect.Effect<boolean, PlatformError.PlatformError>,
205
+ reload: Reload,
102
206
  retry: Effect.Effect<void, E, R>,
103
207
  ) {
104
- const fs = yield* FileSystem.FileSystem
105
208
  const path = yield* Path.Path
209
+ const name = path.basename(file)
210
+ return yield* watchSource(
211
+ file,
212
+ path.dirname(file),
213
+ (changed) => path.basename(changed) === name,
214
+ reload,
215
+ retry,
216
+ )
217
+ })
218
+
219
+ /**
220
+ * Reloads the directory when anything in it changes and retries the loader's
221
+ * entries. A directory that cannot be read keeps its previous values.
222
+ */
223
+ export const watchEnvDir = <E, R>(
224
+ directory: string,
225
+ reload: Reload,
226
+ retry: Effect.Effect<void, E, R>,
227
+ ) => watchSource(directory, directory, () => true, reload, retry)
228
+
229
+ const watchSource = Effect.fnUntraced(function* <E, R>(
230
+ file: string,
231
+ directory: string,
232
+ matches: (path: string) => boolean,
233
+ reload: Reload,
234
+ retry: Effect.Effect<void, E, R>,
235
+ ) {
236
+ const fs = yield* FileSystem.FileSystem
106
237
  const ready = yield* Deferred.make<void>()
107
238
  // As in HMR, own the source scope inside the producer. Observe its
108
239
  // complete Exit after cleanup, before merge can discard it.
@@ -112,13 +243,11 @@ export const watchEnvFile = Effect.fnUntraced(function* <E, R>(
112
243
  >(
113
244
  (queue) =>
114
245
  fs
115
- .watch(path.dirname(file), {
246
+ .watch(directory, {
116
247
  onReady: () => Deferred.doneUnsafe(ready, Effect.void),
117
248
  })
118
249
  .pipe(
119
- Stream.filter(
120
- (event) => path.basename(event.path) === path.basename(file),
121
- ),
250
+ Stream.filter((event) => matches(event.path)),
122
251
  Stream.runForEachArray((events) => Queue.offerAll(queue, events)),
123
252
  Effect.scoped,
124
253
  (effect) =>