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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +206 -516
- data/Rakefile +0 -8
- data/docs/api/configuration.md +31 -327
- data/docs/api/constants.md +60 -0
- data/docs/api/index.md +14 -0
- data/docs/api/metadata.md +99 -0
- data/docs/api/parsed.md +98 -0
- data/docs/api/pm-module.md +131 -0
- data/docs/api/render-context.md +51 -0
- data/docs/architecture/design-decisions.md +70 -0
- data/docs/architecture/index.md +6 -0
- data/docs/architecture/processing-pipeline.md +112 -0
- data/docs/assets/css/custom.css +1 -0
- data/docs/assets/images/prompt_manager.gif +0 -0
- data/docs/assets/images/prompt_manager.mp4 +0 -0
- data/docs/examples/ai-agent-prompts.md +173 -0
- data/docs/examples/code-review-prompt.md +107 -0
- data/docs/examples/index.md +7 -0
- data/docs/examples/multi-file-composition.md +123 -0
- data/docs/getting-started/configuration.md +106 -0
- data/docs/getting-started/index.md +7 -0
- data/docs/getting-started/installation.md +10 -73
- data/docs/getting-started/quick-start.md +50 -225
- data/docs/guides/comment-stripping.md +64 -0
- data/docs/guides/custom-directives.md +115 -0
- data/docs/guides/erb-rendering.md +102 -0
- data/docs/guides/includes.md +146 -0
- data/docs/guides/index.md +11 -0
- data/docs/guides/parameters.md +96 -0
- data/docs/guides/parsing.md +127 -0
- data/docs/guides/shell-expansion.md +108 -0
- data/docs/index.md +54 -214
- data/lib/pm/configuration.rb +17 -0
- data/lib/pm/directives.rb +61 -0
- data/lib/pm/metadata.rb +17 -0
- data/lib/pm/parsed.rb +59 -0
- data/lib/pm/shell.rb +57 -0
- data/lib/pm/version.rb +5 -0
- data/lib/pm.rb +121 -0
- data/lib/prompt_manager.rb +2 -27
- data/mkdocs.yml +101 -66
- metadata +42 -101
- data/docs/.keep +0 -0
- data/docs/advanced/custom-keywords.md +0 -421
- data/docs/advanced/dynamic-directives.md +0 -535
- data/docs/advanced/performance.md +0 -612
- data/docs/advanced/search-integration.md +0 -635
- data/docs/api/directive-processor.md +0 -431
- data/docs/api/prompt-class.md +0 -354
- data/docs/api/storage-adapters.md +0 -462
- data/docs/assets/favicon.ico +0 -1
- data/docs/assets/logo.svg +0 -24
- data/docs/core-features/comments.md +0 -48
- data/docs/core-features/directive-processing.md +0 -38
- data/docs/core-features/erb-integration.md +0 -68
- data/docs/core-features/error-handling.md +0 -197
- data/docs/core-features/parameter-history.md +0 -76
- data/docs/core-features/parameterized-prompts.md +0 -500
- data/docs/core-features/shell-integration.md +0 -79
- data/docs/development/architecture.md +0 -544
- data/docs/development/contributing.md +0 -425
- data/docs/development/roadmap.md +0 -234
- data/docs/development/testing.md +0 -822
- data/docs/examples/advanced.md +0 -523
- data/docs/examples/basic.md +0 -688
- data/docs/examples/real-world.md +0 -776
- data/docs/examples.md +0 -337
- data/docs/getting-started/basic-concepts.md +0 -318
- data/docs/migration/v0.9.0.md +0 -459
- data/docs/migration/v1.0.0.md +0 -591
- data/docs/storage/activerecord-adapter.md +0 -348
- data/docs/storage/custom-adapters.md +0 -176
- data/docs/storage/filesystem-adapter.md +0 -236
- data/docs/storage/overview.md +0 -427
- data/examples/advanced_integrations.rb +0 -52
- data/examples/directives.rb +0 -102
- data/examples/prompts_dir/advanced_demo.txt +0 -79
- data/examples/prompts_dir/directive_example.json +0 -1
- data/examples/prompts_dir/directive_example.txt +0 -8
- data/examples/prompts_dir/todo.json +0 -1
- data/examples/prompts_dir/todo.txt +0 -7
- data/examples/prompts_dir/toy/8-ball.txt +0 -4
- data/examples/rgfzf +0 -44
- data/examples/simple.rb +0 -160
- data/examples/using_search_proc.rb +0 -68
- data/improvement_plan.md +0 -996
- data/lib/prompt_manager/directive_processor.rb +0 -47
- data/lib/prompt_manager/prompt.rb +0 -195
- data/lib/prompt_manager/storage/active_record_adapter.rb +0 -157
- data/lib/prompt_manager/storage/file_system_adapter.rb +0 -339
- data/lib/prompt_manager/storage.rb +0 -34
- data/lib/prompt_manager/version.rb +0 -5
- 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
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
</
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
+
## Quick Example
|
|
125
43
|
|
|
126
|
-
|
|
44
|
+
Given a file `review.md`:
|
|
127
45
|
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
55
|
+
<%= code %>
|
|
140
56
|
```
|
|
141
57
|
|
|
142
|
-
|
|
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
|
-
|
|
151
|
-
|
|
60
|
+
```ruby
|
|
61
|
+
require 'pm'
|
|
152
62
|
|
|
153
|
-
|
|
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
|
-
|
|
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,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
|
data/lib/pm/metadata.rb
ADDED
|
@@ -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
|