prompt_manager 0.5.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +206 -516
  4. data/Rakefile +0 -8
  5. data/docs/api/configuration.md +31 -327
  6. data/docs/api/constants.md +60 -0
  7. data/docs/api/index.md +14 -0
  8. data/docs/api/metadata.md +99 -0
  9. data/docs/api/parsed.md +98 -0
  10. data/docs/api/pm-module.md +131 -0
  11. data/docs/api/render-context.md +51 -0
  12. data/docs/architecture/design-decisions.md +70 -0
  13. data/docs/architecture/index.md +6 -0
  14. data/docs/architecture/processing-pipeline.md +112 -0
  15. data/docs/assets/css/custom.css +1 -0
  16. data/docs/assets/images/prompt_manager.gif +0 -0
  17. data/docs/assets/images/prompt_manager.mp4 +0 -0
  18. data/docs/examples/ai-agent-prompts.md +173 -0
  19. data/docs/examples/code-review-prompt.md +107 -0
  20. data/docs/examples/index.md +7 -0
  21. data/docs/examples/multi-file-composition.md +123 -0
  22. data/docs/getting-started/configuration.md +106 -0
  23. data/docs/getting-started/index.md +7 -0
  24. data/docs/getting-started/installation.md +10 -73
  25. data/docs/getting-started/quick-start.md +50 -225
  26. data/docs/guides/comment-stripping.md +64 -0
  27. data/docs/guides/custom-directives.md +115 -0
  28. data/docs/guides/erb-rendering.md +102 -0
  29. data/docs/guides/includes.md +146 -0
  30. data/docs/guides/index.md +11 -0
  31. data/docs/guides/parameters.md +96 -0
  32. data/docs/guides/parsing.md +127 -0
  33. data/docs/guides/shell-expansion.md +108 -0
  34. data/docs/index.md +54 -214
  35. data/lib/pm/configuration.rb +17 -0
  36. data/lib/pm/directives.rb +61 -0
  37. data/lib/pm/metadata.rb +17 -0
  38. data/lib/pm/parsed.rb +59 -0
  39. data/lib/pm/shell.rb +57 -0
  40. data/lib/pm/version.rb +5 -0
  41. data/lib/pm.rb +121 -0
  42. data/lib/prompt_manager.rb +2 -27
  43. data/mkdocs.yml +101 -66
  44. metadata +42 -101
  45. data/docs/.keep +0 -0
  46. data/docs/advanced/custom-keywords.md +0 -421
  47. data/docs/advanced/dynamic-directives.md +0 -535
  48. data/docs/advanced/performance.md +0 -612
  49. data/docs/advanced/search-integration.md +0 -635
  50. data/docs/api/directive-processor.md +0 -431
  51. data/docs/api/prompt-class.md +0 -354
  52. data/docs/api/storage-adapters.md +0 -462
  53. data/docs/assets/favicon.ico +0 -1
  54. data/docs/assets/logo.svg +0 -24
  55. data/docs/core-features/comments.md +0 -48
  56. data/docs/core-features/directive-processing.md +0 -38
  57. data/docs/core-features/erb-integration.md +0 -68
  58. data/docs/core-features/error-handling.md +0 -197
  59. data/docs/core-features/parameter-history.md +0 -76
  60. data/docs/core-features/parameterized-prompts.md +0 -500
  61. data/docs/core-features/shell-integration.md +0 -79
  62. data/docs/development/architecture.md +0 -544
  63. data/docs/development/contributing.md +0 -425
  64. data/docs/development/roadmap.md +0 -234
  65. data/docs/development/testing.md +0 -822
  66. data/docs/examples/advanced.md +0 -523
  67. data/docs/examples/basic.md +0 -688
  68. data/docs/examples/real-world.md +0 -776
  69. data/docs/examples.md +0 -337
  70. data/docs/getting-started/basic-concepts.md +0 -318
  71. data/docs/migration/v0.9.0.md +0 -459
  72. data/docs/migration/v1.0.0.md +0 -591
  73. data/docs/storage/activerecord-adapter.md +0 -348
  74. data/docs/storage/custom-adapters.md +0 -176
  75. data/docs/storage/filesystem-adapter.md +0 -236
  76. data/docs/storage/overview.md +0 -427
  77. data/examples/advanced_integrations.rb +0 -52
  78. data/examples/directives.rb +0 -102
  79. data/examples/prompts_dir/advanced_demo.txt +0 -79
  80. data/examples/prompts_dir/directive_example.json +0 -1
  81. data/examples/prompts_dir/directive_example.txt +0 -8
  82. data/examples/prompts_dir/todo.json +0 -1
  83. data/examples/prompts_dir/todo.txt +0 -7
  84. data/examples/prompts_dir/toy/8-ball.txt +0 -4
  85. data/examples/rgfzf +0 -44
  86. data/examples/simple.rb +0 -160
  87. data/examples/using_search_proc.rb +0 -68
  88. data/improvement_plan.md +0 -996
  89. data/lib/prompt_manager/directive_processor.rb +0 -47
  90. data/lib/prompt_manager/prompt.rb +0 -195
  91. data/lib/prompt_manager/storage/active_record_adapter.rb +0 -157
  92. data/lib/prompt_manager/storage/file_system_adapter.rb +0 -339
  93. data/lib/prompt_manager/storage.rb +0 -34
  94. data/lib/prompt_manager/version.rb +0 -5
  95. data/prompt_manager_logo.png +0 -0
