prompt_manager 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -1
- data/README.md +78 -20
- data/docs/api/pm-module.md +38 -0
- data/docs/getting-started/quick-start.md +13 -0
- data/docs/guides/custom-directives.md +67 -2
- data/docs/guides/includes.md +38 -0
- data/docs/guides/parsing.md +24 -0
- data/lib/pm/core_directives.rb +51 -0
- data/lib/pm/directive.rb +144 -0
- data/lib/pm/directives.rb +2 -30
- data/lib/pm/version.rb +1 -1
- data/lib/pm.rb +6 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f2de9f9b60b1bad9884fba07f280c00316c05dbd8d5f6b86e01805e461aaf968
|
|
4
|
+
data.tar.gz: 73162b249b9676d13bdf015a7cba72949d3fe9834ff6b6441fbe452048613e04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb50e40c8eeb0873a68b1d84481391a1fa7b0c8f1971a461b662b86faa101bd3fd58a584f4364f2510e9df282904e3329f64b6319a16a684020b574f3aa0f1a1
|
|
7
|
+
data.tar.gz: 20f9e743881dca244716a5e14e97a34a050e07afb6ac954122988acf0cd7fd3b9bdf20c726ab73885c1a10301cc5ccdd194bcc38c50611340b6f77c06e87d2ab
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
### [1.0.2] - 2026-02-09
|
|
2
|
+
|
|
3
|
+
#### Changed
|
|
4
|
+
- Made `PM.parse_string` a public class method so it can be called directly for parsing strings without file-based metadata enrichment.
|
|
5
|
+
|
|
6
|
+
### [1.0.1] - 2026-02-04
|
|
7
|
+
|
|
8
|
+
#### Added
|
|
9
|
+
- **`PM::Directive` base class** — class-based DSL for defining directive categories. Use `desc` before method definitions to mark them as directives, and `alias_method` for aliases. Subclass tracking, `register_all`, `category_name`, and `build_dispatch_block` are all built-in.
|
|
10
|
+
- **Built-in `insert` directive** (alias: `read`) — insert any file's raw content into a prompt. Unlike `include`, the inserted content is not parsed, shell-expanded, or ERB-rendered.
|
|
11
|
+
- New test file `directive_base_test.rb` with tests for the `PM::Directive` DSL, subclass tracking, category naming, and dispatch block building.
|
|
12
|
+
- New test fixtures for `insert`/`read` directive coverage.
|
|
13
|
+
|
|
14
|
+
#### Changed
|
|
15
|
+
- **`PM::CoreDirectives`** — built-in `include`, `insert`/`read` directives refactored as a `PM::Directive` subclass using the `desc` DSL.
|
|
16
|
+
- `PM.register` directive aliases now support registration via `alias_method` in `PM::Directive` subclasses, with automatic detection via `UnboundMethod#original_name`.
|
|
17
|
+
- Directive registry simplified; `PM.reset_directives!` now delegates to `PM::Directive.register_all`.
|
|
18
|
+
- Updated README and documentation guides for custom directives and includes.
|
|
2
19
|
|
|
3
20
|
## Released
|
|
4
21
|
### [1.0.0] = 2026-02-03
|
data/README.md
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
- <strong>YAML Metadata</strong> - Parse from markdown strings or files<br>
|
|
15
15
|
- <strong>Conditional Shell Expansion</strong> - $ENVAR, ${ENVAR}, $(command) substitution<br>
|
|
16
16
|
- <strong>Conditional ERB Templates</strong> - On-demand rendering with named parameters<br>
|
|
17
|
-
- <strong>
|
|
17
|
+
- <strong>Recursive Include System</strong> - Compose prompts from multiple files<br>
|
|
18
|
+
- <strong>Raw File Insert</strong> - Insert any file's content verbatim<br>
|
|
18
19
|
- <strong>Custom Directives</strong> - Register custom methods for ERB templates<br>
|
|
19
20
|
- <strong>Configurable Pipeline</strong> - Enable/disable stages per prompt or globally<br>
|
|
20
21
|
- <strong>Comment Stripping</strong> - HTML comments removed before processing
|
|
@@ -94,6 +95,14 @@ parsed = PM.parse("---\ntitle: Hello\n---\nContent here")
|
|
|
94
95
|
|
|
95
96
|
When given a file path, `parse` adds `directory`, `name`, `created_at`, and `modified_at` to the metadata. Both forms run the full processing pipeline.
|
|
96
97
|
|
|
98
|
+
Note that `PM.parse` treats single words (e.g. `'hello'`) as prompt IDs and appends `.md` to look them up as files. To parse a single word as literal string content, use `PM.parse_string`:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
PM.parse('hello') #=> looks for hello.md in prompts_dir
|
|
102
|
+
PM.parse_string('hello') #=> parses "hello" as string content
|
|
103
|
+
PM.parse_string('Any string') #=> always parsed as content, never as a file
|
|
104
|
+
```
|
|
105
|
+
|
|
97
106
|
Given a file `code_review.md`:
|
|
98
107
|
|
|
99
108
|
```md
|
|
@@ -190,6 +199,37 @@ Included files go through the full processing pipeline (comment stripping, metad
|
|
|
190
199
|
|
|
191
200
|
Nested includes work — A can include B which includes C. Circular includes raise an error.
|
|
192
201
|
|
|
202
|
+
### Inserting raw file content
|
|
203
|
+
|
|
204
|
+
Use `insert` (or its alias `read`) to insert any file's content verbatim. Unlike `include`, the inserted content is not parsed, shell-expanded, or ERB-rendered — it appears as-is:
|
|
205
|
+
|
|
206
|
+
```md
|
|
207
|
+
---
|
|
208
|
+
title: Code Review
|
|
209
|
+
parameters:
|
|
210
|
+
feedback_style: null
|
|
211
|
+
---
|
|
212
|
+
Review this Ruby code:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
<%= insert 'app/models/user.rb' %>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Use a <%= feedback_style %> tone.
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Paths are resolved relative to the parent file's directory (same as `include`). Absolute paths work from any context. Missing files raise an error.
|
|
222
|
+
|
|
223
|
+
`insert` vs `include`:
|
|
224
|
+
|
|
225
|
+
| | `insert` | `include` |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| File types | Any | `.md` only |
|
|
228
|
+
| ERB in content | Preserved as literal text | Rendered |
|
|
229
|
+
| Shell expansion | Not applied | Applied |
|
|
230
|
+
| Recursion | None | Nested includes supported |
|
|
231
|
+
| Metadata tracking | None | `metadata.includes` tree |
|
|
232
|
+
|
|
193
233
|
After calling `to_s`, the parent's metadata has an `includes` key with a tree of what was included:
|
|
194
234
|
|
|
195
235
|
```ruby
|
|
@@ -215,10 +255,11 @@ parsed.metadata.includes
|
|
|
215
255
|
|
|
216
256
|
### Custom directives
|
|
217
257
|
|
|
258
|
+
#### Block-based registration
|
|
259
|
+
|
|
218
260
|
Register custom methods available in ERB templates:
|
|
219
261
|
|
|
220
262
|
```ruby
|
|
221
|
-
PM.register(:read) { |_ctx, path| File.read(path) }
|
|
222
263
|
PM.register(:env) { |_ctx, key| ENV.fetch(key, '') }
|
|
223
264
|
PM.register(:run) { |_ctx, cmd| `#{cmd}`.chomp }
|
|
224
265
|
```
|
|
@@ -229,26 +270,45 @@ Register multiple names for the same directive (aliases):
|
|
|
229
270
|
PM.register(:webpage, :website, :web) { |_ctx, url| fetch_page(url) }
|
|
230
271
|
```
|
|
231
272
|
|
|
232
|
-
|
|
273
|
+
#### Class-based directives
|
|
274
|
+
|
|
275
|
+
For organized groups of directives, subclass `PM::Directive`:
|
|
233
276
|
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
277
|
+
```ruby
|
|
278
|
+
class MyDirectives < PM::Directive
|
|
279
|
+
desc "Fetch environment variable"
|
|
280
|
+
def env(ctx, key)
|
|
281
|
+
ENV.fetch(key, '')
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
desc "Run a shell command"
|
|
285
|
+
def run(ctx, cmd)
|
|
286
|
+
`#{cmd}`.chomp
|
|
287
|
+
end
|
|
288
|
+
alias_method :exec, :run
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Register all directive subclasses with PM
|
|
292
|
+
PM::Directive.register_all
|
|
238
293
|
```
|
|
239
294
|
|
|
240
|
-
|
|
295
|
+
`desc` marks the next method as a directive. Methods without `desc` are helpers and won't be registered. `alias_method` aliases are detected automatically.
|
|
241
296
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
297
|
+
Override `build_dispatch_block` on your base class to customize how methods are called:
|
|
298
|
+
|
|
299
|
+
```ruby
|
|
300
|
+
class MyDirectives < PM::Directive
|
|
301
|
+
class << self
|
|
302
|
+
def build_dispatch_block(inst, method_name)
|
|
303
|
+
proc { |_ctx, *args| inst.send(method_name, args) }
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
249
307
|
```
|
|
250
308
|
|
|
251
|
-
|
|
309
|
+
#### RenderContext
|
|
310
|
+
|
|
311
|
+
The first argument to every directive block (or class method) is a `PM::RenderContext`:
|
|
252
312
|
|
|
253
313
|
- `ctx.directory` — directory of the file being rendered
|
|
254
314
|
- `ctx.params` — merged parameter values
|
|
@@ -256,10 +316,7 @@ The first argument to every directive block is a `PM::RenderContext` with access
|
|
|
256
316
|
- `ctx.depth` — include nesting depth
|
|
257
317
|
- `ctx.included` — Set of file paths already in the include chain
|
|
258
318
|
|
|
259
|
-
|
|
260
|
-
PM.register(:current_file) { |ctx| ctx.metadata.name || 'unknown' }
|
|
261
|
-
PM.register(:depth) { |ctx| ctx.depth.to_s }
|
|
262
|
-
```
|
|
319
|
+
#### Duplicate detection and reset
|
|
263
320
|
|
|
264
321
|
Registering a name that already exists raises an error:
|
|
265
322
|
|
|
@@ -272,6 +329,7 @@ Reset to built-in directives only:
|
|
|
272
329
|
|
|
273
330
|
```ruby
|
|
274
331
|
PM.reset_directives!
|
|
332
|
+
# Restores: include, insert, read
|
|
275
333
|
```
|
|
276
334
|
|
|
277
335
|
### Disabling processing stages
|
data/docs/api/pm-module.md
CHANGED
|
@@ -38,6 +38,44 @@ parsed = PM.parse("---\ntitle: Hello\n---\nContent")
|
|
|
38
38
|
|
|
39
39
|
---
|
|
40
40
|
|
|
41
|
+
### PM.parse_string(string) → Parsed
|
|
42
|
+
|
|
43
|
+
Parse a string directly through the processing pipeline, bypassing all file-detection logic. Unlike `PM.parse`, single words are never treated as prompt IDs.
|
|
44
|
+
|
|
45
|
+
**Parameters:**
|
|
46
|
+
|
|
47
|
+
| Name | Type | Description |
|
|
48
|
+
|------|------|-------------|
|
|
49
|
+
| `string` | String | Raw string content to parse |
|
|
50
|
+
|
|
51
|
+
**Returns:** `PM::Parsed` (Struct with `metadata` and `content`)
|
|
52
|
+
|
|
53
|
+
**Behavior:**
|
|
54
|
+
|
|
55
|
+
- Always parses the input as string content — never looks up files
|
|
56
|
+
- Runs the same pipeline as `PM.parse`: strip comments → extract YAML → shell expansion
|
|
57
|
+
- Does not add `directory`, `name`, `created_at`, or `modified_at` to metadata
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
# Single words are parsed as content, not treated as file basenames
|
|
61
|
+
parsed = PM.parse_string('hello')
|
|
62
|
+
parsed.content #=> "hello"
|
|
63
|
+
|
|
64
|
+
# Strings with YAML front-matter work as expected
|
|
65
|
+
parsed = PM.parse_string("---\ntitle: Hello\n---\nContent")
|
|
66
|
+
parsed.metadata.title #=> "Hello"
|
|
67
|
+
parsed.content #=> "Content\n"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**When to use `parse_string` instead of `parse`:**
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
PM.parse('summarize') #=> looks for summarize.md
|
|
74
|
+
PM.parse_string('summarize') #=> parses "summarize" as content
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
41
79
|
## Configuration
|
|
42
80
|
|
|
43
81
|
### PM.config → Configuration
|
|
@@ -40,6 +40,19 @@ puts parsed.to_s('name' => 'Alice')
|
|
|
40
40
|
|
|
41
41
|
When parsing a file, PM also adds `directory`, `name`, `created_at`, and `modified_at` to the metadata.
|
|
42
42
|
|
|
43
|
+
## Parse a String Directly
|
|
44
|
+
|
|
45
|
+
`PM.parse` treats single words as prompt IDs and looks them up as files. To always parse a string as content, use `PM.parse_string`:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
PM.parse('hello') #=> looks for hello.md in prompts_dir
|
|
49
|
+
PM.parse_string('hello') #=> parses "hello" as string content
|
|
50
|
+
|
|
51
|
+
parsed = PM.parse_string("---\ntitle: Inline\n---\nSome content")
|
|
52
|
+
parsed.metadata.title #=> "Inline"
|
|
53
|
+
parsed.content #=> "Some content\n"
|
|
54
|
+
```
|
|
55
|
+
|
|
43
56
|
## Parameters
|
|
44
57
|
|
|
45
58
|
Parameters declared in the YAML front-matter define template variables:
|
|
@@ -84,7 +84,7 @@ Remove all custom directives and restore only the built-ins:
|
|
|
84
84
|
PM.reset_directives!
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
After reset, only `include`
|
|
87
|
+
After reset, only the built-in directives are registered: `include`, `insert`, and `read`.
|
|
88
88
|
|
|
89
89
|
## Directives in Included Files
|
|
90
90
|
|
|
@@ -107,9 +107,74 @@ PM.register(:my_helper) { |_ctx| "works" }
|
|
|
107
107
|
PM.register('other_helper') { |_ctx| "also works" }
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
+
## Class-Based Directives
|
|
111
|
+
|
|
112
|
+
For organized groups of directives, subclass `PM::Directive`:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
class MyDirectives < PM::Directive
|
|
116
|
+
desc "Fetch environment variable"
|
|
117
|
+
def env(ctx, key)
|
|
118
|
+
ENV.fetch(key, '')
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
desc "Run a shell command"
|
|
122
|
+
def run(ctx, cmd)
|
|
123
|
+
`#{cmd}`.chomp
|
|
124
|
+
end
|
|
125
|
+
alias_method :exec, :run
|
|
126
|
+
|
|
127
|
+
# No desc — not registered as a directive
|
|
128
|
+
def helper
|
|
129
|
+
"internal"
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
PM::Directive.register_all
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`desc` marks the next method as a directive. Methods without `desc` are ordinary helpers and won't be registered. `alias_method` aliases are detected automatically via `UnboundMethod#original_name`.
|
|
137
|
+
|
|
138
|
+
### Category name
|
|
139
|
+
|
|
140
|
+
The class name determines a category heading (useful for help output):
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
MyDirectives.category_name #=> "My"
|
|
144
|
+
PM::CoreDirectives.category_name #=> "Core"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The last segment of the class name is used, with `Directives` stripped and camelCase split.
|
|
148
|
+
|
|
149
|
+
### Dispatch customization
|
|
150
|
+
|
|
151
|
+
Override `build_dispatch_block` to customize how methods are called when dispatched through `PM.directives`:
|
|
152
|
+
|
|
153
|
+
```ruby
|
|
154
|
+
class MyDirectives < PM::Directive
|
|
155
|
+
class << self
|
|
156
|
+
# Default: proc { |ctx, *args| inst.send(method_name, ctx, *args) }
|
|
157
|
+
def build_dispatch_block(inst, method_name)
|
|
158
|
+
proc { |_ctx, *args| inst.send(method_name, args.flatten) }
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Subclass tracking
|
|
165
|
+
|
|
166
|
+
All `PM::Directive` subclasses (direct and indirect) are tracked centrally:
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
PM::Directive.directive_subclasses
|
|
170
|
+
#=> [PM::CoreDirectives, MyDirectives, ...]
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`register_all` iterates this list, creates a singleton instance per subclass, and registers each described method with `PM.register`.
|
|
174
|
+
|
|
110
175
|
## Listing Directives
|
|
111
176
|
|
|
112
177
|
```ruby
|
|
113
178
|
PM.directives
|
|
114
|
-
#=> { include: #<Proc>, read: #<Proc>, ... }
|
|
179
|
+
#=> { include: #<Proc>, insert: #<Proc>, read: #<Proc>, ... }
|
|
115
180
|
```
|
data/docs/guides/includes.md
CHANGED
|
@@ -134,6 +134,44 @@ Nested includes form a tree -- each entry's `includes` array contains its own ch
|
|
|
134
134
|
|
|
135
135
|
The `includes` array is `nil` before `to_s` is called and is reset on each call.
|
|
136
136
|
|
|
137
|
+
## `insert` / `read` — Raw File Insertion
|
|
138
|
+
|
|
139
|
+
Use `insert` (or its alias `read`) to insert a file's content verbatim. Unlike `include`, the content is not parsed, shell-expanded, or ERB-rendered:
|
|
140
|
+
|
|
141
|
+
```markdown
|
|
142
|
+
---
|
|
143
|
+
title: Code Review
|
|
144
|
+
---
|
|
145
|
+
Review this code:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
<%= insert 'src/app.rb' %>
|
|
149
|
+
```
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Differences from `include`
|
|
153
|
+
|
|
154
|
+
| | `insert` | `include` |
|
|
155
|
+
|---|---|---|
|
|
156
|
+
| File types | Any | `.md` only |
|
|
157
|
+
| ERB in content | Preserved as literal text | Rendered |
|
|
158
|
+
| Shell expansion | Not applied | Applied |
|
|
159
|
+
| Recursion | None | Nested includes supported |
|
|
160
|
+
| Metadata tracking | None | `metadata.includes` tree |
|
|
161
|
+
|
|
162
|
+
### Path resolution
|
|
163
|
+
|
|
164
|
+
Same as `include` — paths resolve relative to the parent file's directory. Absolute paths work from any context, including string-parsed prompts.
|
|
165
|
+
|
|
166
|
+
### Missing files
|
|
167
|
+
|
|
168
|
+
Raises an error:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
PM.parse("---\n---\n<%= insert '/no/such/file' %>").to_s
|
|
172
|
+
#=> RuntimeError: insert: file not found: /no/such/file
|
|
173
|
+
```
|
|
174
|
+
|
|
137
175
|
## Requirements
|
|
138
176
|
|
|
139
177
|
- The `include` directive requires file context. Using it with string-parsed prompts raises an error:
|
data/docs/guides/parsing.md
CHANGED
|
@@ -69,6 +69,30 @@ String-parsed prompts do not have `directory`, `name`, `created_at`, or `modifie
|
|
|
69
69
|
!!! warning "Include limitations"
|
|
70
70
|
The `include` directive requires file context to resolve relative paths. It raises an error when used with string-parsed prompts.
|
|
71
71
|
|
|
72
|
+
### PM.parse_string vs PM.parse
|
|
73
|
+
|
|
74
|
+
Because `PM.parse` treats single words as prompt IDs (appending `.md` and looking them up as files), short strings like `"hello"` or `"summarize"` will trigger a file lookup instead of being parsed as content. Use `PM.parse_string` to always parse a string as content:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# PM.parse treats single words as prompt IDs
|
|
78
|
+
PM.parse('hello') #=> looks for hello.md in prompts_dir
|
|
79
|
+
PM.parse('summarize') #=> looks for summarize.md in prompts_dir
|
|
80
|
+
|
|
81
|
+
# PM.parse_string always parses as string content
|
|
82
|
+
PM.parse_string('hello') #=> Parsed with content "hello"
|
|
83
|
+
PM.parse_string('summarize') #=> Parsed with content "summarize"
|
|
84
|
+
|
|
85
|
+
# Multi-word strings without .md extension work the same in both
|
|
86
|
+
PM.parse("Hello world") #=> Parsed with content "Hello world"
|
|
87
|
+
PM.parse_string("Hello world") #=> Parsed with content "Hello world"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use `PM.parse_string` when:
|
|
91
|
+
|
|
92
|
+
- Your input may be a single word that should not be treated as a file
|
|
93
|
+
- You know the source is always a string, never a file path
|
|
94
|
+
- You want to skip the file-detection logic entirely
|
|
95
|
+
|
|
72
96
|
## Metadata Extraction
|
|
73
97
|
|
|
74
98
|
YAML front-matter is delimited by `---` fences:
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/pm/core_directives.rb
|
|
4
|
+
#
|
|
5
|
+
# PM's built-in directives: include, insert/read.
|
|
6
|
+
# Defined as a PM::Directive subclass using the desc/alias DSL.
|
|
7
|
+
|
|
8
|
+
module PM
|
|
9
|
+
class CoreDirectives < Directive
|
|
10
|
+
desc "Include and render another prompt file"
|
|
11
|
+
def include(ctx, path)
|
|
12
|
+
unless ctx.directory
|
|
13
|
+
raise 'include requires a file context (use PM.parse with a file path)'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
full_path = File.expand_path(path, ctx.directory)
|
|
17
|
+
|
|
18
|
+
if ctx.included.include?(full_path)
|
|
19
|
+
raise "Circular include detected: #{full_path}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
child = PM.parse(full_path)
|
|
23
|
+
result = child.render_with(ctx.params, ctx.included, ctx.depth + 1)
|
|
24
|
+
|
|
25
|
+
ctx.metadata.includes << {
|
|
26
|
+
path: full_path,
|
|
27
|
+
depth: ctx.depth + 1,
|
|
28
|
+
metadata: child.metadata.to_h.reject { |k, _| k == :includes },
|
|
29
|
+
includes: child.metadata.includes
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "Insert a file's raw content verbatim (no ERB, no shell expansion)"
|
|
36
|
+
def insert(ctx, path)
|
|
37
|
+
full_path = if ctx&.directory
|
|
38
|
+
File.expand_path(path, ctx.directory)
|
|
39
|
+
else
|
|
40
|
+
File.expand_path(path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
unless File.exist?(full_path)
|
|
44
|
+
raise "insert: file not found: #{full_path}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
File.read(full_path)
|
|
48
|
+
end
|
|
49
|
+
alias_method :read, :insert
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/pm/directive.rb
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/pm/directive.rb
|
|
4
|
+
#
|
|
5
|
+
# Base class for all directive categories.
|
|
6
|
+
#
|
|
7
|
+
# Subclass to define a category of directives. Use `desc` immediately
|
|
8
|
+
# before a method definition to mark it as a directive and provide its
|
|
9
|
+
# help description. Methods without a preceding `desc` are ordinary
|
|
10
|
+
# helpers and will not be registered.
|
|
11
|
+
#
|
|
12
|
+
# Use Ruby's own `alias_method` to create directive aliases;
|
|
13
|
+
# they are detected automatically via UnboundMethod#original_name.
|
|
14
|
+
#
|
|
15
|
+
# Example:
|
|
16
|
+
#
|
|
17
|
+
# class MyDirectives < PM::Directive
|
|
18
|
+
# desc "Greet someone"
|
|
19
|
+
# def greet(ctx, name)
|
|
20
|
+
# "Hello, #{name}!"
|
|
21
|
+
# end
|
|
22
|
+
# alias_method :hi, :greet
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
|
|
26
|
+
module PM
|
|
27
|
+
class Directive
|
|
28
|
+
# Centralized list of all subclasses across the hierarchy.
|
|
29
|
+
# Initialized here on PM::Directive; the inherited hook always
|
|
30
|
+
# appends to this specific array.
|
|
31
|
+
@directive_subclasses = []
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# ---- Subclass tracking ------------------------------------------------
|
|
35
|
+
# Every subclass (direct or indirect) is tracked in PM::Directive's
|
|
36
|
+
# centralized list so register_all can find them all.
|
|
37
|
+
|
|
38
|
+
def inherited(subclass)
|
|
39
|
+
PM::Directive.directive_subclasses << subclass
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the centralized subclass list. Only meaningful when called
|
|
44
|
+
# on PM::Directive itself; subclasses delegate here explicitly.
|
|
45
|
+
def directive_subclasses
|
|
46
|
+
if equal?(PM::Directive)
|
|
47
|
+
@directive_subclasses
|
|
48
|
+
else
|
|
49
|
+
PM::Directive.directive_subclasses
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# ---- Description helper -----------------------------------------------
|
|
54
|
+
# Call `desc "text"` on the line immediately before `def method_name`.
|
|
55
|
+
|
|
56
|
+
def desc(text)
|
|
57
|
+
@_pending_desc = text
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# ---- Automatic metadata capture via method_added ----------------------
|
|
61
|
+
|
|
62
|
+
def method_added(method_name)
|
|
63
|
+
if @_pending_desc
|
|
64
|
+
directive_descriptions[method_name] = @_pending_desc
|
|
65
|
+
@_pending_desc = nil
|
|
66
|
+
else
|
|
67
|
+
# Detect aliases created by alias_method.
|
|
68
|
+
# UnboundMethod#original_name returns the original method name;
|
|
69
|
+
# when it differs from method_name, this method is an alias.
|
|
70
|
+
begin
|
|
71
|
+
um = instance_method(method_name)
|
|
72
|
+
original = um.original_name
|
|
73
|
+
if original != method_name && directive_descriptions.key?(original)
|
|
74
|
+
(directive_aliases[original] ||= []) << method_name
|
|
75
|
+
end
|
|
76
|
+
rescue NameError
|
|
77
|
+
# ignore — method may reference undefined constants at load time
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
super
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Per-subclass metadata stores (instance variables on the class object).
|
|
84
|
+
|
|
85
|
+
def directive_descriptions
|
|
86
|
+
@directive_descriptions ||= {}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def directive_aliases
|
|
90
|
+
@directive_aliases ||= {}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ---- Category name derived from class name ----------------------------
|
|
94
|
+
# PM::CoreDirectives --> "Core"
|
|
95
|
+
# AIA::WebAndFileDirectives --> "Web and File"
|
|
96
|
+
|
|
97
|
+
def category_name
|
|
98
|
+
name.split('::').last
|
|
99
|
+
.sub(/Directives$/, '')
|
|
100
|
+
.gsub(/([a-z])([A-Z])/, '\1 \2')
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# ---- Singleton instance per subclass ----------------------------------
|
|
104
|
+
# Created by register_all, accessible for tests and state management.
|
|
105
|
+
|
|
106
|
+
def instance
|
|
107
|
+
@instance
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ---- Dispatch block builder -------------------------------------------
|
|
111
|
+
# Override in subclasses to customize how directive methods are called.
|
|
112
|
+
# The default passes (ctx, *args) straight through — suitable for
|
|
113
|
+
# PM::CoreDirectives whose methods use the RenderContext.
|
|
114
|
+
|
|
115
|
+
def build_dispatch_block(inst, method_name)
|
|
116
|
+
proc { |ctx, *args| inst.send(method_name, ctx, *args) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# ---- PM registration --------------------------------------------------
|
|
120
|
+
# Creates one instance per subclass and registers every described
|
|
121
|
+
# method (plus its aliases) with PM.
|
|
122
|
+
|
|
123
|
+
def register_all
|
|
124
|
+
PM::Directive.directive_subclasses.each do |klass|
|
|
125
|
+
next if klass.directive_descriptions.empty?
|
|
126
|
+
|
|
127
|
+
klass.instance_variable_set(:@instance, klass.new)
|
|
128
|
+
inst = klass.instance
|
|
129
|
+
|
|
130
|
+
klass.directive_descriptions.each_key do |method_name|
|
|
131
|
+
aliases = klass.directive_aliases[method_name] || []
|
|
132
|
+
names = [method_name, *aliases]
|
|
133
|
+
|
|
134
|
+
# Remove previously registered names (idempotent re-init)
|
|
135
|
+
names.each { |n| PM.directives.delete(n.to_sym) }
|
|
136
|
+
|
|
137
|
+
block = klass.build_dispatch_block(inst, method_name)
|
|
138
|
+
PM.register(*names, &block)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
data/lib/pm/directives.rb
CHANGED
|
@@ -25,37 +25,9 @@ module PM
|
|
|
25
25
|
@directives
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Clears all directives and re-registers
|
|
28
|
+
# Clears all directives and re-registers from PM::Directive subclasses.
|
|
29
29
|
def self.reset_directives!
|
|
30
30
|
@directives.clear
|
|
31
|
-
|
|
31
|
+
PM::Directive.register_all
|
|
32
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
33
|
end
|
data/lib/pm/version.rb
CHANGED
data/lib/pm.rb
CHANGED
|
@@ -98,7 +98,7 @@ module PM
|
|
|
98
98
|
|
|
99
99
|
Parsed.new(metadata: metadata, content: content)
|
|
100
100
|
end
|
|
101
|
-
|
|
101
|
+
|
|
102
102
|
|
|
103
103
|
# Builds a Metadata object with defaults for shell and erb.
|
|
104
104
|
def self.build_metadata(hash)
|
|
@@ -118,4 +118,9 @@ require_relative 'pm/version'
|
|
|
118
118
|
require_relative 'pm/metadata'
|
|
119
119
|
require_relative 'pm/parsed'
|
|
120
120
|
require_relative 'pm/directives'
|
|
121
|
+
require_relative 'pm/directive'
|
|
122
|
+
require_relative 'pm/core_directives'
|
|
121
123
|
require_relative 'pm/shell'
|
|
124
|
+
|
|
125
|
+
# Register built-in directives from PM::CoreDirectives
|
|
126
|
+
PM::Directive.register_all
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prompt_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -128,6 +128,8 @@ files:
|
|
|
128
128
|
- docs/index.md
|
|
129
129
|
- lib/pm.rb
|
|
130
130
|
- lib/pm/configuration.rb
|
|
131
|
+
- lib/pm/core_directives.rb
|
|
132
|
+
- lib/pm/directive.rb
|
|
131
133
|
- lib/pm/directives.rb
|
|
132
134
|
- lib/pm/metadata.rb
|
|
133
135
|
- lib/pm/parsed.rb
|
|
@@ -157,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
157
159
|
- !ruby/object:Gem::Version
|
|
158
160
|
version: '0'
|
|
159
161
|
requirements: []
|
|
160
|
-
rubygems_version: 4.0.
|
|
162
|
+
rubygems_version: 4.0.6
|
|
161
163
|
specification_version: 4
|
|
162
164
|
summary: Parse YAML metadata from markdown prompts with shell expansion and ERB rendering
|
|
163
165
|
test_files: []
|