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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44ffbc06761a515acea5e843fefe10ac6df3cec9d3daf88dcca7f3696c700f07
4
- data.tar.gz: da7892847261aec52d5db4807eb0d27d27e5a51611bb286e25dfdc24e7a6fb55
3
+ metadata.gz: f2de9f9b60b1bad9884fba07f280c00316c05dbd8d5f6b86e01805e461aaf968
4
+ data.tar.gz: 73162b249b9676d13bdf015a7cba72949d3fe9834ff6b6441fbe452048613e04
5
5
  SHA512:
6
- metadata.gz: a6b73bd17ff08c89658179a86ca6e69c22646119561d3339bb3f6204a61f9c249cc0acd34c48f049bb191689c72a52e66bc9565061482e0db29a36fea3ab6e38
7
- data.tar.gz: fa017dfaf15bb2da2cb37407f1308e92031bd0ae28d26caef6e9d76e5a5056511efa1324e1f50a7e7116e5e93f26f0d6d9fd802d047d5ceb98585d646ae805d1
6
+ metadata.gz: eb50e40c8eeb0873a68b1d84481391a1fa7b0c8f1971a461b662b86faa101bd3fd58a584f4364f2510e9df282904e3329f64b6319a16a684020b574f3aa0f1a1
7
+ data.tar.gz: 20f9e743881dca244716a5e14e97a34a050e07afb6ac954122988acf0cd7fd3b9bdf20c726ab73885c1a10301cc5ccdd194bcc38c50611340b6f77c06e87d2ab
data/CHANGELOG.md CHANGED
@@ -1,4 +1,21 @@
1
- ## Unreleased
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>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
@@ -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
- All three names call the same block. Use any of them in ERB:
273
+ #### Class-based directives
274
+
275
+ For organized groups of directives, subclass `PM::Directive`:
233
276
 
234
- ```markdown
235
- <%= webpage 'https://example.com' %>
236
- <%= website 'https://example.com' %>
237
- <%= web 'https://example.com' %>
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
- Use them in any prompt file:
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
- ```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' %>
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
- The first argument to every directive block is a `PM::RenderContext` with access to the current render state:
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
- ```ruby
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
@@ -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` 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:
@@ -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
@@ -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.2'
5
5
  end
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
- private_class_method :parse_string
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.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.5
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: []