@@ -0,0 +1,108 @@
1
+ # Shell Expansion
2
+
3
+ Shell expansion replaces environment variable references and command substitutions with their values at parse time.
4
+
5
+ ## Syntax
6
+
7
+ | Pattern | Description | Example |
8
+ |---------|-------------|---------|
9
+ | `$VAR` | Environment variable | `$USER` |
10
+ | `${VAR}` | Environment variable (braced) | `${HOME}` |
11
+ | `$(command)` | Command substitution | `$(date +%Y-%m-%d)` |
12
+
13
+ ## Environment Variables
14
+
15
+ ```markdown
16
+ ---
17
+ title: Info
18
+ ---
19
+ Current user: $USER
20
+ Home directory: ${HOME}
21
+ ```
22
+
23
+ After parsing:
24
+
25
+ ```ruby
26
+ parsed = PM.parse('info.md')
27
+ parsed.content
28
+ #=> "Current user: dewayne\nHome directory: /Users/dewayne\n"
29
+ ```
30
+
31
+ Only UPPERCASE variable names are expanded. Lowercase names like `$foo` are left as-is.
32
+
33
+ Missing environment variables are replaced with an empty string.
34
+
35
+ ## Command Substitution
36
+
37
+ ```markdown
38
+ ---
39
+ title: Context
40
+ ---
41
+ Date: $(date +%Y-%m-%d)
42
+ Branch: $(git rev-parse --abbrev-ref HEAD)
43
+ ```
44
+
45
+ Commands are executed via the shell and replaced with their stdout (trailing newline stripped).
46
+
47
+ ### Nested Commands
48
+
49
+ Nested parentheses are handled correctly:
50
+
51
+ ```markdown
52
+ $(echo $(echo hello))
53
+ ```
54
+
55
+ ### Failed Commands
56
+
57
+ A command that exits with a non-zero status raises `RuntimeError`:
58
+
59
+ ```ruby
60
+ PM.parse("Output: $(exit 1)")
61
+ #=> RuntimeError: Command failed with exit status 1: exit 1
62
+ ```
63
+
64
+ ## Direct Access
65
+
66
+ Shell expansion is available as a standalone method:
67
+
68
+ ```ruby
69
+ PM.expand_shell("Hello $USER from $(hostname)")
70
+ #=> "Hello dewayne from venus"
71
+ ```
72
+
73
+ ## Disabling Shell Expansion
74
+
75
+ Set `shell: false` in the file's YAML metadata:
76
+
77
+ ```markdown
78
+ ---
79
+ title: Raw
80
+ shell: false
81
+ ---
82
+ This $USER reference is preserved as-is.
83
+ ```
84
+
85
+ Or disable globally:
86
+
87
+ ```ruby
88
+ PM.configure { |c| c.shell = false }
89
+ ```
90
+
91
+ Per-file metadata always overrides the global setting. A file with `shell: true` gets shell expansion even when the global default is `false`.
92
+
93
+ ## Interaction with ERB
94
+
95
+ Shell expansion happens at parse time (step 3 of the pipeline). ERB rendering happens later when `to_s` is called (step 4). This means shell references in the raw template are expanded before ERB sees the content.
96
+
97
+ If you need dynamic shell output at render time, use a custom directive instead:
98
+
99
+ ```ruby
100
+ PM.register(:sh) { |_ctx, cmd| `#{cmd}`.chomp }
101
+ ```
102
+
103
+ ```markdown
104
+ ---
105
+ title: Dynamic
106
+ ---
107
+ Branch: <%= sh 'git rev-parse --abbrev-ref HEAD' %>
108
+ ```
data/docs/index.md CHANGED
@@ -1,230 +1,70 @@
1
- # PromptManager Documentation
2
-
3
- <div align="center" style="background-color: #fff3cd; color: #856404; padding: 20px; margin: 20px 0; border: 2px solid #ffeaa7; border-radius: 5px; font-size: 18px; font-weight: bold;">
4
- ⚠️ CAUTION ⚠️<br />
5
- Breaking Changes are Coming<br />
6
- See <a href="development/roadmap/">Roadmap</a> for details
7
- </div>
8
-
9
- <div align="center">
10
- <table>
11
- <tr>
12
- <td width="40%" align="center" valign="top">
13
- <img src="../prompt_manager_logo.png" alt="PromptManager - The Enchanted Librarian of AI Prompts" width="400">
14
- </td>
15
- <td width="60%" align="left" valign="top">
16
- <p><strong>Like an enchanted librarian organizing floating books of knowledge, PromptManager helps you masterfully orchestrate and organize your AI prompts through wisdom and experience.</strong></p>
17
-
18
- <p>Each prompt becomes a living entity that can be categorized, parameterized, and interconnected with golden threads of relationships.</p>
19
-
20
- <h3>Key Features</h3>
21
- <ul>
22
- <li><strong>📚 <a href="storage/overview/">Multiple Storage Adapters</a></strong></li>
23
- <li><strong>🔧 <a href="core-features/parameterized-prompts/">Parameterized Prompts</a></strong></li>
24
- <li><strong>📋 <a href="core-features/directive-processing/">Directive Processing</a></strong></li>
25
- <li><strong>🎨 <a href="core-features/erb-integration/">ERB Integration</a></strong></li>
26
- <li><strong>🌍 <a href="core-features/shell-integration/">Shell Integration</a></strong></li>
27
- <li><strong>📖 <a href="core-features/comments/">Inline Documentation</a></strong></li>
28
- <li><strong>📊 <a href="core-features/parameter-history/">Parameter History</a></strong></li>
29
- <li><strong>⚡ <a href="core-features/error-handling/">Error Handling</a></strong></li>
30
- <li><strong>🔌 <a href="advanced/custom-keywords/">Extensible Architecture</a></strong></li>
31
- </ul>
32
- </td>
33
- </tr>
34
- </table>
35
- </div>
36
-
37
- ## What is PromptManager?
38
-
39
- PromptManager is a Ruby gem designed for managing parameterized prompts used in generative AI applications. It provides a sophisticated system for organizing, templating, and processing prompts with support for multiple storage backends, directive processing, and advanced templating features.
40
-
41
- Think of it as your personal AI prompt librarian - organizing your prompts, managing their parameters, processing their directives, and ensuring they're always ready when you need them.
42
-
43
- ## Quick Start
44
-
45
- Get up and running with PromptManager in minutes:
46
-
47
- === "Installation"
48
-
49
- ```bash
50
- gem install prompt_manager
51
- # or add to Gemfile
52
- bundle add prompt_manager
53
- ```
54
-
55
- === "Basic Usage"
56
-
57
- ```ruby
58
- require 'prompt_manager'
59
-
60
- # Configure storage
61
- PromptManager::Prompt.storage_adapter =
62
- PromptManager::Storage::FileSystemAdapter.config do |config|
63
- config.prompts_dir = '~/.prompts'
64
- end.new
65
-
66
- # Use a prompt
67
- prompt = PromptManager::Prompt.new(id: 'greeting')
68
- prompt.parameters = {
69
- "[NAME]" => "Alice",
70
- "[LANGUAGE]" => "English"
71
- }
72
-
73
- puts prompt.to_s
74
- ```
75
-
76
- === "Create a Prompt"
77
-
78
- ```text
79
- # ~/.prompts/greeting.txt
80
- # Description: A friendly greeting prompt
81
-
82
- Hello [NAME]! How can I assist you today?
83
- Please respond in [LANGUAGE].
84
- ```
85
-
86
- ## Architecture Overview
87
-
88
- PromptManager follows a modular architecture designed for flexibility and extensibility:
1
+ # PM (PromptManager)
2
+
3
+ <table>
4
+ <tr>
5
+ <td width="40%" align="center" valign="top">
6
+ <img src="assets/images/prompt_manager.gif" alt="PromptManager" width="100%"><br>
7
+ <em>"Prompts with superpowers"</em>
8
+ </td>
9
+ <td width="60%" valign="top">
10
+ <strong>Parse YAML metadata from markdown, expand shell references, and render ERB templates on demand</strong><br><br>
11
+ PM (PromptManager) treats prompt files as composable, parameterized templates. Write prompts in markdown with YAML front matter, shell references, and ERB — PM handles the rest.
12
+
13
+ <h3>Key Features</h3>
14
+
15
+ <li> <strong>YAML Metadata</strong> - Parse from markdown strings or files<br>
16
+ <li> <strong>Shell Expansion</strong> - $VAR, ${VAR}, and $(command) substitution<br>
17
+ <li> <strong>ERB Rendering</strong> - On-demand rendering with named parameters<br>
18
+ <li> <strong>File Includes</strong> - Compose prompts from multiple files<br>
19
+ <li> <strong>Custom Directives</strong> - Register custom methods for ERB templates<br>
20
+ <li> <strong>Configurable Pipeline</strong> - Enable/disable stages per prompt or globally<br>
21
+ <li> <strong>Comment Stripping</strong> - HTML comments removed before processing
22
+ </td>
23
+ </tr>
24
+ </table>
25
+
26
+ ## Processing Pipeline
27
+
28
+ Every prompt passes through four stages:
89
29
 
90
30
  ```mermaid
