@graftland/cli 0.4.0 → 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/README.md CHANGED
@@ -11,8 +11,9 @@ The `graft` command: `start`, `create`, `add` and `remove`.
11
11
  npm install @graftland/cli effect
12
12
  ```
13
13
 
14
- Create a project with `npx @graftland/cli create my-app --install`. Then run it
15
- with `npx graft start --watch`. You need Node.js 22.19 or later.
14
+ Create a project with `npm create @graftland@latest my-app`, which runs the same
15
+ `create` command. Then run it with `npm run dev`. You need Node.js 22.19 or
16
+ later.
16
17
 
17
18
  ## Documentation
18
19
 
package/README.zh-CN.md CHANGED
@@ -10,8 +10,8 @@
10
10
  npm install @graftland/cli effect
11
11
  ```
12
12
 
13
- 用 `npx @graftland/cli create my-app --install` 创建项目,然后用
14
- `npx graft start --watch` 运行它。需要 Node.js 22.19 或更高版本。
13
+ 用 `npm create @graftland@latest my-app` 创建项目,它运行的就是同一个 `create`
14
+ 命令。然后用 `npm run dev` 运行它。需要 Node.js 22.19 或更高版本。
15
15
 
16
16
  ## 文档
17
17
 
package/README.zh-TW.md CHANGED
@@ -10,8 +10,8 @@
10
10
  npm install @graftland/cli effect
