prompt_manager 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44ffbc06761a515acea5e843fefe10ac6df3cec9d3daf88dcca7f3696c700f07
4
- data.tar.gz: da7892847261aec52d5db4807eb0d27d27e5a51611bb286e25dfdc24e7a6fb55
3
+ metadata.gz: 8f6339dd195179a1d9d9f996d1dfc4d0c689a3ed4bb9415fe2d5d68eaaa761ba
4
+ data.tar.gz: 2b443af6d4ea259fd124c6ff9d225129d0e233fbffc0d9e86a42ce2189595f29
5
5
  SHA512:
6
- metadata.gz: a6b73bd17ff08c89658179a86ca6e69c22646119561d3339bb3f6204a61f9c249cc0acd34c48f049bb191689c72a52e66bc9565061482e0db29a36fea3ab6e38
7
- data.tar.gz: fa017dfaf15bb2da2cb37407f1308e92031bd0ae28d26caef6e9d76e5a5056511efa1324e1f50a7e7116e5e93f26f0d6d9fd802d047d5ceb98585d646ae805d1
6
+ metadata.gz: 26ec66ace02ff31ced22d3bd331884c0c74d5ee7356e138897759ed70be2be7d52dca239d3f1ac30c6599c8133446d199db7833de18fda7053e6fc977a1bde3b
7
+ data.tar.gz: a60049ad709699ec112b43c6f281dd50ae6b80d3774c5c9a58e56d6daebcb5ccfc647544461f62f3f6e4a23367421d1401b34988ae22657764b72bf387c03ff8
data/CHANGELOG.md CHANGED
@@ -1,4 +1,16 @@
1
- ## Unreleased
1
+ ### [1.0.1] - 2026-02-04
2
+
3
+ #### Added
4
+ - **`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.
5
+ - **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.
6
+ - New test file `directive_base_test.rb` with tests for the `PM::Directive` DSL, subclass tracking, category naming, and dispatch block building.
7
+ - New test fixtures for `insert`/`read` directive coverage.
8
+
9
+ #### Changed
10
+ - **`PM::CoreDirectives`** — built-in `include`, `insert`/`read` directives refactored as a `PM::Directive` subclass using the `desc` DSL.
11
+ - `PM.register` directive aliases now support registration via `alias_method` in `PM::Directive` subclasses, with automatic detection via `UnboundMethod#original_name`.
12
+ - Directive registry simplified; `PM.reset_directives!` now delegates to `PM::Directive.register_all`.
13
+ - Updated README and documentation guides for custom directives and includes.
2
14
 
3
15
  ## Released