91
- graph TB
92
- subgraph "Application Layer"
93
- A[Your Application]
94
- P[PromptManager::Prompt]
95
- end
96
-
97
- subgraph "Processing Layer"
98
- D[Directive Processor]
99
- E[ERB Engine]
100
- K[Keyword Substitution]
101
- end
102
-
103
- subgraph "Storage Layer"
104
- FS[FileSystem Adapter]
105
- AR[ActiveRecord Adapter]
106
- CA[Custom Adapter]
107
- end
108
-
109
- A --> P
110
- P --> D
111
- P --> E
112
- P --> K
113
- P --> FS
114
- P --> AR
115
- P --> CA
116
-
117
- D --> |includes| FS
118
- E --> |templates| P
119
- K --> |substitutes| P
31
+ graph LR
32
+ A[Strip HTML Comments] --> B[Extract YAML Metadata]
33
+ B --> C[Shell Expansion]
34
+ C --> D["ERB Rendering (on to_s)"]
120
35
  ```
121
36
 
122
- ## Core Concepts
37
+ 1. **Strip HTML comments** -- `<!-- ... -->` removed before anything else
38
+ 2. **Extract YAML metadata** -- Front-matter between `---` fences parsed into `PM::Metadata`
39
+ 3. **Shell expansion** -- Environment variables and commands expanded (when `shell: true`)
40
+ 4. **ERB rendering** -- Templates evaluated on demand when `to_s` is called (when `erb: true`)
123
41
 
124
- ### Parameterized Prompts
42
+ ## Quick Example
125
43
 
126
- Transform static prompts into dynamic templates:
44
+ Given a file `review.md`:
127
45
 
128
- ```ruby
129
- # Template with parameters
130
- prompt_text = "Translate '[TEXT]' from [SOURCE_LANG] to [TARGET_LANG]"
131
-
132
- # Filled with values
133
- prompt.parameters = {
134
- "[TEXT]" => "Hello world",
135
- "[SOURCE_LANG]" => "English",
136
- "[TARGET_LANG]" => "Spanish"
137
- }
46
+ ```markdown
47
+ ---
48
+ title: Code Review
49
+ parameters:
50
+ language: ruby
51
+ code: null
52
+ ---
53
+ Review the following <%= language %> code:
138
54
 
139
- # Result: "Translate 'Hello world' from English to Spanish"
55
+ <%= code %>
140
56
  ```