11
11
  ```
12
12
 
13
- 用 `npx @graftland/cli create my-app --install` 建立專案,然後用
14
- `npx graft start --watch` 執行它。需要 Node.js 22.19 或更新版本。
13
+ 用 `npm create @graftland@latest my-app` 建立專案,它執行的是同一個 `create`
14
+ 命令。然後用 `npm run dev` 執行它。需要 Node.js 22.19 或更新版本。
15
15
 
16
16
  ## 文件
17
17
 
package/dist/Manage.d.ts CHANGED
@@ -1,15 +1,9 @@
1
1
  import { CommandConfigError } from "./Commands.js";
2
- import { Config, Effect, FileSystem, Option, Path } from "effect";
3
- import * as Create from "@graftland/create";
2
+ import { Config, Effect, FileSystem, Path } from "effect";
4
3
  import * as PackageManager from "@graftland/package-manager";
5
4
  declare namespace Manage_d_exports {
6
- export { add, addPlugin, create, remove };
5
+ export { add, addPlugin, remove };
7
6
  }
8
- export declare const create: (options: {
9
- readonly directory: string;
10
- readonly name: Option.Option<string>;
11
- readonly install: boolean;
12
- }) => Effect.Effect<void, Config.ConfigError | Create.CreateError | Create.InvalidName | Create.InvalidPackageManager | PackageManager.Error, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
13
7
  export declare const add: (specs: readonly string[]) => Effect.Effect<void, Config.ConfigError | PackageManager.Error, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
14
8
  export declare const remove: (names: ReadonlyArray<string>) => Effect.Effect<void, Config.ConfigError | PackageManager.Error, import("effect/process/ChildProcessSpawner").ChildProcessSpawner | FileSystem.FileSystem | Path.Path>;
15
9
  /** Install a command plugin and explicitly enable its /cli entry. */
@@ -1 +1 @@
1
- {"version":3,"file":"Manage.d.ts","names":[],"sources":["../src/Manage.ts"],"mappings":";;;;;;;qBAqEa,SAAM;WACE;WACL,MAAA,OAAO;WACJ;MAWjB,OAAA,aAAA,OAAA,cAAA,OAAA,cAAA,OAAA,cAAA,OAAA,wBAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;qBAEW,MAAG,6BAAA,OAAA,aAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;qBAgBH,SAAM,OAAW,0BAAqB,OAAA,aAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;;qBAuDtC,YAAS,cAAA,iCAAA,OAAA,aAAA,qBAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA"}
1
+ {"version":3,"file":"Manage.d.ts","names":[],"sources":["../src/Manage.ts"],"mappings":";;;;;;qBAoEa,MAAG,6BAAA,OAAA,aAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;qBAgBH,SAAM,OAAW,0BAAqB,OAAA,aAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA;;qBAuDtC,YAAS,cAAA,iCAAA,OAAA,aAAA,qBAAA,OAAA,cAAA,eAAA,oDAAA,sBAAA,WAAA,aAAA,KAAA"}
package/dist/Manage.js CHANGED
@@ -1,18 +1,17 @@
1
1
  import { __exportAll } from "./_virtual/_rolldown/runtime.js";
2
2
  import { CommandConfigError } from "./Commands.js";
3
3
  import { home } from "./internal/commandConfig.js";
4
- import { Array, Config, Effect, FileSystem, Option, Path, Predicate, Record, Schema, String } from "effect";
4
+ import { Array, Config, Effect, FileSystem, Option, Path, Predicate, Record, Schema } from "effect";
5
5
  import * as Create from "@graftland/create";
6
6
  import * as PackageManager from "@graftland/package-manager";
7
7
  import { parseDocument } from "yaml";
8
8
  // src/Manage.ts
9
9
  /**
10
- * Creating projects and changing their dependencies.
10
+ * Changing a project's dependencies.
11
11
  */
12
12
  var Manage_exports = /* @__PURE__ */ __exportAll({
13
13
  add: () => add,
14
14
  addPlugin: () => addPlugin,
15
- create: () => create,
16
15
  remove: () => remove
17
16
  });
18
17
  const report = (result) => Effect.logInfo(`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`).pipe(Effect.andThen(Option.match(result.snapshotCleanupFailure, {
@@ -38,17 +37,6 @@ const install = Effect.fn("Manage.install")(function* (directory, changes) {
38
37
  })).install(decoded);
39
38
  yield* report(result);
40
39
  });
41
- const create = Effect.fn("Manage.create")(function* (options) {
42
- const project = yield* Create.create({
43
- directory: options.directory,
44
- ...Option.match(options.name, {
45
- onNone: () => ({}),
46
- onSome: (name) => ({ name })
47
- })
48
- });
49
- yield* Effect.logInfo(`Created ${project.name} in ${project.directory}`);
50
- if (options.install) yield* install(project.directory, {});
51
- });
52
40
  const add = Effect.fn("Manage.add")(function* (specs) {
53
41
  const decoded = yield* decodeSpecs(specs);
54
42
  yield* install(".", Record.fromEntries(Array.map(decoded, (spec) => Predicate.isString(spec) ? [spec, "latest"] : [spec[0], spec[2]])));
@@ -99,7 +87,7 @@ const addPlugin = Effect.fn("Manage.addPlugin")(function* (name, global = false)
99
87
  const path = yield* Path.Path;
100
88
  const fs = yield* FileSystem.FileSystem;
101
89
  const directory = global ? yield* home : path.resolve(".");
102
- const packageName = String.startsWith("@")(name) || String.startsWith("graft-plugin-")(name) ? name : `@graftland/plugin-${name}`;
90
+ const packageName = Create.pluginPackage(name);
103
91
  const module = `${packageName}/cli`;
104
92
  const file = path.join(directory, "graft.yml");
105
93
  const text = yield* enabledConfig(file, module);
@@ -108,6 +96,6 @@ const addPlugin = Effect.fn("Manage.addPlugin")(function* (name, global = false)
108
96
  yield* fs.writeFileString(file, text).pipe(Effect.uninterruptible);
109
97
  yield* Effect.logInfo(`Enabled ${module} in ${file}`);
110
98
  });
111
- export { Manage_exports, add, addPlugin, create, remove };
99
+ export { Manage_exports, add, addPlugin, remove };
112
100
 
113
101
  //# sourceMappingURL=Manage.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Manage.js","names":[],"sources":["../src/Manage.ts"],"sourcesContent":["/**\n * Creating projects and changing their dependencies.\n */\nimport {\n\tArray,\n\tConfig,\n\tEffect,\n\tFileSystem,\n\tOption,\n\tPath,\n\tPredicate,\n\tRecord,\n\tSchema,\n\tString,\n} from \"effect\"\n\nimport * as Create from \"@graftland/create\"\nimport * as PackageManager from \"@graftland/package-manager\"\n\nimport { parseDocument, type Document } from \"yaml\"\n\nimport { CommandConfigError } from \"./Commands\"\nimport { home } from \"./internal/commandConfig\"\n\nconst report = (result: PackageManager.InstallResult) =>\n\tEffect.logInfo(\n\t\t`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`,\n\t).pipe(\n\t\tEffect.andThen(\n\t\t\tOption.match(result.snapshotCleanupFailure, {\n\t\t\t\tonNone: () => Effect.void,\n\t\t\t\tonSome: ({ cause, snapshotDirectory }) =>\n\t\t\t\t\tEffect.logWarning(\n\t\t\t\t\t\t\"Installation committed; snapshot cleanup failed. Retry removing the retained snapshot.\",\n\t\t\t\t\t\tcause,\n\t\t\t\t\t\t{ snapshotDirectory },\n\t\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t)\n\n/**\n * `name@range`, split at the last `@` so a scope's `@` stays in the name. A\n * bare name means the latest version.\n */\nconst Spec = Schema.Union([\n\tSchema.TemplateLiteralParser([\n\t\tPackageManager.PackageName,\n\t\t\"@\",\n\t\tSchema.String,\n\t]),\n\tPackageManager.PackageName,\n])\nconst decodeSpecs = Schema.decodeEffect(Schema.Array(Spec))\n\nconst install = Effect.fn(\"Manage.install\")(function* (\n\tdirectory: string,\n\tchanges: Readonly<Record<string, string | null>>,\n) {\n\tconst decoded = yield* Schema.decodeEffect(PackageManager.Changes)(changes)\n\t// Set by npm, pnpm and Yarn when they run graft, such as through `pnpm dlx`.\n\tconst userAgent = yield* Config.String(\"npm_config_user_agent\").pipe(\n\t\tConfig.withDefault(\"\"),\n\t)\n\tconst manager = yield* PackageManager.make({ directory, userAgent })\n\tconst result = yield* manager.install(decoded)\n\tyield* report(result)\n})\n\nexport const create = Effect.fn(\"Manage.create\")(function* (options: {\n\treadonly directory: string\n\treadonly name: Option.Option<string>\n\treadonly install: boolean\n}) {\n\tconst project = yield* Create.create({\n\t\tdirectory: options.directory,\n\t\t...Option.match(options.name, {\n\t\t\tonNone: () => ({}),\n\t\t\tonSome: (name) => ({ name }),\n\t\t}),\n\t})\n\tyield* Effect.logInfo(`Created ${project.name} in ${project.directory}`)\n\tif (options.install) yield* install(project.directory, {})\n})\n\nexport const add = Effect.fn(\"Manage.add\")(function* (\n\tspecs: ReadonlyArray<string>,\n) {\n\tconst decoded = yield* decodeSpecs(specs)\n\tyield* install(\n\t\t\".\",\n\t\tRecord.fromEntries(\n\t\t\tArray.map(decoded, (spec) =>\n\t\t\t\tPredicate.isString(spec)\n\t\t\t\t\t? ([spec, \"latest\"] as const)\n\t\t\t\t\t: ([spec[0], spec[2]] as const),\n\t\t\t),\n\t\t),\n\t)\n})\n\nexport const remove = (names: ReadonlyArray<string>) =>\n\tinstall(\n\t\t\".\",\n\t\tRecord.fromEntries(Array.map(names, (name) => [name, null] as const)),\n\t)\n\nconst enabledConfig = Effect.fnUntraced(function* (\n\tfile: string,\n\tmodule: string,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst text = (yield* fs.exists(file))\n\t\t? yield* fs.readFileString(file)\n\t\t: \"[]\\n\"\n\tconst document = yield* Effect.try({\n\t\ttry: () => {\n\t\t\tconst document: Document = parseDocument(text)\n\t\t\tconst error = document.errors[0]\n\t\t\tif (error !== undefined) throw error\n\t\t\tif (document.toJS() === null) document.contents = document.createNode([])\n\t\t\treturn document\n\t\t},\n\t\tcatch: (cause) => CommandConfigError.make({ file, cause }),\n\t})\n\tconst entries = yield* Schema.decodeUnknownEffect(\n\t\tSchema.Array(Schema.Unknown),\n\t)(document.toJS())\n\tconst existing = Array.findFirstIndex(\n\t\tentries,\n\t\t(entry) =>\n\t\t\tPredicate.hasProperty(entry, \"name\") &&\n\t\t\tentry.name === module &&\n\t\t\tPredicate.hasProperty(entry, \"cli\") &&\n\t\t\tentry.cli === true,\n\t)\n\treturn yield* Effect.try({\n\t\ttry: () => {\n\t\t\tif (Option.isSome(existing))\n\t\t\t\tdocument.setIn([existing.value, \"disabled\"], false)\n\t\t\telse document.add({ name: module, cli: true })\n\t\t\treturn document.toString()\n\t\t},\n\t\tcatch: (cause) => CommandConfigError.make({ file, cause }),\n\t})\n})\nconst prepareHome = Effect.fnUntraced(function* (directory: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tyield* fs.makeDirectory(directory, { recursive: true })\n\tconst manifest = path.join(directory, \"package.json\")\n\tif (!(yield* fs.exists(manifest)))\n\t\tyield* fs.writeFileString(manifest, '{\"private\":true,\"type\":\"module\"}\\n')\n}, Effect.uninterruptible)\n\n/** Install a command plugin and explicitly enable its /cli entry. */\nexport const addPlugin = Effect.fn(\"Manage.addPlugin\")(function* (\n\tname: string,\n\tglobal: boolean = false,\n) {\n\tconst path = yield* Path.Path\n\tconst fs = yield* FileSystem.FileSystem\n\tconst directory = global ? yield* home : path.resolve(\".\")\n\tconst packageName =\n\t\tString.startsWith(\"@\")(name) || String.startsWith(\"graft-plugin-\")(name)\n\t\t\t? name\n\t\t\t: `@graftland/plugin-${name}`\n\tconst module = `${packageName}/cli`\n\tconst file = path.join(directory, \"graft.yml\")\n\tconst text = yield* enabledConfig(file, module)\n\tif (global) yield* prepareHome(directory)\n\tyield* install(directory, Record.singleton(packageName, \"latest\"))\n\t// Once accepted, the native write must finish before command cleanup can proceed.\n\tyield* fs.writeFileString(file, text).pipe(Effect.uninterruptible)\n\tyield* Effect.logInfo(`Enabled ${module} in ${file}`)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"Manage.js","names":[],"sources":["../src/Manage.ts"],"sourcesContent":["/**\n * Changing a project's dependencies.\n */\nimport {\n\tArray,\n\tConfig,\n\tEffect,\n\tFileSystem,\n\tOption,\n\tPath,\n\tPredicate,\n\tRecord,\n\tSchema,\n} from \"effect\"\n\nimport * as Create from \"@graftland/create\"\nimport * as PackageManager from \"@graftland/package-manager\"\n\nimport { parseDocument, type Document } from \"yaml\"\n\nimport { CommandConfigError } from \"./Commands\"\nimport { home } from \"./internal/commandConfig\"\n\nconst report = (result: PackageManager.InstallResult) =>\n\tEffect.logInfo(\n\t\t`${result.agent}: ${result.added.length} added, ${result.updated.length} updated, ${result.removed.length} removed`,\n\t).pipe(\n\t\tEffect.andThen(\n\t\t\tOption.match(result.snapshotCleanupFailure, {\n\t\t\t\tonNone: () => Effect.void,\n\t\t\t\tonSome: ({ cause, snapshotDirectory }) =>\n\t\t\t\t\tEffect.logWarning(\n\t\t\t\t\t\t\"Installation committed; snapshot cleanup failed. Retry removing the retained snapshot.\",\n\t\t\t\t\t\tcause,\n\t\t\t\t\t\t{ snapshotDirectory },\n\t\t\t\t\t),\n\t\t\t}),\n\t\t),\n\t)\n\n/**\n * `name@range`, split at the last `@` so a scope's `@` stays in the name. A\n * bare name means the latest version.\n */\nconst Spec = Schema.Union([\n\tSchema.TemplateLiteralParser([\n\t\tPackageManager.PackageName,\n\t\t\"@\",\n\t\tSchema.String,\n\t]),\n\tPackageManager.PackageName,\n])\nconst decodeSpecs = Schema.decodeEffect(Schema.Array(Spec))\n\nconst install = Effect.fn(\"Manage.install\")(function* (\n\tdirectory: string,\n\tchanges: Readonly<Record<string, string | null>>,\n) {\n\tconst decoded = yield* Schema.decodeEffect(PackageManager.Changes)(changes)\n\t// Set by npm, pnpm and Yarn when they run graft, such as through `pnpm dlx`.\n\tconst userAgent = yield* Config.String(\"npm_config_user_agent\").pipe(\n\t\tConfig.withDefault(\"\"),\n\t)\n\tconst manager = yield* PackageManager.make({ directory, userAgent })\n\tconst result = yield* manager.install(decoded)\n\tyield* report(result)\n})\n\nexport const add = Effect.fn(\"Manage.add\")(function* (\n\tspecs: ReadonlyArray<string>,\n) {\n\tconst decoded = yield* decodeSpecs(specs)\n\tyield* install(\n\t\t\".\",\n\t\tRecord.fromEntries(\n\t\t\tArray.map(decoded, (spec) =>\n\t\t\t\tPredicate.isString(spec)\n\t\t\t\t\t? ([spec, \"latest\"] as const)\n\t\t\t\t\t: ([spec[0], spec[2]] as const),\n\t\t\t),\n\t\t),\n\t)\n})\n\nexport const remove = (names: ReadonlyArray<string>) =>\n\tinstall(\n\t\t\".\",\n\t\tRecord.fromEntries(Array.map(names, (name) => [name, null] as const)),\n\t)\n\nconst enabledConfig = Effect.fnUntraced(function* (\n\tfile: string,\n\tmodule: string,\n) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst text = (yield* fs.exists(file))\n\t\t? yield* fs.readFileString(file)\n\t\t: \"[]\\n\"\n\tconst document = yield* Effect.try({\n\t\ttry: () => {\n\t\t\tconst document: Document = parseDocument(text)\n\t\t\tconst error = document.errors[0]\n\t\t\tif (error !== undefined) throw error\n\t\t\tif (document.toJS() === null) document.contents = document.createNode([])\n\t\t\treturn document\n\t\t},\n\t\tcatch: (cause) => CommandConfigError.make({ file, cause }),\n\t})\n\tconst entries = yield* Schema.decodeUnknownEffect(\n\t\tSchema.Array(Schema.Unknown),\n\t)(document.toJS())\n\tconst existing = Array.findFirstIndex(\n\t\tentries,\n\t\t(entry) =>\n\t\t\tPredicate.hasProperty(entry, \"name\") &&\n\t\t\tentry.name === module &&\n\t\t\tPredicate.hasProperty(entry, \"cli\") &&\n\t\t\tentry.cli === true,\n\t)\n\treturn yield* Effect.try({\n\t\ttry: () => {\n\t\t\tif (Option.isSome(existing))\n\t\t\t\tdocument.setIn([existing.value, \"disabled\"], false)\n\t\t\telse document.add({ name: module, cli: true })\n\t\t\treturn document.toString()\n\t\t},\n\t\tcatch: (cause) => CommandConfigError.make({ file, cause }),\n\t})\n})\nconst prepareHome = Effect.fnUntraced(function* (directory: string) {\n\tconst fs = yield* FileSystem.FileSystem\n\tconst path = yield* Path.Path\n\tyield* fs.makeDirectory(directory, { recursive: true })\n\tconst manifest = path.join(directory, \"package.json\")\n\tif (!(yield* fs.exists(manifest)))\n\t\tyield* fs.writeFileString(manifest, '{\"private\":true,\"type\":\"module\"}\\n')\n}, Effect.uninterruptible)\n\n/** Install a command plugin and explicitly enable its /cli entry. */\nexport const addPlugin = Effect.fn(\"Manage.addPlugin\")(function* (\n\tname: string,\n\tglobal: boolean = false,\n) {\n\tconst path = yield* Path.Path\n\tconst fs = yield* FileSystem.FileSystem\n\tconst directory = global ? yield* home : path.resolve(\".\")\n\tconst packageName = Create.pluginPackage(name)\n\tconst module = `${packageName}/cli`\n\tconst file = path.join(directory, \"graft.yml\")\n\tconst text = yield* enabledConfig(file, module)\n\tif (global) yield* prepareHome(directory)\n\tyield* install(directory, Record.singleton(packageName, \"latest\"))\n\t// Once accepted, the native write must finish before command cleanup can proceed.\n\tyield* fs.writeFileString(file, text).pipe(Effect.uninterruptible)\n\tyield* Effect.logInfo(`Enabled ${module} in ${file}`)\n})\n"],"mappings":";;;;;;;;;;;;;;;;AAuBA,MAAM,UAAU,WACf,OAAO,QACN,GAAG,OAAO,MAAM,IAAI,OAAO,MAAM,OAAO,UAAU,OAAO,QAAQ,OAAO,YAAY,OAAO,QAAQ,OAAO,SAC3G,CAAC,CAAC,KACD,OAAO,QACN,OAAO,MAAM,OAAO,wBAAwB;CAC3C,cAAc,OAAO;CACrB,SAAS,EAAE,OAAO,wBACjB,OAAO,WACN,0FACA,OACA,EAAE,kBAAkB,CACrB;AACF,CAAC,CACF,CACD;;;;;AAMD,MAAM,OAAO,OAAO,MAAM,CACzB,OAAO,sBAAsB;CAC5B,eAAe;CACf;CACA,OAAO;AACR,CAAC,GACD,eAAe,WAChB,CAAC;AACD,MAAM,cAAc,OAAO,aAAa,OAAO,MAAM,IAAI,CAAC;AAE1D,MAAM,UAAU,OAAO,GAAG,gBAAgB,CAAC,CAAC,WAC3C,WACA,SACC;CACD,MAAM,UAAU,OAAO,OAAO,aAAa,eAAe,OAAO,CAAC,CAAC,OAAO;CAE1E,MAAM,YAAY,OAAO,OAAO,OAAO,uBAAuB,CAAC,CAAC,KAC/D,OAAO,YAAY,EAAE,CACtB;CAEA,MAAM,SAAS,QAAO,OADC,eAAe,KAAK;EAAE;EAAW;CAAU,CAAC,EAAA,CACrC,QAAQ,OAAO;CAC7C,OAAO,OAAO,MAAM;AACrB,CAAC;AAED,MAAa,MAAM,OAAO,GAAG,YAAY,CAAC,CAAC,WAC1C,OACC;CACD,MAAM,UAAU,OAAO,YAAY,KAAK;CACxC,OAAO,QACN,KACA,OAAO,YACN,MAAM,IAAI,UAAU,SACnB,UAAU,SAAS,IAAI,IACnB,CAAC,MAAM,QAAQ,IACf,CAAC,KAAK,IAAI,KAAK,EAAE,CACtB,CACD,CACD;AACD,CAAC;AAED,MAAa,UAAU,UACtB,QACC,KACA,OAAO,YAAY,MAAM,IAAI,QAAQ,SAAS,CAAC,MAAM,IAAI,CAAU,CAAC,CACrE;AAED,MAAM,gBAAgB,OAAO,WAAW,WACvC,MACA,QACC;CACD,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,QAAQ,OAAO,GAAG,OAAO,IAAI,KAChC,OAAO,GAAG,eAAe,IAAI,IAC7B;CACH,MAAM,WAAW,OAAO,OAAO,IAAI;EAClC,WAAW;GACV,MAAM,WAAqB,cAAc,IAAI;GAC7C,MAAM,QAAQ,SAAS,OAAO;GAC9B,IAAI,UAAU,KAAA,GAAW,MAAM;GAC/B,IAAI,SAAS,KAAK,MAAM,MAAM,SAAS,WAAW,SAAS,WAAW,CAAC,CAAC;GACxE,OAAO;EACR;EACA,QAAQ,UAAU,mBAAmB,KAAK;GAAE;GAAM;EAAM,CAAC;CAC1D,CAAC;CACD,MAAM,UAAU,OAAO,OAAO,oBAC7B,OAAO,MAAM,OAAO,OAAO,CAC5B,CAAC,CAAC,SAAS,KAAK,CAAC;CACjB,MAAM,WAAW,MAAM,eACtB,UACC,UACA,UAAU,YAAY,OAAO,MAAM,KACnC,MAAM,SAAS,UACf,UAAU,YAAY,OAAO,KAAK,KAClC,MAAM,QAAQ,IAChB;CACA,OAAO,OAAO,OAAO,IAAI;EACxB,WAAW;GACV,IAAI,OAAO,OAAO,QAAQ,GACzB,SAAS,MAAM,CAAC,SAAS,OAAO,UAAU,GAAG,KAAK;QAC9C,SAAS,IAAI;IAAE,MAAM;IAAQ,KAAK;GAAK,CAAC;GAC7C,OAAO,SAAS,SAAS;EAC1B;EACA,QAAQ,UAAU,mBAAmB,KAAK;GAAE;GAAM;EAAM,CAAC;CAC1D,CAAC;AACF,CAAC;AACD,MAAM,cAAc,OAAO,WAAW,WAAW,WAAmB;CACnE,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CACzB,OAAO,GAAG,cAAc,WAAW,EAAE,WAAW,KAAK,CAAC;CACtD,MAAM,WAAW,KAAK,KAAK,WAAW,cAAc;CACpD,IAAI,EAAE,OAAO,GAAG,OAAO,QAAQ,IAC9B,OAAO,GAAG,gBAAgB,UAAU,0CAAoC;AAC1E,GAAG,OAAO,eAAe;;AAGzB,MAAa,YAAY,OAAO,GAAG,kBAAkB,CAAC,CAAC,WACtD,MACA,SAAkB,OACjB;CACD,MAAM,OAAO,OAAO,KAAK;CACzB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,YAAY,SAAS,OAAO,OAAO,KAAK,QAAQ,GAAG;CACzD,MAAM,cAAc,OAAO,cAAc,IAAI;CAC7C,MAAM,SAAS,GAAG,YAAY;CAC9B,MAAM,OAAO,KAAK,KAAK,WAAW,WAAW;CAC7C,MAAM,OAAO,OAAO,cAAc,MAAM,MAAM;CAC9C,IAAI,QAAQ,OAAO,YAAY,SAAS;CACxC,OAAO,QAAQ,WAAW,OAAO,UAAU,aAAa,QAAQ,CAAC;CAEjE,OAAO,GAAG,gBAAgB,MAAM,IAAI,CAAC,CAAC,KAAK,OAAO,eAAe;CACjE,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAM,MAAM;AACrD,CAAC"}
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":"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"}
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
@@ -6,6 +6,7 @@ import { Array, Effect, Layer, Logger, Option, Stdio } from "effect";
6
6
  import { NodeRuntime } from "@effect/platform-node";
7
7
  import { Graft } from "@graftland/graft";
8
8
  import { Argument, CliError, Command, Flag } from "effect/cli";
9
+ import { command } from "@graftland/create/Command";
9
10
  // src/bin.ts
10
11
  /**
11
12
  * The `graft` executable. Running this module starts the program.
@@ -13,16 +14,13 @@ import { Argument, CliError, Command, Flag } from "effect/cli";
13
14
  const start = Command.make("start", {
14
15
  config: Argument.String("config").pipe(Argument.withDescription("Path of the config file"), Argument.withDefault("graft.yml")),
15
16
  watch: Flag.Boolean("watch").pipe(Flag.withAlias("w"), Flag.withDescription("Reload plugins and config when files change"), Flag.withDefault(false)),
16
- 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)
17
19
  }, (options) => Effect.promise(() => import("./Start.js")).pipe(Effect.uninterruptible, Effect.flatMap((Start) => Start.run({
18
20
  ...options,
19
- envFile: Option.getOrUndefined(options.envFile)
21
+ envFile: Option.getOrUndefined(options.envFile),
22
+ envDir: Option.getOrUndefined(options.envDir)
20
23
  })))).pipe(Command.withDescription("Run the application described by a config file"));
21
- const create = Command.make("create", {
22
- directory: Argument.String("directory").pipe(Argument.withDescription("Where to create the project")),
23
- name: Flag.String("name").pipe(Flag.withDescription("Package name; defaults to the directory name"), Flag.optional),
24
- install: Flag.Boolean("install").pipe(Flag.withDescription("Install dependencies afterwards"), Flag.withDefault(false))
25
- }, (options) => Effect.promise(() => import("./Manage.js")).pipe(Effect.uninterruptible, Effect.flatMap((Manage) => Manage.create(options)))).pipe(Command.withDescription("Create a new Graft project"));
26
24
  const add = Command.make("add", {
27
25
  packages: Argument.variadic(Argument.String("package")),
28
26
  plugin: Flag.String("plugin").pipe(Flag.optional),
@@ -46,7 +44,7 @@ const program = Effect.gen(function* () {
46
44
  const context = yield* Layer.build(Graft.layer);
47
45
  return yield* run([
48
46
  start,
49
- create,
47
+ command,
50
48
  add,
51
49
  remove
52
50
  ], graft, version, args).pipe(Effect.provideContext(context));
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","names":["Host.output","Host.run"],"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 { 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 create = Command.make(\n\t\"create\",\n\t{\n\t\tdirectory: Argument.String(\"directory\").pipe(\n\t\t\tArgument.withDescription(\"Where to create the project\"),\n\t\t),\n\t\tname: Flag.String(\"name\").pipe(\n\t\t\tFlag.withDescription(\"Package name; defaults to the directory name\"),\n\t\t\tFlag.optional,\n\t\t),\n\t\tinstall: Flag.Boolean(\"install\").pipe(\n\t\t\tFlag.withDescription(\"Install dependencies afterwards\"),\n\t\t\tFlag.withDefault(false),\n\t\t),\n\t},\n\t(options) =>\n\t\tEffect.promise(() => import(\"./Manage\")).pipe(\n\t\t\tEffect.uninterruptible,\n\t\t\tEffect.flatMap((Manage) => Manage.create(options)),\n\t\t),\n).pipe(Command.withDescription(\"Create a new Graft project\"))\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.0",
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.0",
41
- "@graftland/loader": "0.4.0",
42
- "@graftland/hmr": "0.4.0",
43
- "@graftland/create": "0.4.0",
44
- "@graftland/package-manager": "0.4.0",
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/Manage.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Creating projects and changing their dependencies.
2
+ * Changing a project's dependencies.
3
3
  */
4
4
  import {
5
5
  Array,
@@ -11,7 +11,6 @@ import {
11
11
  Predicate,
12
12
  Record,
13
13
  Schema,
14
- String,
15
14
  } from "effect"
16
15
 
17
16
  import * as Create from "@graftland/create"
@@ -67,22 +66,6 @@ const install = Effect.fn("Manage.install")(function* (
67
66
  yield* report(result)
68
67
  })
69
68
 
70
- export const create = Effect.fn("Manage.create")(function* (options: {
71
- readonly directory: string
72
- readonly name: Option.Option<string>
73
- readonly install: boolean
74
- }) {
75
- const project = yield* Create.create({
76
- directory: options.directory,
77
- ...Option.match(options.name, {
78
- onNone: () => ({}),
79
- onSome: (name) => ({ name }),
80
- }),
81
- })
82
- yield* Effect.logInfo(`Created ${project.name} in ${project.directory}`)
83
- if (options.install) yield* install(project.directory, {})
84
- })
85
-
86
69
  export const add = Effect.fn("Manage.add")(function* (
87
70
  specs: ReadonlyArray<string>,
88
71
  ) {
@@ -161,10 +144,7 @@ export const addPlugin = Effect.fn("Manage.addPlugin")(function* (
161
144
  const path = yield* Path.Path
162
145
  const fs = yield* FileSystem.FileSystem
163
146
  const directory = global ? yield* home : path.resolve(".")
164
- const packageName =
165
- String.startsWith("@")(name) || String.startsWith("graft-plugin-")(name)
166
- ? name
167
- : `@graftland/plugin-${name}`
147
+ const packageName = Create.pluginPackage(name)
168
148
  const module = `${packageName}/cli`
169
149
  const file = path.join(directory, "graft.yml")
170
150
  const text = yield* enabledConfig(file, module)
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
@@ -6,6 +6,7 @@ import { Array, Effect, Layer, Logger, Option, Stdio } from "effect"
6
6
  import { Argument, CliError, Command, Flag } from "effect/cli"
7
7
  import { NodeRuntime } from "@effect/platform-node"
8
8
 
9
+ import { command as create } from "@graftland/create/Command"
9
10
  import { Graft } from "@graftland/graft"
10
11
 
11
12
  import { version } from "../package.json" with { type: "json" }
@@ -30,6 +31,12 @@ const start = Command.make(
30
31
  ),
31
32
  Flag.optional,
32
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
+ ),
33
40
  },
34
41
  (options) =>
35
42
  Effect.promise(() => import("./Start")).pipe(
@@ -38,6 +45,7 @@ const start = Command.make(
38
45
  Start.run({
39
46
  ...options,
40
47
  envFile: Option.getOrUndefined(options.envFile),
48
+ envDir: Option.getOrUndefined(options.envDir),
41
49
  }),
42
50
  ),
43
51
  ),
@@ -45,28 +53,6 @@ const start = Command.make(
45
53
  Command.withDescription("Run the application described by a config file"),
46
54
  )
47
55
 
48
- const create = Command.make(
49
- "create",
50
- {
51
- directory: Argument.String("directory").pipe(
52
- Argument.withDescription("Where to create the project"),
53
- ),
54
- name: Flag.String("name").pipe(
55
- Flag.withDescription("Package name; defaults to the directory name"),
56
- Flag.optional,
57
- ),
58
- install: Flag.Boolean("install").pipe(
59
- Flag.withDescription("Install dependencies afterwards"),
60
- Flag.withDefault(false),
61
- ),
62
- },
63
- (options) =>
64
- Effect.promise(() => import("./Manage")).pipe(
65
- Effect.uninterruptible,
66
- Effect.flatMap((Manage) => Manage.create(options)),
67
- ),
68
- ).pipe(Command.withDescription("Create a new Graft project"))
69
-
70
56
  const add = Command.make(
71
57
  "add",
72
58
  {
@@ -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) =>