4
16
  ### [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>Reclusive Include System</strong> - Compose prompts from multiple files<br>
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
@@ -190,6 +191,37 @@ Included files go through the full processing pipeline (comment stripping, metad
190
191
 
191
192
  Nested includes work — A can include B which includes C. Circular includes raise an error.
192
193
 
194
+ ### Inserting raw file content
195
+
196
+ 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:
197
+
198
+ ```md
199
+ ---
200
+ title: Code Review
201
+ parameters:
202
+ feedback_style: null
203
+ ---
204
+ Review this Ruby code:
205
+
206
+ ```ruby
207
+ <%= insert 'app/models/user.rb' %>
208
+ ```
209
+
210
+ Use a <%= feedback_style %> tone.
211
+ ```
212
+
213
+ Paths are resolved relative to the parent file's directory (same as `include`). Absolute paths work from any context. Missing files raise an error.
214
+
215
+ `insert` vs `include`:
216
+
217
+ | | `insert` | `include` |
218
+ |---|---|---|
219
+ | File types | Any | `.md` only |
220
+ | ERB in content | Preserved as literal text | Rendered |
221
+ | Shell expansion | Not applied | Applied |
222
+ | Recursion | None | Nested includes supported |
223
+ | Metadata tracking | None | `metadata.includes` tree |
224
+
193
225
  After calling `to_s`, the parent's metadata has an `includes` key with a tree of what was included:
194
226
 
195
227
  ```ruby
@@ -215,10 +247,11 @@ parsed.metadata.includes
215
247
 
216
248
  ### Custom directives
217
249
 
250
+ #### Block-based registration
251
+
218
252
  Register custom methods available in ERB templates:
219
253
 
220
254
  ```ruby
221
- PM.register(:read) { |_ctx, path| File.read(path) }
222
255
  PM.register(:env) { |_ctx, key| ENV.fetch(key, '') }
223
256
  PM.register(:run) { |_ctx, cmd| `#{cmd}`.chomp }
224
257
  ```
@@ -229,26 +262,45 @@ Register multiple names for the same directive (aliases):
229
262
  PM.register(:webpage, :website, :web) { |_ctx, url| fetch_page(url) }
230
263
  ```
231
264
 
232
- All three names call the same block. Use any of them in ERB:
265
+ #### Class-based directives
266
+
267
+ For organized groups of directives, subclass `PM::Directive`:
268
+
269
+ ```ruby
270
+ class MyDirectives < PM::Directive
271
+ desc "Fetch environment variable"
272
+ def env(ctx, key)
273
+ ENV.fetch(key, '')
274
+ end
275
+
276
+ desc "Run a shell command"
277
+ def run(ctx, cmd)
278
+ `#{cmd}`.chomp
279
+ end
280
+ alias_method :exec, :run
281
+ end
233
282
 
234
- ```markdown
235
- <%= webpage 'https://example.com' %>
236
- <%= website 'https://example.com' %>
237
- <%= web 'https://example.com' %>
283
+ # Register all directive subclasses with PM
284
+ PM::Directive.register_all
238
285
  ```
239
286
 
240
- Use them in any prompt file:
287
+ `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
288
 
242
- ```md
243
- ---
244
- title: Deploy Prompt
245
- ---
246
- Hostname: <%= read '/etc/hostname' %>
247
- Environment: <%= env 'DEPLOY_ENV' %>
248
- Recent commits: <%= run 'git log --oneline -5' %>
289
+ Override `build_dispatch_block` on your base class to customize how methods are called:
290
+
291
+ ```ruby
292
+ class MyDirectives < PM::Directive
293
+ class << self
294
+ def build_dispatch_block(inst, method_name)
295
+ proc { |_ctx, *args| inst.send(method_name, args) }
296
+ end
297
+ end
298
+ end
249
299
  ```
250
300
 
251
- The first argument to every directive block is a `PM::RenderContext` with access to the current render state:
301
+ #### RenderContext
302
+
303
+ The first argument to every directive block (or class method) is a `PM::RenderContext`:
252
304
 
253
305
  - `ctx.directory` — directory of the file being rendered
254
306
  - `ctx.params` — merged parameter values
@@ -256,10 +308,7 @@ The first argument to every directive block is a `PM::RenderContext` with access
256
308
  - `ctx.depth` — include nesting depth
257
309
  - `ctx.included` — Set of file paths already in the include chain
258
310
 
259
- ```ruby
260
- PM.register(:current_file) { |ctx| ctx.metadata.name || 'unknown' }
261
- PM.register(:depth) { |ctx| ctx.depth.to_s }
262
- ```
311
+ #### Duplicate detection and reset
263
312
 
264
313
  Registering a name that already exists raises an error:
265
314
 
@@ -272,6 +321,7 @@ Reset to built-in directives only:
272
321
 
273
322
  ```ruby
274
323
  PM.reset_directives!
324
+ # Restores: include, insert, read
275
325
  ```
276
326
 
277
327
  ### Disabling processing stages
@@ -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` is registered.
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
  ```
@@ -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:
@@ -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
@@ -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 the built-ins.
28
+ # Clears all directives and re-registers from PM::Directive subclasses.
29
29
  def self.reset_directives!
30
30
  @directives.clear
31
- register_builtins
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PM
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.1'
5
5
  end
data/lib/pm.rb CHANGED
@@ -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.0
4
+ version: 1.0.1
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