mdcontext 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/README.md +28 -0
- package/.changeset/config.json +11 -0
- package/.claude/settings.local.json +25 -0
- package/.github/workflows/ci.yml +83 -0
- package/.github/workflows/claude-code-review.yml +44 -0
- package/.github/workflows/claude.yml +85 -0
- package/.github/workflows/release.yml +113 -0
- package/.tldrignore +112 -0
- package/BACKLOG.md +338 -0
- package/CONTRIBUTING.md +186 -0
- package/NOTES/NOTES +44 -0
- package/README.md +434 -11
- package/biome.json +36 -0
- package/cspell.config.yaml +14 -0
- package/dist/chunk-23UPXDNL.js +3044 -0
- package/dist/chunk-2W7MO2DL.js +1366 -0
- package/dist/chunk-3NUAZGMA.js +1689 -0
- package/dist/chunk-7TOWB2XB.js +366 -0
- package/dist/chunk-7XOTOADQ.js +3065 -0
- package/dist/chunk-AH2PDM2K.js +3042 -0
- package/dist/chunk-BNXWSZ63.js +3742 -0
- package/dist/chunk-BTL5DJVU.js +3222 -0
- package/dist/chunk-HDHYG7E4.js +104 -0
- package/dist/chunk-HLR4KZBP.js +3234 -0
- package/dist/chunk-IP3FRFEB.js +1045 -0
- package/dist/chunk-KHU56VDO.js +3042 -0
- package/dist/chunk-KRYIFLQR.js +88 -0
- package/dist/chunk-LBSDNLEM.js +287 -0
- package/dist/chunk-MNTQ7HCP.js +2643 -0
- package/dist/chunk-MUJELQQ6.js +1387 -0
- package/dist/chunk-MXJGMSLV.js +2199 -0
- package/dist/chunk-N6QJGC3Z.js +2636 -0
- package/dist/chunk-OBELGBPM.js +1713 -0
- package/dist/chunk-OT7R5XTA.js +3192 -0
- package/dist/chunk-P7X4RA2T.js +106 -0
- package/dist/chunk-PIDUQNC2.js +3185 -0
- package/dist/chunk-POGCDIH4.js +3187 -0
- package/dist/chunk-PSIEOQGZ.js +3043 -0
- package/dist/chunk-PVRT3IHA.js +3238 -0
- package/dist/chunk-QNN4TT23.js +1430 -0
- package/dist/chunk-RE3R45RJ.js +3042 -0
- package/dist/chunk-S7E6TFX6.js +803 -0
- package/dist/chunk-SG6GLU4U.js +1378 -0
- package/dist/chunk-SJCDV2ST.js +274 -0
- package/dist/chunk-SYE5XLF3.js +104 -0
- package/dist/chunk-T5VLYBZD.js +103 -0
- package/dist/chunk-TOQB7VWU.js +3238 -0
- package/dist/chunk-VFNMZ4ZQ.js +3228 -0
- package/dist/chunk-VVTGZNBT.js +1629 -0
- package/dist/chunk-W7Q4RFEV.js +104 -0
- package/dist/chunk-XTYYVRLO.js +3190 -0
- package/dist/chunk-Y6MDYVJD.js +3063 -0
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +5458 -0
- package/dist/index.d.ts +653 -0
- package/dist/index.js +79 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +472 -0
- package/dist/schema-BAWSG7KY.js +22 -0
- package/dist/schema-E3QUPL26.js +20 -0
- package/dist/schema-EHL7WUT6.js +20 -0
- package/docs/019-USAGE.md +625 -0
- package/docs/020-current-implementation.md +364 -0
- package/docs/021-DOGFOODING-FINDINGS.md +175 -0
- package/docs/BACKLOG.md +80 -0
- package/docs/CONFIG.md +1123 -0
- package/docs/DESIGN.md +439 -0
- package/docs/ERRORS.md +383 -0
- package/docs/PROJECT.md +88 -0
- package/docs/ROADMAP.md +407 -0
- package/docs/summarization.md +320 -0
- package/docs/test-links.md +9 -0
- package/justfile +40 -0
- package/package.json +74 -9
- package/pnpm-workspace.yaml +5 -0
- package/research/INDEX.md +315 -0
- package/research/code-review/README.md +90 -0
- package/research/code-review/cli-error-handling-review.md +979 -0
- package/research/code-review/code-review-validation-report.md +464 -0
- package/research/code-review/main-ts-review.md +1128 -0
- package/research/config-analysis/01-current-implementation.md +470 -0
- package/research/config-analysis/02-strategy-recommendation.md +428 -0
- package/research/config-analysis/03-task-candidates.md +715 -0
- package/research/config-analysis/033-research-configuration-management.md +828 -0
- package/research/config-analysis/034-research-effect-cli-config.md +1504 -0
- package/research/config-analysis/04-consolidated-task-candidates.md +277 -0
- package/research/config-docs/SUMMARY.md +357 -0
- package/research/config-docs/TEST-RESULTS.md +776 -0
- package/research/config-docs/TODO.md +542 -0
- package/research/config-docs/analysis.md +744 -0
- package/research/config-docs/fix-validation.md +502 -0
- package/research/config-docs/help-audit.md +264 -0
- package/research/config-docs/help-system-analysis.md +890 -0
- package/research/dogfood/consolidated-tool-evaluation.md +373 -0
- package/research/dogfood/strategy-a/a-synthesis.md +184 -0
- package/research/dogfood/strategy-a/a1-docs.md +226 -0
- package/research/dogfood/strategy-a/a2-amorphic.md +156 -0
- package/research/dogfood/strategy-a/a3-llm.md +164 -0
- package/research/dogfood/strategy-b/b-synthesis.md +228 -0
- package/research/dogfood/strategy-b/b1-architecture.md +207 -0
- package/research/dogfood/strategy-b/b2-gaps.md +258 -0
- package/research/dogfood/strategy-b/b3-workflows.md +250 -0
- package/research/dogfood/strategy-c/c-synthesis.md +451 -0
- package/research/dogfood/strategy-c/c1-explorer.md +192 -0
- package/research/dogfood/strategy-c/c2-diver-memory.md +145 -0
- package/research/dogfood/strategy-c/c3-diver-control.md +148 -0
- package/research/dogfood/strategy-c/c4-diver-failure.md +151 -0
- package/research/dogfood/strategy-c/c5-diver-execution.md +221 -0
- package/research/dogfood/strategy-c/c6-diver-org.md +221 -0
- package/research/effect-cli-error-handling.md +845 -0
- package/research/effect-errors-as-values.md +943 -0
- package/research/errors-task-analysis/00-consolidated-tasks.md +207 -0
- package/research/errors-task-analysis/cli-commands-analysis.md +909 -0
- package/research/errors-task-analysis/embeddings-analysis.md +709 -0
- package/research/errors-task-analysis/index-search-analysis.md +812 -0
- package/research/frontmatter/COMMENTS-ARE-SKIPPED.md +149 -0
- package/research/frontmatter/LLM-CODE-NAVIGATION.md +276 -0
- package/research/issue-review.md +603 -0
- package/research/llm-summarization/agent-cli-tools-2026.md +1082 -0
- package/research/llm-summarization/alternative-providers-2026.md +1428 -0
- package/research/llm-summarization/anthropic-2026.md +367 -0
- package/research/llm-summarization/claude-cli-integration.md +1706 -0
- package/research/llm-summarization/cli-integration-patterns.md +3155 -0
- package/research/llm-summarization/openai-2026.md +473 -0
- package/research/llm-summarization/openai-compatible-providers-2026.md +1022 -0
- package/research/llm-summarization/opencode-cli-integration.md +1552 -0
- package/research/llm-summarization/prompt-engineering-2026.md +1426 -0
- package/research/llm-summarization/prototype-results.md +56 -0
- package/research/llm-summarization/provider-switching-patterns-2026.md +2153 -0
- package/research/llm-summarization/typescript-llm-libraries-2026.md +2436 -0
- package/research/mdcontext-error-analysis.md +521 -0
- package/research/mdcontext-pudding/00-EXECUTIVE-SUMMARY.md +282 -0
- package/research/mdcontext-pudding/01-index-embed.md +956 -0
- package/research/mdcontext-pudding/02-search-COMMANDS.md +142 -0
- package/research/mdcontext-pudding/02-search-SUMMARY.md +146 -0
- package/research/mdcontext-pudding/02-search.md +970 -0
- package/research/mdcontext-pudding/03-context.md +779 -0
- package/research/mdcontext-pudding/04-navigation-and-analytics.md +803 -0
- package/research/mdcontext-pudding/04-tree.md +704 -0
- package/research/mdcontext-pudding/05-config.md +1038 -0
- package/research/mdcontext-pudding/06-links-summary.txt +87 -0
- package/research/mdcontext-pudding/06-links.md +679 -0
- package/research/mdcontext-pudding/07-stats.md +693 -0
- package/research/mdcontext-pudding/BUG-FIX-PLAN.md +388 -0
- package/research/mdcontext-pudding/P0-BUG-VALIDATION.md +167 -0
- package/research/mdcontext-pudding/README.md +168 -0
- package/research/mdcontext-pudding/TESTING-SUMMARY.md +128 -0
- package/research/npm_publish/011-npm-workflow-research-agent2.md +792 -0
- package/research/npm_publish/012-npm-workflow-research-agent1.md +530 -0
- package/research/npm_publish/013-npm-workflow-research-agent3.md +722 -0
- package/research/npm_publish/014-npm-workflow-synthesis.md +556 -0
- package/research/npm_publish/031-npm-workflow-task-analysis.md +134 -0
- package/research/research-quality-review.md +834 -0
- package/research/semantic-search/002-research-embedding-models.md +490 -0
- package/research/semantic-search/003-research-rag-alternatives.md +523 -0
- package/research/semantic-search/004-research-vector-search.md +841 -0
- package/research/semantic-search/032-research-semantic-search.md +427 -0
- package/research/semantic-search/embedding-text-analysis.md +156 -0
- package/research/semantic-search/multi-word-failure-reproduction.md +171 -0
- package/research/semantic-search/query-processing-analysis.md +207 -0
- package/research/semantic-search/root-cause-and-solution.md +114 -0
- package/research/semantic-search/threshold-validation-report.md +69 -0
- package/research/semantic-search/vector-search-analysis.md +63 -0
- package/research/task-management-2026/00-synthesis-recommendations.md +295 -0
- package/research/task-management-2026/01-ai-workflow-tools.md +416 -0
- package/research/task-management-2026/02-agent-framework-patterns.md +476 -0
- package/research/task-management-2026/03-lightweight-file-based.md +567 -0
- package/research/task-management-2026/04-established-tools-ai-features.md +541 -0
- package/research/task-management-2026/linear/01-core-features-workflow.md +771 -0
- package/research/task-management-2026/linear/02-api-integrations.md +930 -0
- package/research/task-management-2026/linear/03-ai-features.md +368 -0
- package/research/task-management-2026/linear/04-pricing-setup.md +205 -0
- package/research/task-management-2026/linear/05-usage-patterns-best-practices.md +605 -0
- package/research/test-path-issues.md +276 -0
- package/review/ALP-76/1-error-type-design.md +962 -0
- package/review/ALP-76/2-error-handling-patterns.md +906 -0
- package/review/ALP-76/3-error-presentation.md +624 -0
- package/review/ALP-76/4-test-coverage.md +625 -0
- package/review/ALP-76/5-migration-completeness.md +440 -0
- package/review/ALP-76/6-effect-best-practices.md +755 -0
- package/scripts/apply-branch-protection.sh +47 -0
- package/scripts/branch-protection-templates.json +79 -0
- package/scripts/prototype-summarization.ts +346 -0
- package/scripts/rebuild-hnswlib.js +58 -0
- package/scripts/setup-branch-protection.sh +64 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/active-provider.json +7 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.json +541 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/bm25.meta.json +5 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/config.json +8 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/documents.json +60 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/links.json +13 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/.mdcontext/indexes/sections.json +1197 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/configuration-management.md +99 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/distributed-systems.md +92 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/error-handling.md +78 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/failure-automation.md +55 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/job-context.md +69 -0
- package/src/__tests__/fixtures/semantic-search/multi-word-corpus/process-orchestration.md +99 -0
- package/src/cli/argv-preprocessor.test.ts +210 -0
- package/src/cli/argv-preprocessor.ts +202 -0
- package/src/cli/cli.test.ts +627 -0
- package/src/cli/commands/backlinks.ts +54 -0
- package/src/cli/commands/config-cmd.ts +642 -0
- package/src/cli/commands/context.ts +285 -0
- package/src/cli/commands/duplicates.ts +122 -0
- package/src/cli/commands/embeddings.ts +529 -0
- package/src/cli/commands/index-cmd.ts +480 -0
- package/src/cli/commands/index.ts +16 -0
- package/src/cli/commands/links.ts +52 -0
- package/src/cli/commands/search.ts +1281 -0
- package/src/cli/commands/stats.ts +149 -0
- package/src/cli/commands/tree.ts +128 -0
- package/src/cli/config-layer.ts +176 -0
- package/src/cli/error-handler.test.ts +235 -0
- package/src/cli/error-handler.ts +655 -0
- package/src/cli/flag-schemas.ts +341 -0
- package/src/cli/help.ts +588 -0
- package/src/cli/index.ts +9 -0
- package/src/cli/main.ts +435 -0
- package/src/cli/options.ts +41 -0
- package/src/cli/shared-error-handling.ts +199 -0
- package/src/cli/typo-suggester.test.ts +105 -0
- package/src/cli/typo-suggester.ts +130 -0
- package/src/cli/utils.ts +259 -0
- package/src/config/file-provider.test.ts +320 -0
- package/src/config/file-provider.ts +273 -0
- package/src/config/index.ts +72 -0
- package/src/config/integration.test.ts +667 -0
- package/src/config/precedence.test.ts +277 -0
- package/src/config/precedence.ts +451 -0
- package/src/config/schema.test.ts +414 -0
- package/src/config/schema.ts +603 -0
- package/src/config/service.test.ts +320 -0
- package/src/config/service.ts +243 -0
- package/src/config/testing.test.ts +264 -0
- package/src/config/testing.ts +110 -0
- package/src/core/index.ts +1 -0
- package/src/core/types.ts +113 -0
- package/src/duplicates/detector.test.ts +183 -0
- package/src/duplicates/detector.ts +414 -0
- package/src/duplicates/index.ts +18 -0
- package/src/embeddings/embedding-namespace.test.ts +300 -0
- package/src/embeddings/embedding-namespace.ts +947 -0
- package/src/embeddings/heading-boost.test.ts +222 -0
- package/src/embeddings/hnsw-build-options.test.ts +198 -0
- package/src/embeddings/hyde.test.ts +272 -0
- package/src/embeddings/hyde.ts +264 -0
- package/src/embeddings/index.ts +10 -0
- package/src/embeddings/openai-provider.ts +414 -0
- package/src/embeddings/pricing.json +22 -0
- package/src/embeddings/provider-constants.ts +204 -0
- package/src/embeddings/provider-errors.test.ts +967 -0
- package/src/embeddings/provider-errors.ts +565 -0
- package/src/embeddings/provider-factory.test.ts +240 -0
- package/src/embeddings/provider-factory.ts +225 -0
- package/src/embeddings/provider-integration.test.ts +788 -0
- package/src/embeddings/query-preprocessing.test.ts +187 -0
- package/src/embeddings/semantic-search-threshold.test.ts +508 -0
- package/src/embeddings/semantic-search.ts +1270 -0
- package/src/embeddings/types.ts +359 -0
- package/src/embeddings/vector-store.ts +708 -0
- package/src/embeddings/voyage-provider.ts +313 -0
- package/src/errors/errors.test.ts +845 -0
- package/src/errors/index.ts +533 -0
- package/src/index/ignore-patterns.test.ts +354 -0
- package/src/index/ignore-patterns.ts +305 -0
- package/src/index/index.ts +4 -0
- package/src/index/indexer.ts +684 -0
- package/src/index/storage.ts +260 -0
- package/src/index/types.ts +147 -0
- package/src/index/watcher.ts +189 -0
- package/src/index.ts +30 -0
- package/src/integration/search-keyword.test.ts +678 -0
- package/src/mcp/server.ts +612 -0
- package/src/parser/index.ts +1 -0
- package/src/parser/parser.test.ts +291 -0
- package/src/parser/parser.ts +394 -0
- package/src/parser/section-filter.test.ts +277 -0
- package/src/parser/section-filter.ts +392 -0
- package/src/search/__tests__/hybrid-search.test.ts +650 -0
- package/src/search/bm25-store.ts +366 -0
- package/src/search/cross-encoder.test.ts +253 -0
- package/src/search/cross-encoder.ts +406 -0
- package/src/search/fuzzy-search.test.ts +419 -0
- package/src/search/fuzzy-search.ts +273 -0
- package/src/search/hybrid-search.ts +448 -0
- package/src/search/path-matcher.test.ts +276 -0
- package/src/search/path-matcher.ts +33 -0
- package/src/search/query-parser.test.ts +260 -0
- package/src/search/query-parser.ts +319 -0
- package/src/search/searcher.test.ts +280 -0
- package/src/search/searcher.ts +724 -0
- package/src/search/wink-bm25.d.ts +30 -0
- package/src/summarization/cli-providers/claude.ts +202 -0
- package/src/summarization/cli-providers/detection.test.ts +273 -0
- package/src/summarization/cli-providers/detection.ts +118 -0
- package/src/summarization/cli-providers/index.ts +8 -0
- package/src/summarization/cost.test.ts +139 -0
- package/src/summarization/cost.ts +102 -0
- package/src/summarization/error-handler.test.ts +127 -0
- package/src/summarization/error-handler.ts +111 -0
- package/src/summarization/index.ts +102 -0
- package/src/summarization/pipeline.test.ts +498 -0
- package/src/summarization/pipeline.ts +231 -0
- package/src/summarization/prompts.test.ts +269 -0
- package/src/summarization/prompts.ts +133 -0
- package/src/summarization/provider-factory.test.ts +396 -0
- package/src/summarization/provider-factory.ts +178 -0
- package/src/summarization/types.ts +184 -0
- package/src/summarize/budget-bugs.test.ts +620 -0
- package/src/summarize/formatters.ts +419 -0
- package/src/summarize/index.ts +20 -0
- package/src/summarize/summarizer.test.ts +275 -0
- package/src/summarize/summarizer.ts +597 -0
- package/src/summarize/verify-bugs.test.ts +238 -0
- package/src/types/huggingface-transformers.d.ts +66 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/tokens.test.ts +142 -0
- package/src/utils/tokens.ts +186 -0
- package/tests/fixtures/cli/.mdcontext/active-provider.json +7 -0
- package/tests/fixtures/cli/.mdcontext/config.json +8 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/embeddings/openai_text-embedding-3-small_512/vectors.meta.bin +0 -0
- package/tests/fixtures/cli/.mdcontext/indexes/documents.json +33 -0
- package/tests/fixtures/cli/.mdcontext/indexes/links.json +12 -0
- package/tests/fixtures/cli/.mdcontext/indexes/sections.json +247 -0
- package/tests/fixtures/cli/README.md +9 -0
- package/tests/fixtures/cli/api-reference.md +11 -0
- package/tests/fixtures/cli/getting-started.md +11 -0
- package/tests/integration/embed-index.test.ts +712 -0
- package/tests/integration/search-context.test.ts +469 -0
- package/tests/integration/search-semantic.test.ts +522 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +16 -0
- package/vitest.setup.ts +12 -0
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
# Effect for CLI Development and Configuration Management
|
|
2
|
+
|
|
3
|
+
## Executive Summary
|
|
4
|
+
|
|
5
|
+
This research evaluates Effect's ecosystem for building CLI applications and managing configuration. Effect provides a comprehensive, type-safe foundation through `@effect/cli` for command-line interfaces and its built-in `Config` module for configuration management.
|
|
6
|
+
|
|
7
|
+
**Key Findings**:
|
|
8
|
+
|
|
9
|
+
- `@effect/cli` offers a declarative, composable approach to CLI development with automatic help generation, wizard mode, and shell completions
|
|
10
|
+
- Effect's `Config` module provides type-safe configuration loading with `ConfigProvider` abstraction for multiple sources
|
|
11
|
+
- The `Layer` system enables elegant dependency injection and configuration composition
|
|
12
|
+
- Effect's `Redacted` type provides first-class secrets handling
|
|
13
|
+
- Real-world adoption includes Effect's own tooling, VS Code extension, and Discord bot
|
|
14
|
+
|
|
15
|
+
**Recommendation**: Effect is a strong candidate for mdcontext's CLI and config layer, offering superior type safety and composability compared to traditional approaches. However, it requires commitment to the Effect paradigm throughout the application.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. @effect/cli - Command-Line Interface Framework
|
|
20
|
+
|
|
21
|
+
### Overview
|
|
22
|
+
|
|
23
|
+
`@effect/cli` is a declarative framework for building type-safe CLI applications. It provides:
|
|
24
|
+
|
|
25
|
+
- Hierarchical command structures (commands and subcommands)
|
|
26
|
+
- Type-safe argument and option parsing
|
|
27
|
+
- Automatic help documentation generation
|
|
28
|
+
- Interactive wizard mode
|
|
29
|
+
- Shell completion generation (bash, zsh, fish, sh)
|
|
30
|
+
- Built-in logging level control
|
|
31
|
+
|
|
32
|
+
**Installation**:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @effect/cli @effect/platform @effect/platform-node effect
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Core Concepts
|
|
39
|
+
|
|
40
|
+
#### Commands
|
|
41
|
+
|
|
42
|
+
Commands are the fundamental building blocks. Each command has:
|
|
43
|
+
|
|
44
|
+
- **Name**: Identifier for invocation
|
|
45
|
+
- **Configuration**: Options and arguments
|
|
46
|
+
- **Handler**: Effect-returning function
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
50
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
51
|
+
import { Console, Effect } from "effect";
|
|
52
|
+
|
|
53
|
+
// Define a simple command
|
|
54
|
+
const greet = Command.make(
|
|
55
|
+
"greet", // Command name
|
|
56
|
+
{ name: Args.text({ name: "name" }) }, // Arguments
|
|
57
|
+
({ name }) => Console.log(`Hello, ${name}!`), // Handler
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Run the CLI
|
|
61
|
+
const cli = Command.run(greet, {
|
|
62
|
+
name: "My CLI",
|
|
63
|
+
version: "1.0.0",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Arguments (Args)
|
|
70
|
+
|
|
71
|
+
Arguments are positional values passed to commands.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { Args } from "@effect/cli";
|
|
75
|
+
|
|
76
|
+
// Basic types
|
|
77
|
+
const textArg = Args.text({ name: "message" });
|
|
78
|
+
const numberArg = Args.integer({ name: "count" });
|
|
79
|
+
const floatArg = Args.float({ name: "value" });
|
|
80
|
+
const boolArg = Args.boolean({ name: "flag" });
|
|
81
|
+
const dateArg = Args.date({ name: "when" });
|
|
82
|
+
|
|
83
|
+
// File system
|
|
84
|
+
const fileArg = Args.file({ name: "input" });
|
|
85
|
+
const dirArg = Args.directory({ name: "output" });
|
|
86
|
+
const pathArg = Args.path({ name: "location" });
|
|
87
|
+
|
|
88
|
+
// File content (reads and returns content)
|
|
89
|
+
const fileContent = Args.fileContent({ name: "config" });
|
|
90
|
+
const fileText = Args.fileText({ name: "readme" });
|
|
91
|
+
|
|
92
|
+
// Modifiers
|
|
93
|
+
const optionalArg = Args.text({ name: "desc" }).pipe(Args.optional);
|
|
94
|
+
const repeatedArg = Args.text({ name: "files" }).pipe(Args.repeated);
|
|
95
|
+
const withDefault = Args.integer({ name: "port" }).pipe(Args.withDefault(3000));
|
|
96
|
+
const validated = Args.text({ name: "email" }).pipe(
|
|
97
|
+
Args.validate((v) => v.includes("@"), "Must be a valid email"),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Choice from predefined values
|
|
101
|
+
const format = Args.choice("format", ["json", "yaml", "toml"]);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Options
|
|
105
|
+
|
|
106
|
+
Options are named flags with optional values.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { Options } from "@effect/cli";
|
|
110
|
+
|
|
111
|
+
// Boolean flags
|
|
112
|
+
const verbose = Options.boolean("verbose").pipe(Options.withAlias("v"));
|
|
113
|
+
const debug = Options.boolean("debug").pipe(
|
|
114
|
+
Options.withDefault(false),
|
|
115
|
+
Options.withDescription("Enable debug mode"),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Valued options
|
|
119
|
+
const port = Options.integer("port").pipe(
|
|
120
|
+
Options.withAlias("p"),
|
|
121
|
+
Options.withDefault(8080),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const host = Options.text("host").pipe(Options.withDefault("localhost"));
|
|
125
|
+
|
|
126
|
+
// Choice options
|
|
127
|
+
const logLevel = Options.choice("log-level", [
|
|
128
|
+
"debug",
|
|
129
|
+
"info",
|
|
130
|
+
"warn",
|
|
131
|
+
"error",
|
|
132
|
+
]).pipe(Options.withDefault("info"));
|
|
133
|
+
|
|
134
|
+
// Optional values
|
|
135
|
+
const config = Options.file("config").pipe(
|
|
136
|
+
Options.withAlias("c"),
|
|
137
|
+
Options.optional,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Repeated options (--include a --include b)
|
|
141
|
+
const includes = Options.text("include").pipe(
|
|
142
|
+
Options.withAlias("i"),
|
|
143
|
+
Options.repeated,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Sensitive data (not shown in help/logs)
|
|
147
|
+
const apiKey = Options.redacted("api-key");
|
|
148
|
+
const secret = Options.secret("token"); // Deprecated, use redacted
|
|
149
|
+
|
|
150
|
+
// Key-value maps (--header "Content-Type: application/json")
|
|
151
|
+
const headers = Options.keyValueMap("header");
|
|
152
|
+
|
|
153
|
+
// Schema validation
|
|
154
|
+
import { Schema } from "effect";
|
|
155
|
+
const email = Options.text("email").pipe(
|
|
156
|
+
Options.withSchema(Schema.String.pipe(Schema.includes("@"))),
|
|
157
|
+
);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Subcommands
|
|
161
|
+
|
|
162
|
+
Commands can be nested to create hierarchical CLI structures.
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
166
|
+
import { Console, Effect } from "effect";
|
|
167
|
+
|
|
168
|
+
// Define subcommands
|
|
169
|
+
const add = Command.make(
|
|
170
|
+
"add",
|
|
171
|
+
{
|
|
172
|
+
files: Args.text({ name: "pathspec" }).pipe(Args.repeated),
|
|
173
|
+
verbose: Options.boolean("verbose").pipe(Options.withAlias("v")),
|
|
174
|
+
},
|
|
175
|
+
({ files, verbose }) =>
|
|
176
|
+
Effect.gen(function* () {
|
|
177
|
+
if (verbose) yield* Console.log(`Adding ${files.length} files`);
|
|
178
|
+
yield* Console.log(`git add ${files.join(" ")}`);
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const commit = Command.make(
|
|
183
|
+
"commit",
|
|
184
|
+
{
|
|
185
|
+
message: Options.text("message").pipe(Options.withAlias("m")),
|
|
186
|
+
amend: Options.boolean("amend").pipe(Options.withDefault(false)),
|
|
187
|
+
},
|
|
188
|
+
({ message, amend }) =>
|
|
189
|
+
Console.log(`git commit ${amend ? "--amend" : ""} -m "${message}"`),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const clone = Command.make(
|
|
193
|
+
"clone",
|
|
194
|
+
{
|
|
195
|
+
url: Args.text({ name: "repository" }),
|
|
196
|
+
directory: Args.text({ name: "directory" }).pipe(Args.optional),
|
|
197
|
+
},
|
|
198
|
+
({ url, directory }) =>
|
|
199
|
+
Console.log(`git clone ${url}${directory ? ` ${directory}` : ""}`),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Parent command with subcommands
|
|
203
|
+
const git = Command.make("git", {}).pipe(
|
|
204
|
+
Command.withSubcommands([add, commit, clone]),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Run
|
|
208
|
+
const cli = Command.run(git, {
|
|
209
|
+
name: "minigit",
|
|
210
|
+
version: "1.0.0",
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### Accessing Parent Command Context
|
|
215
|
+
|
|
216
|
+
Subcommands can access their parent's configuration:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const child = Command.make(
|
|
220
|
+
"child",
|
|
221
|
+
{ childArg: Args.text({ name: "value" }) },
|
|
222
|
+
({ childArg }) =>
|
|
223
|
+
// Access parent command's parsed config
|
|
224
|
+
parent.pipe(
|
|
225
|
+
Effect.flatMap((parentConfig) =>
|
|
226
|
+
Console.log(
|
|
227
|
+
`Parent verbose: ${parentConfig.verbose}, Child arg: ${childArg}`,
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const parent = Command.make("parent", {
|
|
234
|
+
verbose: Options.boolean("verbose"),
|
|
235
|
+
}).pipe(Command.withSubcommands([child]));
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Built-in Features
|
|
239
|
+
|
|
240
|
+
Every `@effect/cli` application automatically includes:
|
|
241
|
+
|
|
242
|
+
| Flag | Description |
|
|
243
|
+
| ----------------------- | ------------------------------------------------ |
|
|
244
|
+
| `--version` | Display application version |
|
|
245
|
+
| `-h, --help` | Show help documentation |
|
|
246
|
+
| `--wizard` | Interactive command builder |
|
|
247
|
+
| `--completions [shell]` | Generate shell completions (bash, sh, fish, zsh) |
|
|
248
|
+
| `--log-level` | Set minimum log level for handlers |
|
|
249
|
+
|
|
250
|
+
#### Wizard Mode
|
|
251
|
+
|
|
252
|
+
The `--wizard` flag provides an interactive prompt-based interface for building commands:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
$ mycli --wizard
|
|
256
|
+
Wizard Mode for CLI Application: My CLI (v1.0.0)
|
|
257
|
+
|
|
258
|
+
Instructions
|
|
259
|
+
The wizard mode will assist you with constructing commands for My CLI (v1.0.0).
|
|
260
|
+
Please answer all prompts provided by the wizard.
|
|
261
|
+
|
|
262
|
+
COMMAND: greet
|
|
263
|
+
? Enter name: Alice
|
|
264
|
+
|
|
265
|
+
Wizard Mode Complete!
|
|
266
|
+
You may now execute your command directly with the following options and arguments:
|
|
267
|
+
greet Alice
|
|
268
|
+
|
|
269
|
+
? Would you like to run the command? (y/n)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
#### Shell Completions
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# Generate completions
|
|
276
|
+
mycli --completions bash > ~/.bash_completion.d/mycli
|
|
277
|
+
mycli --completions zsh > ~/.zsh/completions/_mycli
|
|
278
|
+
mycli --completions fish > ~/.config/fish/completions/mycli.fish
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Complete CLI Example
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
285
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
286
|
+
import { Console, Effect, Option } from "effect";
|
|
287
|
+
|
|
288
|
+
// Color codes for ANSI
|
|
289
|
+
const colors = {
|
|
290
|
+
red: "\x1b[31m",
|
|
291
|
+
green: "\x1b[32m",
|
|
292
|
+
blue: "\x1b[34m",
|
|
293
|
+
yellow: "\x1b[33m",
|
|
294
|
+
reset: "\x1b[0m",
|
|
295
|
+
bold: "\x1b[1m",
|
|
296
|
+
} as const;
|
|
297
|
+
|
|
298
|
+
type Color = keyof typeof colors;
|
|
299
|
+
|
|
300
|
+
// Define arguments and options
|
|
301
|
+
const text = Args.text({ name: "text" });
|
|
302
|
+
const bold = Options.boolean("bold").pipe(Options.withAlias("b"));
|
|
303
|
+
const color = Options.choice("color", [
|
|
304
|
+
"red",
|
|
305
|
+
"green",
|
|
306
|
+
"blue",
|
|
307
|
+
"yellow",
|
|
308
|
+
] as const).pipe(Options.withAlias("c"), Options.optional);
|
|
309
|
+
const count = Args.integer({ name: "count" }).pipe(Args.withDefault(1));
|
|
310
|
+
|
|
311
|
+
// Echo command - prints text with optional formatting
|
|
312
|
+
const echo = Command.make(
|
|
313
|
+
"echo",
|
|
314
|
+
{ text, bold, color },
|
|
315
|
+
({ text, bold, color }) => {
|
|
316
|
+
let output = text;
|
|
317
|
+
|
|
318
|
+
// Apply color if specified
|
|
319
|
+
if (Option.isSome(color)) {
|
|
320
|
+
output = `${colors[color.value]}${output}${colors.reset}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Apply bold if specified
|
|
324
|
+
if (bold) {
|
|
325
|
+
output = `${colors.bold}${output}${colors.reset}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return Console.log(output);
|
|
329
|
+
},
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Repeat command - repeats the echo command multiple times
|
|
333
|
+
const repeat = Command.make("repeat", { count }, ({ count }) =>
|
|
334
|
+
echo.pipe(
|
|
335
|
+
Effect.flatMap((config) => Effect.repeatN(echo.handler(config), count - 1)),
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Main command with subcommands
|
|
340
|
+
const main = echo.pipe(Command.withSubcommands([repeat]));
|
|
341
|
+
|
|
342
|
+
// Create and run CLI
|
|
343
|
+
const cli = Command.run(main, {
|
|
344
|
+
name: "Echo CLI",
|
|
345
|
+
version: "1.0.0",
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Usage:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
# Basic echo
|
|
355
|
+
$ echo-cli "Hello, World!"
|
|
356
|
+
Hello, World!
|
|
357
|
+
|
|
358
|
+
# With formatting
|
|
359
|
+
$ echo-cli "Hello" --bold --color green
|
|
360
|
+
Hello # (in bold green)
|
|
361
|
+
|
|
362
|
+
# Repeat subcommand
|
|
363
|
+
$ echo-cli repeat 3 "Hi"
|
|
364
|
+
Hi
|
|
365
|
+
Hi
|
|
366
|
+
Hi
|
|
367
|
+
|
|
368
|
+
# Help
|
|
369
|
+
$ echo-cli --help
|
|
370
|
+
# Shows auto-generated help
|
|
371
|
+
|
|
372
|
+
# Wizard mode
|
|
373
|
+
$ echo-cli --wizard
|
|
374
|
+
# Interactive prompt for building command
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 2. @effect/platform - Platform Abstractions
|
|
380
|
+
|
|
381
|
+
### Overview
|
|
382
|
+
|
|
383
|
+
`@effect/platform` provides platform-independent abstractions for:
|
|
384
|
+
|
|
385
|
+
- **FileSystem** - File and directory operations
|
|
386
|
+
- **Terminal** - stdin/stdout interaction
|
|
387
|
+
- **Command** - Process execution
|
|
388
|
+
- **Path** - Cross-platform path utilities
|
|
389
|
+
- **KeyValueStore** - Persistent storage
|
|
390
|
+
- **PlatformLogger** - File-based logging
|
|
391
|
+
|
|
392
|
+
Platform-specific implementations:
|
|
393
|
+
|
|
394
|
+
- `@effect/platform-node` - Node.js/Deno
|
|
395
|
+
- `@effect/platform-bun` - Bun runtime
|
|
396
|
+
- `@effect/platform-browser` - Browser environments
|
|
397
|
+
|
|
398
|
+
### FileSystem
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { FileSystem } from "@effect/platform";
|
|
402
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
403
|
+
import { Effect } from "effect";
|
|
404
|
+
|
|
405
|
+
const program = Effect.gen(function* () {
|
|
406
|
+
const fs = yield* FileSystem.FileSystem;
|
|
407
|
+
|
|
408
|
+
// Check existence
|
|
409
|
+
const exists = yield* fs.exists("./config.json");
|
|
410
|
+
|
|
411
|
+
// Read file
|
|
412
|
+
const content = yield* fs.readFileString("./config.json", "utf8");
|
|
413
|
+
|
|
414
|
+
// Write file
|
|
415
|
+
yield* fs.writeFileString("./output.txt", "Hello, World!");
|
|
416
|
+
|
|
417
|
+
// Read directory
|
|
418
|
+
const files = yield* fs.readDirectory("./src");
|
|
419
|
+
|
|
420
|
+
// Create directory
|
|
421
|
+
yield* fs.makeDirectory("./dist", { recursive: true });
|
|
422
|
+
|
|
423
|
+
// Copy file
|
|
424
|
+
yield* fs.copyFile("./src/index.ts", "./dist/index.ts");
|
|
425
|
+
|
|
426
|
+
// Remove file/directory
|
|
427
|
+
yield* fs.remove("./temp", { recursive: true });
|
|
428
|
+
|
|
429
|
+
// Watch for changes
|
|
430
|
+
const watcher = yield* fs.watch("./src");
|
|
431
|
+
// watcher is a Stream of file system events
|
|
432
|
+
|
|
433
|
+
// Temporary files (auto-cleaned with Scope)
|
|
434
|
+
const tempFile = yield* fs.makeTempFileScoped();
|
|
435
|
+
yield* fs.writeFileString(tempFile, "temp content");
|
|
436
|
+
// File is deleted when scope closes
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
NodeRuntime.runMain(program.pipe(Effect.provide(NodeContext.layer)));
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Terminal
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
import { Terminal } from "@effect/platform";
|
|
446
|
+
import { NodeRuntime, NodeTerminal } from "@effect/platform-node";
|
|
447
|
+
import { Effect } from "effect";
|
|
448
|
+
|
|
449
|
+
const program = Effect.gen(function* () {
|
|
450
|
+
const terminal = yield* Terminal.Terminal;
|
|
451
|
+
|
|
452
|
+
// Read a line from stdin
|
|
453
|
+
const name = yield* terminal.readLine;
|
|
454
|
+
|
|
455
|
+
// Display output
|
|
456
|
+
yield* terminal.display(`Hello, ${name}!\n`);
|
|
457
|
+
|
|
458
|
+
// Log (with newline)
|
|
459
|
+
yield* Terminal.log("This is a log message");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
NodeRuntime.runMain(program.pipe(Effect.provide(NodeTerminal.layer)));
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Command (Process Execution)
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
import { Command } from "@effect/platform";
|
|
469
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
470
|
+
import { Effect, Stream, String, pipe } from "effect";
|
|
471
|
+
|
|
472
|
+
// Helper to collect stream as string
|
|
473
|
+
const streamToString = <E, R>(
|
|
474
|
+
stream: Stream.Stream<Uint8Array, E, R>,
|
|
475
|
+
): Effect.Effect<string, E, R> =>
|
|
476
|
+
stream.pipe(Stream.decodeText(), Stream.runFold(String.empty, String.concat));
|
|
477
|
+
|
|
478
|
+
const program = Effect.gen(function* () {
|
|
479
|
+
// Simple command execution
|
|
480
|
+
const exitCode = yield* Command.make("echo", "Hello").pipe(Command.exitCode);
|
|
481
|
+
|
|
482
|
+
// Capture stdout as string
|
|
483
|
+
const output = yield* Command.make("ls", "-la").pipe(Command.string);
|
|
484
|
+
console.log(output);
|
|
485
|
+
|
|
486
|
+
// Capture stdout as lines
|
|
487
|
+
const lines = yield* Command.make("cat", "file.txt").pipe(Command.lines);
|
|
488
|
+
|
|
489
|
+
// Stream stdout to process.stdout
|
|
490
|
+
yield* Command.make("cat", "./large-file.txt").pipe(
|
|
491
|
+
Command.stdout("inherit"),
|
|
492
|
+
Command.exitCode,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
// Full process control
|
|
496
|
+
const [exitCode2, stdout, stderr] = yield* pipe(
|
|
497
|
+
Command.make("npm", "install"),
|
|
498
|
+
Command.start,
|
|
499
|
+
Effect.flatMap((process) =>
|
|
500
|
+
Effect.all(
|
|
501
|
+
[
|
|
502
|
+
process.exitCode,
|
|
503
|
+
streamToString(process.stdout),
|
|
504
|
+
streamToString(process.stderr),
|
|
505
|
+
],
|
|
506
|
+
{ concurrency: 3 },
|
|
507
|
+
),
|
|
508
|
+
),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
// Working directory and environment
|
|
512
|
+
yield* Command.make("npm", "test").pipe(
|
|
513
|
+
Command.workingDirectory("./packages/core"),
|
|
514
|
+
Command.env({ NODE_ENV: "test" }),
|
|
515
|
+
Command.exitCode,
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
// Pipe commands together
|
|
519
|
+
yield* Command.make("cat", "file.txt").pipe(
|
|
520
|
+
Command.pipeTo(Command.make("grep", "pattern")),
|
|
521
|
+
Command.string,
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
NodeRuntime.runMain(
|
|
526
|
+
Effect.scoped(program).pipe(Effect.provide(NodeContext.layer)),
|
|
527
|
+
);
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## 3. Effect Config Module
|
|
533
|
+
|
|
534
|
+
### Overview
|
|
535
|
+
|
|
536
|
+
Effect provides a built-in configuration system with:
|
|
537
|
+
|
|
538
|
+
- Type-safe config primitives
|
|
539
|
+
- `ConfigProvider` abstraction for loading from various sources
|
|
540
|
+
- Composition and transformation combinators
|
|
541
|
+
- First-class secrets handling with `Redacted`
|
|
542
|
+
|
|
543
|
+
### Basic Configuration
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
import { Effect, Config } from "effect";
|
|
547
|
+
|
|
548
|
+
const program = Effect.gen(function* () {
|
|
549
|
+
// Basic types
|
|
550
|
+
const host = yield* Config.string("HOST");
|
|
551
|
+
const port = yield* Config.number("PORT");
|
|
552
|
+
const debug = yield* Config.boolean("DEBUG");
|
|
553
|
+
const timeout = yield* Config.duration("TIMEOUT"); // "1s", "500ms", etc.
|
|
554
|
+
const logLevel = yield* Config.logLevel("LOG_LEVEL"); // LogLevel type
|
|
555
|
+
const apiUrl = yield* Config.url("API_URL"); // URL type
|
|
556
|
+
|
|
557
|
+
console.log(`Server: ${host}:${port}`);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Run with environment variables:
|
|
561
|
+
// HOST=localhost PORT=8080 npx tsx index.ts
|
|
562
|
+
Effect.runPromise(program);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Config Combinators
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import { Effect, Config, ConfigError } from "effect";
|
|
569
|
+
|
|
570
|
+
const program = Effect.gen(function* () {
|
|
571
|
+
// Default values
|
|
572
|
+
const port = yield* Config.number("PORT").pipe(Config.withDefault(3000));
|
|
573
|
+
|
|
574
|
+
// Optional values
|
|
575
|
+
const apiKey = yield* Config.string("API_KEY").pipe(
|
|
576
|
+
Config.optional, // Returns Option<string>
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
// Validation
|
|
580
|
+
const port2 = yield* Config.number("PORT").pipe(
|
|
581
|
+
Config.validate({
|
|
582
|
+
message: "Port must be between 1 and 65535",
|
|
583
|
+
validation: (n) => n >= 1 && n <= 65535,
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
|
|
587
|
+
// Transformation
|
|
588
|
+
const portString = yield* Config.number("PORT").pipe(
|
|
589
|
+
Config.map((n) => n.toString()),
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Nested configuration (reads SERVER_HOST, SERVER_PORT)
|
|
593
|
+
const serverHost = yield* Config.nested(Config.string("HOST"), "SERVER");
|
|
594
|
+
const serverPort = yield* Config.nested(Config.number("PORT"), "SERVER");
|
|
595
|
+
|
|
596
|
+
// Combine multiple configs
|
|
597
|
+
const [host, port3] = yield* Config.all([
|
|
598
|
+
Config.string("HOST"),
|
|
599
|
+
Config.number("PORT"),
|
|
600
|
+
]);
|
|
601
|
+
|
|
602
|
+
// Struct-based combination
|
|
603
|
+
const serverConfig = yield* Config.all({
|
|
604
|
+
host: Config.string("HOST"),
|
|
605
|
+
port: Config.number("PORT"),
|
|
606
|
+
debug: Config.boolean("DEBUG").pipe(Config.withDefault(false)),
|
|
607
|
+
});
|
|
608
|
+
// serverConfig: { host: string, port: number, debug: boolean }
|
|
609
|
+
|
|
610
|
+
// Fallback to alternative config
|
|
611
|
+
const port4 = yield* Config.number("PORT").pipe(
|
|
612
|
+
Config.orElse(() => Config.number("SERVER_PORT")),
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
// Array of values (comma-separated)
|
|
616
|
+
const hosts = yield* Config.array(Config.string("HOSTS"));
|
|
617
|
+
// HOSTS=a,b,c => ["a", "b", "c"]
|
|
618
|
+
|
|
619
|
+
// Set of values
|
|
620
|
+
const tags = yield* Config.hashSet(Config.string("TAGS"));
|
|
621
|
+
|
|
622
|
+
// Map of values
|
|
623
|
+
const headers = yield* Config.hashMap(Config.string("HEADERS"));
|
|
624
|
+
});
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Sensitive Data with Redacted
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
import { Effect, Config, Redacted } from "effect";
|
|
631
|
+
|
|
632
|
+
const program = Effect.gen(function* () {
|
|
633
|
+
// Load sensitive value as Redacted
|
|
634
|
+
const apiKey = yield* Config.redacted("API_KEY");
|
|
635
|
+
// apiKey: Redacted<string>
|
|
636
|
+
|
|
637
|
+
// Safe to log - shows "<redacted>"
|
|
638
|
+
console.log(`API Key: ${apiKey}`); // "API Key: <redacted>"
|
|
639
|
+
|
|
640
|
+
// Access actual value when needed
|
|
641
|
+
const actualKey = Redacted.value(apiKey);
|
|
642
|
+
// actualKey: string
|
|
643
|
+
|
|
644
|
+
// Use in HTTP request (unwrap at point of use)
|
|
645
|
+
yield* makeApiCall({
|
|
646
|
+
headers: {
|
|
647
|
+
Authorization: `Bearer ${Redacted.value(apiKey)}`,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### ConfigProvider
|
|
654
|
+
|
|
655
|
+
ConfigProvider is the abstraction for loading configuration from various sources.
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
import { Effect, Config, ConfigProvider, Layer } from "effect";
|
|
659
|
+
|
|
660
|
+
// Default: loads from environment variables
|
|
661
|
+
// No setup needed, this is the default behavior
|
|
662
|
+
|
|
663
|
+
// From a Map (useful for testing)
|
|
664
|
+
const testProvider = ConfigProvider.fromMap(
|
|
665
|
+
new Map([
|
|
666
|
+
["HOST", "localhost"],
|
|
667
|
+
["PORT", "8080"],
|
|
668
|
+
["DEBUG", "true"],
|
|
669
|
+
]),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// From JSON object
|
|
673
|
+
const jsonProvider = ConfigProvider.fromJson({
|
|
674
|
+
HOST: "localhost",
|
|
675
|
+
PORT: 8080,
|
|
676
|
+
DEBUG: true,
|
|
677
|
+
SERVER: {
|
|
678
|
+
HOST: "api.example.com",
|
|
679
|
+
PORT: 443,
|
|
680
|
+
},
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// From environment with custom delimiters
|
|
684
|
+
const envProvider = ConfigProvider.fromEnv({
|
|
685
|
+
pathDelim: "__", // For nested: SERVER__HOST
|
|
686
|
+
seqDelim: "|", // For arrays: HOSTS=a|b|c
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Provider combinators
|
|
690
|
+
const combinedProvider = ConfigProvider.fromMap(
|
|
691
|
+
new Map([["A", "from-map"]]),
|
|
692
|
+
).pipe(
|
|
693
|
+
// Fall back to environment if not found in map
|
|
694
|
+
ConfigProvider.orElse(() => ConfigProvider.fromEnv()),
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
// Nested provider (all keys prefixed)
|
|
698
|
+
const nestedProvider = ConfigProvider.fromEnv().pipe(
|
|
699
|
+
ConfigProvider.nested("MYAPP"),
|
|
700
|
+
);
|
|
701
|
+
// Reads MYAPP_HOST instead of HOST
|
|
702
|
+
|
|
703
|
+
// Case conversion
|
|
704
|
+
const snakeCaseProvider = ConfigProvider.fromEnv().pipe(
|
|
705
|
+
ConfigProvider.snakeCase, // Converts camelCase keys to SNAKE_CASE
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
const constantCaseProvider = ConfigProvider.fromEnv().pipe(
|
|
709
|
+
ConfigProvider.constantCase, // Converts to CONSTANT_CASE
|
|
710
|
+
);
|
|
711
|
+
|
|
712
|
+
// Use a custom provider
|
|
713
|
+
const program = Effect.gen(function* () {
|
|
714
|
+
const host = yield* Config.string("HOST");
|
|
715
|
+
const port = yield* Config.number("PORT");
|
|
716
|
+
return { host, port };
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Option 1: withConfigProvider
|
|
720
|
+
Effect.runPromise(Effect.withConfigProvider(program, testProvider));
|
|
721
|
+
|
|
722
|
+
// Option 2: Layer.setConfigProvider
|
|
723
|
+
const configLayer = Layer.setConfigProvider(testProvider);
|
|
724
|
+
Effect.runPromise(program.pipe(Effect.provide(configLayer)));
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Schema-Based Configuration
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
import { Effect, Config, Schema } from "effect";
|
|
731
|
+
|
|
732
|
+
// Define a schema for configuration
|
|
733
|
+
const ServerConfigSchema = Schema.Struct({
|
|
734
|
+
host: Schema.String,
|
|
735
|
+
port: Schema.Number.pipe(Schema.between(1, 65535)),
|
|
736
|
+
debug: Schema.optional(Schema.Boolean, { default: () => false }),
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
type ServerConfig = Schema.Schema.Type<typeof ServerConfigSchema>;
|
|
740
|
+
|
|
741
|
+
// Use Schema.Config to load and validate
|
|
742
|
+
const serverConfig = Schema.Config("SERVER", ServerConfigSchema);
|
|
743
|
+
|
|
744
|
+
const program = Effect.gen(function* () {
|
|
745
|
+
const config = yield* serverConfig;
|
|
746
|
+
// config: ServerConfig (fully typed and validated)
|
|
747
|
+
console.log(`Server: ${config.host}:${config.port}`);
|
|
748
|
+
});
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
## 4. Layer System for Configuration
|
|
754
|
+
|
|
755
|
+
### Overview
|
|
756
|
+
|
|
757
|
+
Effect's Layer system provides:
|
|
758
|
+
|
|
759
|
+
- Dependency injection with full type safety
|
|
760
|
+
- Composable service construction
|
|
761
|
+
- Resource lifecycle management
|
|
762
|
+
- Configuration-to-service pipelines
|
|
763
|
+
|
|
764
|
+
### Basic Layer Pattern
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
import { Context, Effect, Layer } from "effect";
|
|
768
|
+
|
|
769
|
+
// 1. Define a service interface using Context.Tag
|
|
770
|
+
class Config extends Context.Tag("Config")<
|
|
771
|
+
Config,
|
|
772
|
+
{
|
|
773
|
+
readonly host: string;
|
|
774
|
+
readonly port: number;
|
|
775
|
+
readonly debug: boolean;
|
|
776
|
+
}
|
|
777
|
+
>() {}
|
|
778
|
+
|
|
779
|
+
// 2. Create a Layer that provides the service
|
|
780
|
+
const ConfigLive = Layer.succeed(Config, {
|
|
781
|
+
host: "localhost",
|
|
782
|
+
port: 8080,
|
|
783
|
+
debug: false,
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// 3. Create a program that uses the service
|
|
787
|
+
const program = Effect.gen(function* () {
|
|
788
|
+
const config = yield* Config;
|
|
789
|
+
console.log(`Server: ${config.host}:${config.port}`);
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// 4. Provide the layer and run
|
|
793
|
+
Effect.runPromise(program.pipe(Effect.provide(ConfigLive)));
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
### Config-Based Layers
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
import { Context, Effect, Layer, Config } from "effect";
|
|
800
|
+
|
|
801
|
+
// Service definition
|
|
802
|
+
class AppConfig extends Context.Tag("AppConfig")<
|
|
803
|
+
AppConfig,
|
|
804
|
+
{
|
|
805
|
+
readonly host: string;
|
|
806
|
+
readonly port: number;
|
|
807
|
+
readonly debug: boolean;
|
|
808
|
+
readonly apiKey: string;
|
|
809
|
+
}
|
|
810
|
+
>() {}
|
|
811
|
+
|
|
812
|
+
// Layer that loads from Config
|
|
813
|
+
const AppConfigLive = Layer.effect(
|
|
814
|
+
AppConfig,
|
|
815
|
+
Effect.gen(function* () {
|
|
816
|
+
const host = yield* Config.string("HOST").pipe(
|
|
817
|
+
Config.withDefault("localhost"),
|
|
818
|
+
);
|
|
819
|
+
const port = yield* Config.number("PORT").pipe(Config.withDefault(3000));
|
|
820
|
+
const debug = yield* Config.boolean("DEBUG").pipe(
|
|
821
|
+
Config.withDefault(false),
|
|
822
|
+
);
|
|
823
|
+
const apiKey = yield* Config.string("API_KEY");
|
|
824
|
+
|
|
825
|
+
return { host, port, debug, apiKey };
|
|
826
|
+
}),
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
// Use the config service
|
|
830
|
+
const program = Effect.gen(function* () {
|
|
831
|
+
const config = yield* AppConfig;
|
|
832
|
+
yield* Effect.log(`Starting server on ${config.host}:${config.port}`);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
// Run with environment variables
|
|
836
|
+
Effect.runPromise(program.pipe(Effect.provide(AppConfigLive)));
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
### Layer Composition
|
|
840
|
+
|
|
841
|
+
```typescript
|
|
842
|
+
import { Context, Effect, Layer, Config } from "effect";
|
|
843
|
+
|
|
844
|
+
// Config service
|
|
845
|
+
class AppConfig extends Context.Tag("AppConfig")<
|
|
846
|
+
AppConfig,
|
|
847
|
+
{ readonly logLevel: string; readonly dbUrl: string }
|
|
848
|
+
>() {}
|
|
849
|
+
|
|
850
|
+
const AppConfigLive = Layer.effect(
|
|
851
|
+
AppConfig,
|
|
852
|
+
Effect.gen(function* () {
|
|
853
|
+
const logLevel = yield* Config.string("LOG_LEVEL").pipe(
|
|
854
|
+
Config.withDefault("info"),
|
|
855
|
+
);
|
|
856
|
+
const dbUrl = yield* Config.string("DATABASE_URL");
|
|
857
|
+
return { logLevel, dbUrl };
|
|
858
|
+
}),
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
// Logger service (depends on Config)
|
|
862
|
+
class Logger extends Context.Tag("Logger")<
|
|
863
|
+
Logger,
|
|
864
|
+
{ readonly log: (message: string) => Effect.Effect<void> }
|
|
865
|
+
>() {}
|
|
866
|
+
|
|
867
|
+
const LoggerLive = Layer.effect(
|
|
868
|
+
Logger,
|
|
869
|
+
Effect.gen(function* () {
|
|
870
|
+
const config = yield* AppConfig; // Depends on AppConfig
|
|
871
|
+
return {
|
|
872
|
+
log: (message) =>
|
|
873
|
+
Effect.gen(function* () {
|
|
874
|
+
console.log(`[${config.logLevel.toUpperCase()}] ${message}`);
|
|
875
|
+
}),
|
|
876
|
+
};
|
|
877
|
+
}),
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
// Database service (depends on Config and Logger)
|
|
881
|
+
class Database extends Context.Tag("Database")<
|
|
882
|
+
Database,
|
|
883
|
+
{ readonly query: (sql: string) => Effect.Effect<unknown[]> }
|
|
884
|
+
>() {}
|
|
885
|
+
|
|
886
|
+
const DatabaseLive = Layer.effect(
|
|
887
|
+
Database,
|
|
888
|
+
Effect.gen(function* () {
|
|
889
|
+
const config = yield* AppConfig;
|
|
890
|
+
const logger = yield* Logger;
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
query: (sql) =>
|
|
894
|
+
Effect.gen(function* () {
|
|
895
|
+
yield* logger.log(`Executing: ${sql}`);
|
|
896
|
+
// Simulate DB query
|
|
897
|
+
return [{ id: 1, name: "test" }];
|
|
898
|
+
}),
|
|
899
|
+
};
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// Compose layers
|
|
904
|
+
// Method 1: Layer.provide (sequential - inner depends on outer)
|
|
905
|
+
const LoggerWithConfig = Layer.provide(LoggerLive, AppConfigLive);
|
|
906
|
+
|
|
907
|
+
// Method 2: Layer.merge (parallel - combine independent outputs)
|
|
908
|
+
const ConfigAndLogger = Layer.merge(AppConfigLive, LoggerLive);
|
|
909
|
+
|
|
910
|
+
// Method 3: Layer.mergeAll (merge multiple layers)
|
|
911
|
+
const AllServices = Layer.mergeAll(AppConfigLive, LoggerLive, DatabaseLive);
|
|
912
|
+
|
|
913
|
+
// Method 4: Build full dependency graph
|
|
914
|
+
const MainLayer = DatabaseLive.pipe(
|
|
915
|
+
Layer.provide(LoggerLive),
|
|
916
|
+
Layer.provide(AppConfigLive),
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
// Program using all services
|
|
920
|
+
const program = Effect.gen(function* () {
|
|
921
|
+
const db = yield* Database;
|
|
922
|
+
const logger = yield* Logger;
|
|
923
|
+
|
|
924
|
+
yield* logger.log("Starting application");
|
|
925
|
+
const results = yield* db.query("SELECT * FROM users");
|
|
926
|
+
yield* logger.log(`Found ${results.length} users`);
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Run with composed layer
|
|
930
|
+
Effect.runPromise(program.pipe(Effect.provide(MainLayer)));
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
### Testing with Layers
|
|
934
|
+
|
|
935
|
+
```typescript
|
|
936
|
+
import { Context, Effect, Layer, Config, ConfigProvider } from "effect";
|
|
937
|
+
|
|
938
|
+
// Test layer with mock config provider
|
|
939
|
+
const TestConfigProvider = Layer.setConfigProvider(
|
|
940
|
+
ConfigProvider.fromMap(
|
|
941
|
+
new Map([
|
|
942
|
+
["LOG_LEVEL", "debug"],
|
|
943
|
+
["DATABASE_URL", "sqlite::memory:"],
|
|
944
|
+
]),
|
|
945
|
+
),
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
// Test layer for Logger (mock implementation)
|
|
949
|
+
const LoggerTest = Layer.succeed(Logger, {
|
|
950
|
+
log: (message) =>
|
|
951
|
+
Effect.sync(() => {
|
|
952
|
+
// Collect logs for assertions instead of printing
|
|
953
|
+
testLogs.push(message);
|
|
954
|
+
}),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// Run tests with test layers
|
|
958
|
+
const testProgram = program.pipe(
|
|
959
|
+
Effect.provide(
|
|
960
|
+
Layer.mergeAll(TestConfigProvider, AppConfigLive, LoggerTest, DatabaseLive),
|
|
961
|
+
),
|
|
962
|
+
);
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## 5. CLI + Config Integration Pattern
|
|
968
|
+
|
|
969
|
+
### Complete Integration Example
|
|
970
|
+
|
|
971
|
+
This example shows how to integrate CLI options with Effect's config system.
|
|
972
|
+
|
|
973
|
+
```typescript
|
|
974
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
975
|
+
import { FileSystem } from "@effect/platform";
|
|
976
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
977
|
+
import {
|
|
978
|
+
Context,
|
|
979
|
+
Effect,
|
|
980
|
+
Layer,
|
|
981
|
+
Config,
|
|
982
|
+
ConfigProvider,
|
|
983
|
+
Schema,
|
|
984
|
+
Redacted,
|
|
985
|
+
} from "effect";
|
|
986
|
+
|
|
987
|
+
// ============================================
|
|
988
|
+
// 1. Define Configuration Schema
|
|
989
|
+
// ============================================
|
|
990
|
+
|
|
991
|
+
const AppConfigSchema = Schema.Struct({
|
|
992
|
+
host: Schema.String.pipe(
|
|
993
|
+
Schema.propertySignature,
|
|
994
|
+
Schema.withDefault(() => "localhost"),
|
|
995
|
+
),
|
|
996
|
+
port: Schema.Number.pipe(
|
|
997
|
+
Schema.between(1, 65535),
|
|
998
|
+
Schema.propertySignature,
|
|
999
|
+
Schema.withDefault(() => 3000),
|
|
1000
|
+
),
|
|
1001
|
+
logLevel: Schema.Literal("debug", "info", "warn", "error").pipe(
|
|
1002
|
+
Schema.propertySignature,
|
|
1003
|
+
Schema.withDefault(() => "info" as const),
|
|
1004
|
+
),
|
|
1005
|
+
apiKey: Schema.optional(Schema.String),
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
type AppConfig = Schema.Schema.Type<typeof AppConfigSchema>;
|
|
1009
|
+
|
|
1010
|
+
// ============================================
|
|
1011
|
+
// 2. Define Config Service
|
|
1012
|
+
// ============================================
|
|
1013
|
+
|
|
1014
|
+
class AppConfigService extends Context.Tag("AppConfigService")<
|
|
1015
|
+
AppConfigService,
|
|
1016
|
+
AppConfig
|
|
1017
|
+
>() {}
|
|
1018
|
+
|
|
1019
|
+
// Layer that loads config from Effect's Config system
|
|
1020
|
+
const AppConfigFromEnv = Layer.effect(
|
|
1021
|
+
AppConfigService,
|
|
1022
|
+
Effect.gen(function* () {
|
|
1023
|
+
const host = yield* Config.string("HOST").pipe(
|
|
1024
|
+
Config.withDefault("localhost"),
|
|
1025
|
+
);
|
|
1026
|
+
const port = yield* Config.number("PORT").pipe(Config.withDefault(3000));
|
|
1027
|
+
const logLevel = yield* Config.string("LOG_LEVEL").pipe(
|
|
1028
|
+
Config.withDefault("info"),
|
|
1029
|
+
);
|
|
1030
|
+
const apiKey = yield* Config.string("API_KEY").pipe(Config.optional);
|
|
1031
|
+
|
|
1032
|
+
return { host, port, logLevel, apiKey } as AppConfig;
|
|
1033
|
+
}),
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
// ============================================
|
|
1037
|
+
// 3. Define CLI Options
|
|
1038
|
+
// ============================================
|
|
1039
|
+
|
|
1040
|
+
const hostOption = Options.text("host").pipe(
|
|
1041
|
+
Options.withAlias("H"),
|
|
1042
|
+
Options.withDescription("Server host"),
|
|
1043
|
+
Options.optional,
|
|
1044
|
+
);
|
|
1045
|
+
|
|
1046
|
+
const portOption = Options.integer("port").pipe(
|
|
1047
|
+
Options.withAlias("p"),
|
|
1048
|
+
Options.withDescription("Server port"),
|
|
1049
|
+
Options.optional,
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
const logLevelOption = Options.choice("log-level", [
|
|
1053
|
+
"debug",
|
|
1054
|
+
"info",
|
|
1055
|
+
"warn",
|
|
1056
|
+
"error",
|
|
1057
|
+
] as const).pipe(
|
|
1058
|
+
Options.withAlias("l"),
|
|
1059
|
+
Options.withDescription("Log level"),
|
|
1060
|
+
Options.optional,
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
const configFileOption = Options.file("config").pipe(
|
|
1064
|
+
Options.withAlias("c"),
|
|
1065
|
+
Options.withDescription("Path to config file"),
|
|
1066
|
+
Options.optional,
|
|
1067
|
+
);
|
|
1068
|
+
|
|
1069
|
+
// ============================================
|
|
1070
|
+
// 4. Create Config Layer from CLI + File + Env
|
|
1071
|
+
// ============================================
|
|
1072
|
+
|
|
1073
|
+
interface CliOptions {
|
|
1074
|
+
host: typeof hostOption extends Options.Options<infer A> ? A : never;
|
|
1075
|
+
port: typeof portOption extends Options.Options<infer A> ? A : never;
|
|
1076
|
+
logLevel: typeof logLevelOption extends Options.Options<infer A> ? A : never;
|
|
1077
|
+
configFile: typeof configFileOption extends Options.Options<infer A>
|
|
1078
|
+
? A
|
|
1079
|
+
: never;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const makeConfigLayer = (cliOptions: CliOptions) =>
|
|
1083
|
+
Layer.effect(
|
|
1084
|
+
AppConfigService,
|
|
1085
|
+
Effect.gen(function* () {
|
|
1086
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1087
|
+
|
|
1088
|
+
// 1. Start with defaults
|
|
1089
|
+
let config: AppConfig = {
|
|
1090
|
+
host: "localhost",
|
|
1091
|
+
port: 3000,
|
|
1092
|
+
logLevel: "info",
|
|
1093
|
+
apiKey: undefined,
|
|
1094
|
+
};
|
|
1095
|
+
|
|
1096
|
+
// 2. Load from config file if specified
|
|
1097
|
+
if (cliOptions.configFile._tag === "Some") {
|
|
1098
|
+
const fileContent = yield* fs.readFileString(
|
|
1099
|
+
cliOptions.configFile.value,
|
|
1100
|
+
);
|
|
1101
|
+
const fileConfig = JSON.parse(fileContent);
|
|
1102
|
+
config = { ...config, ...fileConfig };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// 3. Override with environment variables
|
|
1106
|
+
const envHost = yield* Config.string("HOST").pipe(Config.optional);
|
|
1107
|
+
const envPort = yield* Config.number("PORT").pipe(Config.optional);
|
|
1108
|
+
const envLogLevel = yield* Config.string("LOG_LEVEL").pipe(
|
|
1109
|
+
Config.optional,
|
|
1110
|
+
);
|
|
1111
|
+
const envApiKey = yield* Config.string("API_KEY").pipe(Config.optional);
|
|
1112
|
+
|
|
1113
|
+
if (envHost._tag === "Some") config.host = envHost.value;
|
|
1114
|
+
if (envPort._tag === "Some") config.port = envPort.value;
|
|
1115
|
+
if (envLogLevel._tag === "Some")
|
|
1116
|
+
config.logLevel = envLogLevel.value as AppConfig["logLevel"];
|
|
1117
|
+
if (envApiKey._tag === "Some") config.apiKey = envApiKey.value;
|
|
1118
|
+
|
|
1119
|
+
// 4. Override with CLI flags (highest priority)
|
|
1120
|
+
if (cliOptions.host._tag === "Some") config.host = cliOptions.host.value;
|
|
1121
|
+
if (cliOptions.port._tag === "Some") config.port = cliOptions.port.value;
|
|
1122
|
+
if (cliOptions.logLevel._tag === "Some")
|
|
1123
|
+
config.logLevel = cliOptions.logLevel.value;
|
|
1124
|
+
|
|
1125
|
+
return config;
|
|
1126
|
+
}),
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
// ============================================
|
|
1130
|
+
// 5. Define Commands
|
|
1131
|
+
// ============================================
|
|
1132
|
+
|
|
1133
|
+
const serve = Command.make(
|
|
1134
|
+
"serve",
|
|
1135
|
+
{
|
|
1136
|
+
host: hostOption,
|
|
1137
|
+
port: portOption,
|
|
1138
|
+
logLevel: logLevelOption,
|
|
1139
|
+
configFile: configFileOption,
|
|
1140
|
+
},
|
|
1141
|
+
(cliOptions) =>
|
|
1142
|
+
Effect.gen(function* () {
|
|
1143
|
+
const config = yield* AppConfigService;
|
|
1144
|
+
|
|
1145
|
+
yield* Effect.log(`Starting server on ${config.host}:${config.port}`);
|
|
1146
|
+
yield* Effect.log(`Log level: ${config.logLevel}`);
|
|
1147
|
+
|
|
1148
|
+
// Server implementation...
|
|
1149
|
+
}).pipe(Effect.provide(makeConfigLayer(cliOptions))),
|
|
1150
|
+
);
|
|
1151
|
+
|
|
1152
|
+
const check = Command.make(
|
|
1153
|
+
"check",
|
|
1154
|
+
{
|
|
1155
|
+
configFile: configFileOption,
|
|
1156
|
+
},
|
|
1157
|
+
({ configFile }) =>
|
|
1158
|
+
Effect.gen(function* () {
|
|
1159
|
+
yield* Effect.log("Checking configuration...");
|
|
1160
|
+
|
|
1161
|
+
const config = yield* AppConfigService;
|
|
1162
|
+
yield* Effect.log(`Host: ${config.host}`);
|
|
1163
|
+
yield* Effect.log(`Port: ${config.port}`);
|
|
1164
|
+
yield* Effect.log(`Log Level: ${config.logLevel}`);
|
|
1165
|
+
yield* Effect.log(`API Key: ${config.apiKey ? "***" : "not set"}`);
|
|
1166
|
+
}).pipe(
|
|
1167
|
+
Effect.provide(
|
|
1168
|
+
makeConfigLayer({
|
|
1169
|
+
host: { _tag: "None" },
|
|
1170
|
+
port: { _tag: "None" },
|
|
1171
|
+
logLevel: { _tag: "None" },
|
|
1172
|
+
configFile,
|
|
1173
|
+
}),
|
|
1174
|
+
),
|
|
1175
|
+
),
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
const main = Command.make("myapp", {}).pipe(
|
|
1179
|
+
Command.withSubcommands([serve, check]),
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
// ============================================
|
|
1183
|
+
// 6. Run CLI
|
|
1184
|
+
// ============================================
|
|
1185
|
+
|
|
1186
|
+
const cli = Command.run(main, {
|
|
1187
|
+
name: "myapp",
|
|
1188
|
+
version: "1.0.0",
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
|
1192
|
+
```
|
|
1193
|
+
|
|
1194
|
+
Usage:
|
|
1195
|
+
|
|
1196
|
+
```bash
|
|
1197
|
+
# Use defaults
|
|
1198
|
+
myapp serve
|
|
1199
|
+
|
|
1200
|
+
# Use config file
|
|
1201
|
+
myapp serve --config ./myapp.json
|
|
1202
|
+
|
|
1203
|
+
# Override with CLI flags
|
|
1204
|
+
myapp serve --host 0.0.0.0 --port 9000 --log-level debug
|
|
1205
|
+
|
|
1206
|
+
# Environment variables work too
|
|
1207
|
+
HOST=prod.example.com PORT=443 myapp serve
|
|
1208
|
+
|
|
1209
|
+
# Check config resolution
|
|
1210
|
+
myapp check --config ./myapp.json
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
## 6. Real-World Examples
|
|
1216
|
+
|
|
1217
|
+
### Effect's Own CLI Template
|
|
1218
|
+
|
|
1219
|
+
The Effect team maintains an official CLI template in their examples repository:
|
|
1220
|
+
|
|
1221
|
+
**Source**: [Effect-TS/examples - CLI Template](https://github.com/Effect-TS/examples)
|
|
1222
|
+
|
|
1223
|
+
Features:
|
|
1224
|
+
|
|
1225
|
+
- Single-file bundling with tsup
|
|
1226
|
+
- Pre-configured for @effect/cli
|
|
1227
|
+
- Recommended project structure
|
|
1228
|
+
|
|
1229
|
+
### Effect Discord Bot
|
|
1230
|
+
|
|
1231
|
+
The Effect team maintains a Discord bot built with Effect:
|
|
1232
|
+
|
|
1233
|
+
**Source**: [Effect-TS/discord-bot](https://github.com/Effect-TS/discord-bot)
|
|
1234
|
+
|
|
1235
|
+
Demonstrates:
|
|
1236
|
+
|
|
1237
|
+
- Effect service architecture
|
|
1238
|
+
- Layer composition
|
|
1239
|
+
- Real-world Effect patterns
|
|
1240
|
+
|
|
1241
|
+
### create-effect-app
|
|
1242
|
+
|
|
1243
|
+
The official scaffolding tool for Effect projects:
|
|
1244
|
+
|
|
1245
|
+
**Source**: [create-effect-app](https://effect.website/docs/getting-started/create-effect-app/)
|
|
1246
|
+
|
|
1247
|
+
```bash
|
|
1248
|
+
# Create new Effect project
|
|
1249
|
+
npx create-effect-app
|
|
1250
|
+
|
|
1251
|
+
# Options:
|
|
1252
|
+
# - Basic template (single package)
|
|
1253
|
+
# - Monorepo template (multi-package)
|
|
1254
|
+
# - CLI template (command-line app)
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
---
|
|
1258
|
+
|
|
1259
|
+
## 7. Comparison with Previous Research
|
|
1260
|
+
|
|
1261
|
+
### vs. c12/citty (from 033-research-configuration-management.md)
|
|
1262
|
+
|
|
1263
|
+
| Aspect | Effect Ecosystem | c12/citty (UnJS) |
|
|
1264
|
+
| ------------------ | ------------------------ | ----------------- |
|
|
1265
|
+
| **CLI Framework** | @effect/cli | citty |
|
|
1266
|
+
| **Config Loading** | ConfigProvider | c12 |
|
|
1267
|
+
| **Validation** | Effect Schema | Zod |
|
|
1268
|
+
| **Type Safety** | Excellent (Effect types) | Good (TypeScript) |
|
|
1269
|
+
| **DI/Services** | Layer system | Manual |
|
|
1270
|
+
| **Error Handling** | Effect errors | Try/catch |
|
|
1271
|
+
| **Learning Curve** | Steep (Effect paradigm) | Moderate |
|
|
1272
|
+
| **Bundle Size** | Larger (full Effect) | Smaller |
|
|
1273
|
+
| **Ecosystem** | Effect ecosystem | UnJS ecosystem |
|
|
1274
|
+
|
|
1275
|
+
### When to Choose Effect
|
|
1276
|
+
|
|
1277
|
+
Choose Effect when:
|
|
1278
|
+
|
|
1279
|
+
- Building a larger CLI with complex service dependencies
|
|
1280
|
+
- Already using Effect elsewhere in the application
|
|
1281
|
+
- Type-safe error handling is critical
|
|
1282
|
+
- Need sophisticated resource management (Scope)
|
|
1283
|
+
- Want unified testing approach with Effect
|
|
1284
|
+
|
|
1285
|
+
Choose c12/citty when:
|
|
1286
|
+
|
|
1287
|
+
- Building a simpler CLI
|
|
1288
|
+
- Want minimal dependencies
|
|
1289
|
+
- Not already invested in Effect
|
|
1290
|
+
- Prefer imperative style
|
|
1291
|
+
- Need faster onboarding for contributors
|
|
1292
|
+
|
|
1293
|
+
---
|
|
1294
|
+
|
|
1295
|
+
## 8. Recommendations for mdcontext
|
|
1296
|
+
|
|
1297
|
+
### Option A: Full Effect (Recommended if committing to Effect)
|
|
1298
|
+
|
|
1299
|
+
Use Effect throughout the application:
|
|
1300
|
+
|
|
1301
|
+
```
|
|
1302
|
+
@effect/cli - CLI framework
|
|
1303
|
+
@effect/platform - File system, process execution
|
|
1304
|
+
effect Config - Configuration loading
|
|
1305
|
+
effect Layer - Dependency injection
|
|
1306
|
+
effect Schema - Validation
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
**Pros**:
|
|
1310
|
+
|
|
1311
|
+
- Unified paradigm
|
|
1312
|
+
- Superior type safety
|
|
1313
|
+
- Excellent composability
|
|
1314
|
+
- Built-in resource management
|
|
1315
|
+
|
|
1316
|
+
**Cons**:
|
|
1317
|
+
|
|
1318
|
+
- Steep learning curve
|
|
1319
|
+
- Larger bundle
|
|
1320
|
+
- All-in commitment
|
|
1321
|
+
|
|
1322
|
+
### Option B: Hybrid Approach
|
|
1323
|
+
|
|
1324
|
+
Use Effect for core functionality, simpler tools for CLI:
|
|
1325
|
+
|
|
1326
|
+
```
|
|
1327
|
+
citty - CLI framework (lighter)
|
|
1328
|
+
c12 - Config loading
|
|
1329
|
+
effect - Core business logic
|
|
1330
|
+
zod - Validation
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
**Pros**:
|
|
1334
|
+
|
|
1335
|
+
- Lower barrier to entry
|
|
1336
|
+
- Smaller CLI bundle
|
|
1337
|
+
- Flexibility
|
|
1338
|
+
|
|
1339
|
+
**Cons**:
|
|
1340
|
+
|
|
1341
|
+
- Mixed paradigms
|
|
1342
|
+
- Manual integration needed
|
|
1343
|
+
|
|
1344
|
+
### Recommended: Start with Option A
|
|
1345
|
+
|
|
1346
|
+
Given that mdcontext is exploring Effect for its core functionality, using the full Effect ecosystem for CLI and config provides:
|
|
1347
|
+
|
|
1348
|
+
1. **Consistency**: Same patterns throughout
|
|
1349
|
+
2. **Type Safety**: End-to-end typed effects
|
|
1350
|
+
3. **Testing**: Unified mocking with Layers
|
|
1351
|
+
4. **Future-Proofing**: Effect ecosystem is growing
|
|
1352
|
+
|
|
1353
|
+
Migration path:
|
|
1354
|
+
|
|
1355
|
+
1. Start with @effect/cli for command structure
|
|
1356
|
+
2. Use Effect Config for environment/file loading
|
|
1357
|
+
3. Build service Layers for business logic
|
|
1358
|
+
4. Use @effect/platform for file system operations
|
|
1359
|
+
|
|
1360
|
+
---
|
|
1361
|
+
|
|
1362
|
+
## 9. Code Templates
|
|
1363
|
+
|
|
1364
|
+
### Minimal CLI with Config
|
|
1365
|
+
|
|
1366
|
+
```typescript
|
|
1367
|
+
// src/cli.ts
|
|
1368
|
+
import { Args, Command, Options } from "@effect/cli";
|
|
1369
|
+
import { NodeContext, NodeRuntime } from "@effect/platform-node";
|
|
1370
|
+
import { Effect, Config, Layer, Context } from "effect";
|
|
1371
|
+
|
|
1372
|
+
// Config service
|
|
1373
|
+
class AppConfig extends Context.Tag("AppConfig")<
|
|
1374
|
+
AppConfig,
|
|
1375
|
+
{
|
|
1376
|
+
readonly verbose: boolean;
|
|
1377
|
+
readonly outputDir: string;
|
|
1378
|
+
}
|
|
1379
|
+
>() {}
|
|
1380
|
+
|
|
1381
|
+
const AppConfigLive = Layer.effect(
|
|
1382
|
+
AppConfig,
|
|
1383
|
+
Effect.gen(function* () {
|
|
1384
|
+
const verbose = yield* Config.boolean("VERBOSE").pipe(
|
|
1385
|
+
Config.withDefault(false),
|
|
1386
|
+
);
|
|
1387
|
+
const outputDir = yield* Config.string("OUTPUT_DIR").pipe(
|
|
1388
|
+
Config.withDefault("./dist"),
|
|
1389
|
+
);
|
|
1390
|
+
return { verbose, outputDir };
|
|
1391
|
+
}),
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
// CLI command
|
|
1395
|
+
const build = Command.make(
|
|
1396
|
+
"build",
|
|
1397
|
+
{
|
|
1398
|
+
input: Args.file({ name: "input" }),
|
|
1399
|
+
verbose: Options.boolean("verbose").pipe(
|
|
1400
|
+
Options.withAlias("v"),
|
|
1401
|
+
Options.optional,
|
|
1402
|
+
),
|
|
1403
|
+
output: Options.directory("output").pipe(
|
|
1404
|
+
Options.withAlias("o"),
|
|
1405
|
+
Options.optional,
|
|
1406
|
+
),
|
|
1407
|
+
},
|
|
1408
|
+
({ input, verbose, output }) =>
|
|
1409
|
+
Effect.gen(function* () {
|
|
1410
|
+
const config = yield* AppConfig;
|
|
1411
|
+
|
|
1412
|
+
const isVerbose =
|
|
1413
|
+
verbose._tag === "Some" ? verbose.value : config.verbose;
|
|
1414
|
+
const outputDir =
|
|
1415
|
+
output._tag === "Some" ? output.value : config.outputDir;
|
|
1416
|
+
|
|
1417
|
+
if (isVerbose) {
|
|
1418
|
+
yield* Effect.log(`Building ${input} to ${outputDir}`);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Build logic...
|
|
1422
|
+
}).pipe(Effect.provide(AppConfigLive)),
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1425
|
+
const cli = Command.run(build, { name: "mybuild", version: "1.0.0" });
|
|
1426
|
+
|
|
1427
|
+
cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
### Config File Support
|
|
1431
|
+
|
|
1432
|
+
```typescript
|
|
1433
|
+
// src/config.ts
|
|
1434
|
+
import { FileSystem } from "@effect/platform";
|
|
1435
|
+
import { Effect, Config, ConfigProvider, Layer, Context, Schema } from "effect";
|
|
1436
|
+
|
|
1437
|
+
// Config schema
|
|
1438
|
+
const ConfigSchema = Schema.Struct({
|
|
1439
|
+
logLevel: Schema.Literal("debug", "info", "warn", "error"),
|
|
1440
|
+
watch: Schema.Struct({
|
|
1441
|
+
enabled: Schema.Boolean,
|
|
1442
|
+
debounce: Schema.Number,
|
|
1443
|
+
}),
|
|
1444
|
+
output: Schema.Struct({
|
|
1445
|
+
format: Schema.Literal("json", "markdown"),
|
|
1446
|
+
dir: Schema.String,
|
|
1447
|
+
}),
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
type AppConfigType = Schema.Schema.Type<typeof ConfigSchema>;
|
|
1451
|
+
|
|
1452
|
+
// Load config from file + env
|
|
1453
|
+
const loadConfigFromFile = (path: string) =>
|
|
1454
|
+
Effect.gen(function* () {
|
|
1455
|
+
const fs = yield* FileSystem.FileSystem;
|
|
1456
|
+
const content = yield* fs.readFileString(path);
|
|
1457
|
+
const json = JSON.parse(content);
|
|
1458
|
+
return Schema.decodeUnknownSync(ConfigSchema)(json);
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
// ConfigProvider that tries file first, then env
|
|
1462
|
+
const makeConfigProvider = (configPath?: string) =>
|
|
1463
|
+
Effect.gen(function* () {
|
|
1464
|
+
if (configPath) {
|
|
1465
|
+
const fileConfig = yield* loadConfigFromFile(configPath);
|
|
1466
|
+
return ConfigProvider.fromJson(fileConfig);
|
|
1467
|
+
}
|
|
1468
|
+
return ConfigProvider.fromEnv();
|
|
1469
|
+
});
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
---
|
|
1473
|
+
|
|
1474
|
+
## References
|
|
1475
|
+
|
|
1476
|
+
### Official Documentation
|
|
1477
|
+
|
|
1478
|
+
- [Effect Documentation](https://effect.website/)
|
|
1479
|
+
- [@effect/cli API](https://effect-ts.github.io/effect/docs/cli)
|
|
1480
|
+
- [Effect Configuration](https://effect.website/docs/configuration/)
|
|
1481
|
+
- [@effect/platform](https://effect.website/docs/platform/introduction/)
|
|
1482
|
+
|
|
1483
|
+
### GitHub
|
|
1484
|
+
|
|
1485
|
+
- [Effect-TS/effect](https://github.com/Effect-TS/effect)
|
|
1486
|
+
- [Effect-TS/examples](https://github.com/Effect-TS/examples)
|
|
1487
|
+
- [@effect/cli README](https://github.com/Effect-TS/effect/blob/main/packages/cli/README.md)
|
|
1488
|
+
- [@effect/platform README](https://github.com/Effect-TS/effect/blob/main/packages/platform/README.md)
|
|
1489
|
+
|
|
1490
|
+
### NPM
|
|
1491
|
+
|
|
1492
|
+
- [@effect/cli](https://www.npmjs.com/package/@effect/cli)
|
|
1493
|
+
- [@effect/platform](https://www.npmjs.com/package/@effect/platform)
|
|
1494
|
+
- [@effect/platform-node](https://www.npmjs.com/package/@effect/platform-node)
|
|
1495
|
+
|
|
1496
|
+
### Community
|
|
1497
|
+
|
|
1498
|
+
- [Effect Discord](https://discord.gg/effect-ts)
|
|
1499
|
+
- [Effect Solutions](https://www.effect.solutions/)
|
|
1500
|
+
- [DeepWiki - Effect CLI Framework](https://deepwiki.com/Effect-TS/effect/8.1-cli-framework)
|
|
1501
|
+
|
|
1502
|
+
---
|
|
1503
|
+
|
|
1504
|
+
_Research completed: 2026-01-21_
|