141
57
 
142
- ### Directive Processing
143
-
144
- Use JCL-style directives for prompt composition:
145
-
146
- ```text
147
- # Common header for all customer service prompts
148
- //include common/customer_service_header.txt
58
+ Parse and render:
149
59
 
150
- # Dynamic template inclusion
151
- //include templates/[TEMPLATE_TYPE].txt
60
+ ```ruby
61
+ require 'pm'
152
62
 
153
- Handle this customer inquiry about [TOPIC].
63
+ parsed = PM.parse('review.md')
64
+ puts parsed.metadata.title #=> "Code Review"
65
+ puts parsed.to_s('code' => source) #=> rendered prompt
154
66
  ```
155
67
 
156
- ### Storage Adapters
157
-
158
- Choose your preferred storage backend:
159
-
160
- - **FileSystemAdapter**: Store prompts as text files
161
- - **ActiveRecordAdapter**: Store prompts in a database
162
- - **Custom Adapters**: Build your own storage solution
163
-
164
- ## Why PromptManager?
165
-
166
- ### 🎯 **Organized Prompts**
167
- Keep your prompts organized in a structured, searchable format instead of scattered across your codebase.
168
-
169
- ### 🔄 **Reusable Templates**
170
- Create parameterized templates that can be reused across different contexts and applications.
171
-
172
- ### 🛠️ **Powerful Processing**
173
- Advanced features like directive processing, ERB templating, and environment variable substitution.
174
-
175
- ### 📈 **Scalable Architecture**
176
- Modular design supports everything from simple scripts to enterprise applications.
177
-
178
- ### 🔍 **Easy Management**
179
- Built-in search capabilities and parameter history make prompt management effortless.
180
-
181
68
  ## Getting Started
182
69
 
183
- Ready to dive in? Here are some great places to start:
184
-
185
- <div class="grid cards" markdown>
186
-
187
- - :fontawesome-solid-rocket:{ .lg .middle } __Quick Start__
188
-
189
- ---
190
-
191
- Get PromptManager running in your project within minutes
192
-
193
- [:octicons-arrow-right-24: Quick Start](getting-started/quick-start.md)
194
-
195
- - :fontawesome-solid-book:{ .lg .middle } __Core Features__
196
-
197
- ---
198
-
199
- Learn about parameterized prompts, directives, and more
200
-
201
- [:octicons-arrow-right-24: Core Features](core-features/parameterized-prompts.md)
202
-
203
- - :fontawesome-solid-database:{ .lg .middle } __Storage Adapters__
204
-
205
- ---
206
-
207
- Choose the right storage solution for your needs
208
-
209
- [:octicons-arrow-right-24: Storage Options](storage/overview.md)
210
-
211
- - :fontawesome-solid-code:{ .lg .middle } __API Reference__
212
-
213
- ---
214
-
215
- Complete reference for all classes and methods
216
-
217
- [:octicons-arrow-right-24: API Docs](api/prompt-class.md)
218
-
219
- </div>
220
-
221
- ## Community & Support
222
-
223
- - **GitHub**: [MadBomber/prompt_manager](https://github.com/MadBomber/prompt_manager)
224
- - **RubyGems**: [prompt_manager](https://rubygems.org/gems/prompt_manager)
225
- - **Issues**: [Report bugs or request features](https://github.com/MadBomber/prompt_manager/issues)
226
- - **Discussions**: [Community discussions](https://github.com/MadBomber/prompt_manager/discussions)
227
-
228
- ## License
229
-
230
- PromptManager is released under the [MIT License](https://opensource.org/licenses/MIT).
70
+ Head to [Installation](getting-started/installation.md) to add PM to your project, then follow the [Quick Start](getting-started/quick-start.md) guide.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PM
4
+ class Configuration
5
+ attr_accessor :prompts_dir, :shell, :erb
6
+
7
+ def initialize
8
+ reset!
9
+ end
10
+
11
+ def reset!
12
+ @prompts_dir = ''
13
+ @shell = true
14
+ @erb = true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PM
4
+ # --- Directive registry ---
5
+
6
+ @directives = {}
7
+
8
+ # Registers one or more named directives available in ERB templates.
9
+ # The block receives a RenderContext as its first argument,
10
+ # followed by any arguments from the ERB call.
11
+ # Multiple names register the same block under each name (aliases).
12
+ # Raises RuntimeError if any name is already registered.
13
+ def self.register(*names, &block)
14
+ names.each do |name|
15
+ name = name.to_sym
16
+ if @directives.key?(name)
17
+ raise "Directive already registered: #{name}"
18
+ end
19
+ @directives[name] = block
20
+ end
21
+ end
22
+
23
+ # Returns the registered directives hash.
24
+ def self.directives
25
+ @directives
26
+ end
27
+
28
+ # Clears all directives and re-registers the built-ins.
29
+ def self.reset_directives!
30
+ @directives.clear
31
+ register_builtins
32
+ end
33
+
34
+ # --- Built-in directives ---
35
+
36
+ def self.register_builtins
37
+ register(:include) do |ctx, path|
38
+ unless ctx.directory
39
+ raise 'include requires a file context (use PM.parse with a file path)'
40
+ end
41
+ full_path = File.expand_path(path, ctx.directory)
42
+ if ctx.included.include?(full_path)
43
+ raise "Circular include detected: #{full_path}"
44
+ end
45
+ child = PM.parse(full_path)
46
+ result = child.render_with(ctx.params, ctx.included, ctx.depth + 1)
47
+
48
+ ctx.metadata.includes << {
49
+ path: full_path,
50
+ depth: ctx.depth + 1,
51
+ metadata: child.metadata.to_h.reject { |k, _| k == :includes },
52
+ includes: child.metadata.includes
53
+ }
54
+
55
+ result
56
+ end
57
+ end
58
+ private_class_method :register_builtins
59
+
60
+ register_builtins
61
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module PM
6
+ # OpenStruct-based metadata with predicate methods for boolean keys.
7
+ class Metadata < OpenStruct
8
+ def initialize(hash = {})
9
+ super(hash)
10
+ hash.each do |key, value|
11
+ if value.is_a?(TrueClass) || value.is_a?(FalseClass)
12
+ define_singleton_method(:"#{key}?") { send(key) }
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/pm/parsed.rb ADDED
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'ostruct'
5
+ require 'erb'
6
+
7
+ module PM
8
+ # Render context passed to registered directives.
9
+ RenderContext = Struct.new(:directory, :params, :included, :depth, :metadata, keyword_init: true)
10
+
11
+ Parsed = Struct.new(:metadata, :content, keyword_init: true) do
12
+ def [](key)
13
+ metadata[key]
14
+ end
15
+
16
+ # Returns the prompt content with ERB tags expanded.
17
+ # Registered directives are available as methods in the ERB binding.
18
+ # Raises if any required parameters (default: null) are not provided.
19
+ # When metadata has erb: false, returns content without ERB processing.
20
+ def to_s(values = {})
21
+ render_with(values, Set.new, 0)
22
+ end
23
+
24
+ def render_with(values, included, depth)
25
+ metadata.includes = []
26
+ return content unless metadata.erb?
27
+
28
+ defaults = metadata.parameters || {}
29
+ params = defaults.merge(values.transform_keys(&:to_s))
30
+
31
+ missing = params.select { |_, v| v.nil? }.keys
32
+ unless missing.empty?
33
+ raise ArgumentError, "Missing required parameters: #{missing.join(', ')}"
34
+ end
35
+
36
+ if metadata.directory && metadata.name
37
+ included.add(File.join(metadata.directory, metadata.name))
38
+ end
39
+
40
+ context = OpenStruct.new(params)
41
+
42
+ render_ctx = PM::RenderContext.new(
43
+ directory: metadata.directory,
44
+ params: params,
45
+ included: included,
46
+ depth: depth,
47
+ metadata: metadata
48
+ )
49
+
50
+ PM.directives.each do |name, block|
51
+ context.define_singleton_method(name) do |*args|
52
+ block.call(render_ctx, *args)
53
+ end
54
+ end
55
+
56
+ ERB.new(content).result(context.instance_eval { binding })
57
+ end
58
+ end
59
+ end
data/lib/pm/shell.rb ADDED
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module PM
6
+ # Expands shell references in a string.
7
+ # $ENVAR and ${ENVAR} are replaced with the environment variable value.
8
+ # $(command) is executed and replaced with its stdout.
9
+ def self.expand_shell(string)
10
+ result = expand_commands(string)
11
+ expand_env_vars(result)
12
+ end
13
+
14
+ # Replaces $(command) with the command's stdout.
15
+ # Handles nested parentheses in commands.
16
+ def self.expand_commands(string)
17
+ result = string.dup
18
+ pos = 0
19
+
20
+ while (start = result.index(COMMAND_START, pos))
21
+ depth = 0
22
+ i = start + 1
23
+
24
+ while i < result.length
25
+ if result[i] == '('
26
+ depth += 1
27
+ elsif result[i] == ')'
28
+ depth -= 1
29
+ if depth == 0
30
+ command = result[(start + 2)...i]
31
+ output, status = Open3.capture2(command)
32
+ output = output.chomp
33
+ unless status.success?
34
+ raise "Shell command failed (exit #{status.exitstatus}): #{command}"
35
+ end
36
+ result[start..i] = output
37
+ pos = start + output.length
38
+ break
39
+ end
40
+ end
41
+ i += 1
42
+ end
43
+
44
+ break if depth != 0
45
+ end
46
+
47
+ result
48
+ end
49
+ private_class_method :expand_commands
50
+
51
+ # Replaces $ENVAR and ${ENVAR} with environment variable values.
52
+ # Missing variables are replaced with an empty string.
53
+ def self.expand_env_vars(string)
54
+ string.gsub(ENV_VAR_REGEXP) { ENV.fetch($1 || $2, '') }
55
+ end
56
+ private_class_method :expand_env_vars
57
+ end
data/lib/pm/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PM
4
+ VERSION = '1.0.0'
5
+ end