@prompd/cli 0.3.3
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.
- package/README.md +162 -0
- package/bin/prompd.js +23 -0
- package/dist/commands/cache.d.ts +3 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +199 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/compile.d.ts +9 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +104 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/config.d.ts +7 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +212 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/create.d.ts +3 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +183 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deps.d.ts +3 -0
- package/dist/commands/deps.d.ts.map +1 -0
- package/dist/commands/deps.js +192 -0
- package/dist/commands/deps.js.map +1 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +227 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/git.d.ts +3 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +306 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +177 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +126 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/mcp.d.ts +3 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +326 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/namespace.d.ts +3 -0
- package/dist/commands/namespace.d.ts.map +1 -0
- package/dist/commands/namespace.js +113 -0
- package/dist/commands/namespace.js.map +1 -0
- package/dist/commands/package.d.ts +23 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +746 -0
- package/dist/commands/package.js.map +1 -0
- package/dist/commands/provider.d.ts +3 -0
- package/dist/commands/provider.d.ts.map +1 -0
- package/dist/commands/provider.js +285 -0
- package/dist/commands/provider.js.map +1 -0
- package/dist/commands/registry.d.ts +9 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +361 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +157 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/show.d.ts +3 -0
- package/dist/commands/show.d.ts.map +1 -0
- package/dist/commands/show.js +90 -0
- package/dist/commands/show.js.map +1 -0
- package/dist/commands/uninstall.d.ts +3 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +95 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/validate.d.ts +3 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +57 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/version.d.ts +3 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +166 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +388 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth.d.ts +164 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +388 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/compiler/file-system.d.ts +178 -0
- package/dist/lib/compiler/file-system.d.ts.map +1 -0
- package/dist/lib/compiler/file-system.js +440 -0
- package/dist/lib/compiler/file-system.js.map +1 -0
- package/dist/lib/compiler/formatters/anthropic.d.ts +21 -0
- package/dist/lib/compiler/formatters/anthropic.d.ts.map +1 -0
- package/dist/lib/compiler/formatters/anthropic.js +95 -0
- package/dist/lib/compiler/formatters/anthropic.js.map +1 -0
- package/dist/lib/compiler/formatters/markdown.d.ts +17 -0
- package/dist/lib/compiler/formatters/markdown.d.ts.map +1 -0
- package/dist/lib/compiler/formatters/markdown.js +114 -0
- package/dist/lib/compiler/formatters/markdown.js.map +1 -0
- package/dist/lib/compiler/formatters/openai.d.ts +21 -0
- package/dist/lib/compiler/formatters/openai.d.ts.map +1 -0
- package/dist/lib/compiler/formatters/openai.js +98 -0
- package/dist/lib/compiler/formatters/openai.js.map +1 -0
- package/dist/lib/compiler/index.d.ts +56 -0
- package/dist/lib/compiler/index.d.ts.map +1 -0
- package/dist/lib/compiler/index.js +165 -0
- package/dist/lib/compiler/index.js.map +1 -0
- package/dist/lib/compiler/language-map.d.ts +31 -0
- package/dist/lib/compiler/language-map.d.ts.map +1 -0
- package/dist/lib/compiler/language-map.js +156 -0
- package/dist/lib/compiler/language-map.js.map +1 -0
- package/dist/lib/compiler/package-resolver.d.ts +68 -0
- package/dist/lib/compiler/package-resolver.d.ts.map +1 -0
- package/dist/lib/compiler/package-resolver.js +254 -0
- package/dist/lib/compiler/package-resolver.js.map +1 -0
- package/dist/lib/compiler/pipeline.d.ts +53 -0
- package/dist/lib/compiler/pipeline.d.ts.map +1 -0
- package/dist/lib/compiler/pipeline.js +209 -0
- package/dist/lib/compiler/pipeline.js.map +1 -0
- package/dist/lib/compiler/prompd-loader.d.ts +108 -0
- package/dist/lib/compiler/prompd-loader.d.ts.map +1 -0
- package/dist/lib/compiler/prompd-loader.js +270 -0
- package/dist/lib/compiler/prompd-loader.js.map +1 -0
- package/dist/lib/compiler/section-override.d.ts +40 -0
- package/dist/lib/compiler/section-override.d.ts.map +1 -0
- package/dist/lib/compiler/section-override.js +296 -0
- package/dist/lib/compiler/section-override.js.map +1 -0
- package/dist/lib/compiler/stages/assets.d.ts +71 -0
- package/dist/lib/compiler/stages/assets.d.ts.map +1 -0
- package/dist/lib/compiler/stages/assets.js +456 -0
- package/dist/lib/compiler/stages/assets.js.map +1 -0
- package/dist/lib/compiler/stages/codegen.d.ts +17 -0
- package/dist/lib/compiler/stages/codegen.d.ts.map +1 -0
- package/dist/lib/compiler/stages/codegen.js +64 -0
- package/dist/lib/compiler/stages/codegen.js.map +1 -0
- package/dist/lib/compiler/stages/dependency.d.ts +38 -0
- package/dist/lib/compiler/stages/dependency.d.ts.map +1 -0
- package/dist/lib/compiler/stages/dependency.js +307 -0
- package/dist/lib/compiler/stages/dependency.js.map +1 -0
- package/dist/lib/compiler/stages/lexical.d.ts +19 -0
- package/dist/lib/compiler/stages/lexical.d.ts.map +1 -0
- package/dist/lib/compiler/stages/lexical.js +92 -0
- package/dist/lib/compiler/stages/lexical.js.map +1 -0
- package/dist/lib/compiler/stages/semantic.d.ts +20 -0
- package/dist/lib/compiler/stages/semantic.d.ts.map +1 -0
- package/dist/lib/compiler/stages/semantic.js +166 -0
- package/dist/lib/compiler/stages/semantic.js.map +1 -0
- package/dist/lib/compiler/stages/template.d.ts +94 -0
- package/dist/lib/compiler/stages/template.d.ts.map +1 -0
- package/dist/lib/compiler/stages/template.js +1044 -0
- package/dist/lib/compiler/stages/template.js.map +1 -0
- package/dist/lib/compiler/types.d.ts +200 -0
- package/dist/lib/compiler/types.d.ts.map +1 -0
- package/dist/lib/compiler/types.js +137 -0
- package/dist/lib/compiler/types.js.map +1 -0
- package/dist/lib/config.d.ts +29 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +375 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/errors.d.ts +19 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +47 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/executor.d.ts +18 -0
- package/dist/lib/executor.d.ts.map +1 -0
- package/dist/lib/executor.js +372 -0
- package/dist/lib/executor.js.map +1 -0
- package/dist/lib/git.d.ts +74 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +254 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/index.d.ts +43 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +108 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/mcp.d.ts +42 -0
- package/dist/lib/mcp.d.ts.map +1 -0
- package/dist/lib/mcp.js +477 -0
- package/dist/lib/mcp.js.map +1 -0
- package/dist/lib/model-updater.d.ts +51 -0
- package/dist/lib/model-updater.d.ts.map +1 -0
- package/dist/lib/model-updater.js +275 -0
- package/dist/lib/model-updater.js.map +1 -0
- package/dist/lib/parser.d.ts +9 -0
- package/dist/lib/parser.d.ts.map +1 -0
- package/dist/lib/parser.js +197 -0
- package/dist/lib/parser.js.map +1 -0
- package/dist/lib/registry.d.ts +183 -0
- package/dist/lib/registry.d.ts.map +1 -0
- package/dist/lib/registry.js +786 -0
- package/dist/lib/registry.js.map +1 -0
- package/dist/lib/rpc-server.d.ts +78 -0
- package/dist/lib/rpc-server.d.ts.map +1 -0
- package/dist/lib/rpc-server.js +404 -0
- package/dist/lib/rpc-server.js.map +1 -0
- package/dist/lib/security.d.ts +120 -0
- package/dist/lib/security.d.ts.map +1 -0
- package/dist/lib/security.js +478 -0
- package/dist/lib/security.js.map +1 -0
- package/dist/lib/validation.d.ts +106 -0
- package/dist/lib/validation.d.ts.map +1 -0
- package/dist/lib/validation.js +398 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/lib/version.d.ts +29 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +202 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/lib/workflow-engine.d.ts +161 -0
- package/dist/lib/workflow-engine.d.ts.map +1 -0
- package/dist/lib/workflow-engine.js +422 -0
- package/dist/lib/workflow-engine.js.map +1 -0
- package/dist/lib/workflow.d.ts +102 -0
- package/dist/lib/workflow.d.ts.map +1 -0
- package/dist/lib/workflow.js +228 -0
- package/dist/lib/workflow.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +134 -0
- package/dist/server.js.map +1 -0
- package/dist/types/index.d.ts +116 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +144 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,1044 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Template Processing Stage
|
|
4
|
+
*
|
|
5
|
+
* Processes Jinja2/Nunjucks templates, package references, and section overrides.
|
|
6
|
+
* This is the most complex stage, handling:
|
|
7
|
+
* - Nunjucks template rendering with custom filters (fromcsv, fromjson, tojson, lines)
|
|
8
|
+
* - Package reference resolution and content injection
|
|
9
|
+
* - Inheritance processing with section-aware merging
|
|
10
|
+
* - Enhanced variable substitution with nested property access
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.TemplateProcessingStage = void 0;
|
|
47
|
+
const nunjucks = __importStar(require("nunjucks"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const parser_1 = require("../../parser");
|
|
50
|
+
const section_override_1 = require("../section-override");
|
|
51
|
+
const package_resolver_1 = require("../package-resolver");
|
|
52
|
+
const language_map_1 = require("../language-map");
|
|
53
|
+
const prompd_loader_1 = require("../prompd-loader");
|
|
54
|
+
class TemplateProcessingStage {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.sectionProcessor = new section_override_1.SectionOverrideProcessor();
|
|
57
|
+
// Configure Nunjucks to use double braces for variables (standard Jinja2/Nunjucks syntax)
|
|
58
|
+
this.nunjucksEnv = new nunjucks.Environment(null, {
|
|
59
|
+
autoescape: false, // Don't escape HTML - we're doing markdown
|
|
60
|
+
throwOnUndefined: false, // Gracefully handle missing variables
|
|
61
|
+
trimBlocks: true,
|
|
62
|
+
lstripBlocks: true,
|
|
63
|
+
tags: {
|
|
64
|
+
blockStart: '{%',
|
|
65
|
+
blockEnd: '%}',
|
|
66
|
+
variableStart: '{{',
|
|
67
|
+
variableEnd: '}}',
|
|
68
|
+
commentStart: '{#',
|
|
69
|
+
commentEnd: '#}'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
// Register custom filters
|
|
73
|
+
this.registerFilters();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Register custom Jinja2/Nunjucks filters for data transformation.
|
|
77
|
+
*/
|
|
78
|
+
registerFilters() {
|
|
79
|
+
// fromcsv - Parse CSV string into array of objects
|
|
80
|
+
this.nunjucksEnv.addFilter('fromcsv', (csvString) => {
|
|
81
|
+
if (!csvString || typeof csvString !== 'string') {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
return this.parseCsv(csvString);
|
|
85
|
+
});
|
|
86
|
+
// fromjson - Parse JSON string into object/array
|
|
87
|
+
this.nunjucksEnv.addFilter('fromjson', (jsonString) => {
|
|
88
|
+
if (!jsonString || typeof jsonString !== 'string') {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(jsonString);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// tojson - Convert object to JSON string
|
|
99
|
+
this.nunjucksEnv.addFilter('tojson', (obj, indent) => {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.stringify(obj, null, indent);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return '{}';
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// lines - Split string into array of lines
|
|
108
|
+
this.nunjucksEnv.addFilter('lines', (str) => {
|
|
109
|
+
if (!str || typeof str !== 'string') {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
return str.split(/\r?\n/);
|
|
113
|
+
});
|
|
114
|
+
// dedent - Remove common leading whitespace from text
|
|
115
|
+
this.nunjucksEnv.addFilter('dedent', (str) => {
|
|
116
|
+
if (!str || typeof str !== 'string') {
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
const lines = str.split(/\r?\n/);
|
|
120
|
+
// Find minimum indentation (ignoring empty lines)
|
|
121
|
+
let minIndent = Infinity;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
if (line.trim().length === 0)
|
|
124
|
+
continue;
|
|
125
|
+
const match = line.match(/^(\s*)/);
|
|
126
|
+
if (match && match[1].length < minIndent) {
|
|
127
|
+
minIndent = match[1].length;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (minIndent === Infinity)
|
|
131
|
+
minIndent = 0;
|
|
132
|
+
// Remove the common indent from all lines
|
|
133
|
+
return lines.map(line => line.slice(minIndent)).join('\n');
|
|
134
|
+
});
|
|
135
|
+
// truncate - Truncate string with ellipsis
|
|
136
|
+
this.nunjucksEnv.addFilter('truncate', (str, length = 80, suffix = '...') => {
|
|
137
|
+
if (!str || typeof str !== 'string') {
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
if (str.length <= length) {
|
|
141
|
+
return str;
|
|
142
|
+
}
|
|
143
|
+
return str.slice(0, length - suffix.length) + suffix;
|
|
144
|
+
});
|
|
145
|
+
// codeblock - Wrap content in fenced code block
|
|
146
|
+
this.nunjucksEnv.addFilter('codeblock', (str, language = '') => {
|
|
147
|
+
if (!str || typeof str !== 'string') {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
return '```' + language + '\n' + str + '\n```';
|
|
151
|
+
});
|
|
152
|
+
// unique - Remove duplicate items from array
|
|
153
|
+
this.nunjucksEnv.addFilter('unique', (arr) => {
|
|
154
|
+
if (!Array.isArray(arr)) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [...new Set(arr)];
|
|
158
|
+
});
|
|
159
|
+
// pluck - Extract single field from array of objects
|
|
160
|
+
this.nunjucksEnv.addFilter('pluck', (arr, field) => {
|
|
161
|
+
if (!Array.isArray(arr)) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
return arr.map(item => item?.[field]).filter(v => v !== undefined);
|
|
165
|
+
});
|
|
166
|
+
// where - Filter objects by field value
|
|
167
|
+
this.nunjucksEnv.addFilter('where', (arr, field, value) => {
|
|
168
|
+
if (!Array.isArray(arr)) {
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
return arr.filter(item => item?.[field] === value);
|
|
172
|
+
});
|
|
173
|
+
// groupby - Group array items by field value
|
|
174
|
+
this.nunjucksEnv.addFilter('groupby', (arr, field) => {
|
|
175
|
+
if (!Array.isArray(arr)) {
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const item of arr) {
|
|
180
|
+
const key = String(item?.[field] ?? 'undefined');
|
|
181
|
+
if (!result[key]) {
|
|
182
|
+
result[key] = [];
|
|
183
|
+
}
|
|
184
|
+
result[key].push(item);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
});
|
|
188
|
+
// shuffle - Randomize array order
|
|
189
|
+
this.nunjucksEnv.addFilter('shuffle', (arr) => {
|
|
190
|
+
if (!Array.isArray(arr)) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const shuffled = [...arr];
|
|
194
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
195
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
196
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
197
|
+
}
|
|
198
|
+
return shuffled;
|
|
199
|
+
});
|
|
200
|
+
// sample - Pick random N items from array
|
|
201
|
+
this.nunjucksEnv.addFilter('sample', (arr, count = 1) => {
|
|
202
|
+
if (!Array.isArray(arr)) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
const shuffled = [...arr];
|
|
206
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
207
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
208
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
209
|
+
}
|
|
210
|
+
return shuffled.slice(0, Math.min(count, shuffled.length));
|
|
211
|
+
});
|
|
212
|
+
// wordwrap - Wrap text at specified width
|
|
213
|
+
this.nunjucksEnv.addFilter('wordwrap', (str, width = 80) => {
|
|
214
|
+
if (!str || typeof str !== 'string') {
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
const words = str.split(/\s+/);
|
|
218
|
+
const lines = [];
|
|
219
|
+
let currentLine = '';
|
|
220
|
+
for (const word of words) {
|
|
221
|
+
if (currentLine.length + word.length + 1 <= width) {
|
|
222
|
+
currentLine += (currentLine ? ' ' : '') + word;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
if (currentLine)
|
|
226
|
+
lines.push(currentLine);
|
|
227
|
+
currentLine = word;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (currentLine)
|
|
231
|
+
lines.push(currentLine);
|
|
232
|
+
return lines.join('\n');
|
|
233
|
+
});
|
|
234
|
+
// bulletlist - Convert array/lines to bullet list
|
|
235
|
+
this.nunjucksEnv.addFilter('bulletlist', (input) => {
|
|
236
|
+
const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
|
|
237
|
+
return items.filter(item => String(item).trim()).map(item => `- ${item}`).join('\n');
|
|
238
|
+
});
|
|
239
|
+
// numberedlist - Convert array/lines to numbered list
|
|
240
|
+
this.nunjucksEnv.addFilter('numberedlist', (input) => {
|
|
241
|
+
const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
|
|
242
|
+
return items.filter(item => String(item).trim()).map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Parse CSV string into array of record objects.
|
|
247
|
+
*/
|
|
248
|
+
parseCsv(csvString) {
|
|
249
|
+
const lines = csvString.trim().split(/\r?\n/);
|
|
250
|
+
if (lines.length === 0) {
|
|
251
|
+
return [];
|
|
252
|
+
}
|
|
253
|
+
// Parse header row
|
|
254
|
+
const headers = this.parseCsvLine(lines[0]);
|
|
255
|
+
// Parse data rows
|
|
256
|
+
const records = [];
|
|
257
|
+
for (let i = 1; i < lines.length; i++) {
|
|
258
|
+
const line = lines[i].trim();
|
|
259
|
+
if (!line)
|
|
260
|
+
continue; // Skip empty lines
|
|
261
|
+
const values = this.parseCsvLine(line);
|
|
262
|
+
const record = {};
|
|
263
|
+
for (let j = 0; j < headers.length; j++) {
|
|
264
|
+
const header = headers[j].trim();
|
|
265
|
+
const value = values[j]?.trim() ?? '';
|
|
266
|
+
record[header] = value;
|
|
267
|
+
}
|
|
268
|
+
records.push(record);
|
|
269
|
+
}
|
|
270
|
+
return records;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Parse a single CSV line, handling quoted values.
|
|
274
|
+
*/
|
|
275
|
+
parseCsvLine(line) {
|
|
276
|
+
const result = [];
|
|
277
|
+
let current = '';
|
|
278
|
+
let inQuotes = false;
|
|
279
|
+
for (let i = 0; i < line.length; i++) {
|
|
280
|
+
const char = line[i];
|
|
281
|
+
if (char === '"') {
|
|
282
|
+
if (inQuotes && line[i + 1] === '"') {
|
|
283
|
+
// Escaped quote
|
|
284
|
+
current += '"';
|
|
285
|
+
i++;
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Toggle quote mode
|
|
289
|
+
inQuotes = !inQuotes;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (char === ',' && !inQuotes) {
|
|
293
|
+
result.push(current);
|
|
294
|
+
current = '';
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
current += char;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
result.push(current);
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
async process(context) {
|
|
304
|
+
if (!context.content) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
let content = context.content;
|
|
308
|
+
// Step 1: Process package references with prefixes (e.g., @security/prompts/audit)
|
|
309
|
+
if (context.dependencies.imports) {
|
|
310
|
+
content = await this.processPackageReferences(context, content);
|
|
311
|
+
}
|
|
312
|
+
// Step 2: Process inheritance with section-aware override support
|
|
313
|
+
if (context.dependencies.inherits) {
|
|
314
|
+
content = await this.processInheritance(context, content);
|
|
315
|
+
}
|
|
316
|
+
// Step 3: Process standalone overrides (without inheritance)
|
|
317
|
+
if (context.metadata?.override && !context.dependencies.inherits) {
|
|
318
|
+
content = await this.processStandaloneOverrides(context, content);
|
|
319
|
+
}
|
|
320
|
+
// Step 4: Filter code blocks based on context file types
|
|
321
|
+
// If context files are attached, only keep code blocks that match those file extensions
|
|
322
|
+
if (context.metadata?.context) {
|
|
323
|
+
content = this.filterCodeBlocksByContext(context, content);
|
|
324
|
+
}
|
|
325
|
+
// Step 5: Process templates with Jinja2/Nunjucks
|
|
326
|
+
content = await this.processTemplate(context, content);
|
|
327
|
+
context.content = content;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Process package references (e.g., @prefix/path/to/file).
|
|
331
|
+
*/
|
|
332
|
+
async processPackageReferences(context, content) {
|
|
333
|
+
if (!context.dependencies.imports) {
|
|
334
|
+
return content;
|
|
335
|
+
}
|
|
336
|
+
for (const [packageRef, packageInfo] of Object.entries(context.dependencies.imports)) {
|
|
337
|
+
if (!packageInfo.prefix || !packageInfo.path) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const prefix = packageInfo.prefix;
|
|
341
|
+
const packagePath = packageInfo.path;
|
|
342
|
+
// Replace references like @security/prompts/audit with actual content
|
|
343
|
+
// Pattern: @prefix/path/to/resource
|
|
344
|
+
const pattern = new RegExp(`@${this.escapeRegex(prefix)}/([^\\s]+)`, 'g');
|
|
345
|
+
// Find all matches first
|
|
346
|
+
const matches = [];
|
|
347
|
+
let match;
|
|
348
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
349
|
+
matches.push({
|
|
350
|
+
match: match[0],
|
|
351
|
+
resourcePath: match[1],
|
|
352
|
+
index: match.index
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Replace in reverse order to preserve indices
|
|
356
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
357
|
+
const m = matches[i];
|
|
358
|
+
const replacement = await this.loadPackageResource(context, packagePath, m.resourcePath, prefix);
|
|
359
|
+
content = content.substring(0, m.index) + replacement + content.substring(m.index + m.match.length);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return content;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Load a resource from a package.
|
|
366
|
+
*/
|
|
367
|
+
async loadPackageResource(context, packagePath, resourcePath, prefix) {
|
|
368
|
+
try {
|
|
369
|
+
const fs = context.fileSystem;
|
|
370
|
+
// Try different extensions
|
|
371
|
+
const possibleExtensions = ['', '.prmd', '.md', '.txt'];
|
|
372
|
+
for (const ext of possibleExtensions) {
|
|
373
|
+
const filePath = (0, package_resolver_1.resolvePackageFile)(packagePath, resourcePath + ext);
|
|
374
|
+
if (await fs.exists(filePath)) {
|
|
375
|
+
const contentData = await fs.readFile(filePath);
|
|
376
|
+
// If it's a .prmd file, parse and extract content
|
|
377
|
+
if (filePath.endsWith('.prmd')) {
|
|
378
|
+
const parser = new parser_1.PrompdParser();
|
|
379
|
+
try {
|
|
380
|
+
const parsed = parser.parseContent(contentData);
|
|
381
|
+
// Return just the content part, not metadata
|
|
382
|
+
return parsed.content || `# Content from @${prefix}/${resourcePath}`;
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
// If parsing fails, return raw content
|
|
386
|
+
return contentData;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// For other files, return as-is
|
|
391
|
+
return contentData;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// File not found
|
|
396
|
+
if (context.verbose) {
|
|
397
|
+
console.log(`Warning: Could not find @${prefix}/${resourcePath} in ${packagePath}`);
|
|
398
|
+
}
|
|
399
|
+
return `[Not found: @${prefix}/${resourcePath}]`;
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
if (context.verbose) {
|
|
403
|
+
console.log(`Warning: Failed to load @${prefix}/${resourcePath}: ${error}`);
|
|
404
|
+
}
|
|
405
|
+
return `[Error loading @${prefix}/${resourcePath}]`;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Process inheritance with section-aware merging.
|
|
410
|
+
*/
|
|
411
|
+
async processInheritance(context, content) {
|
|
412
|
+
const parentPath = context.dependencies.inherits;
|
|
413
|
+
if (!parentPath) {
|
|
414
|
+
return content;
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const fs = context.fileSystem;
|
|
418
|
+
const parser = new parser_1.PrompdParser();
|
|
419
|
+
let parentFile = null;
|
|
420
|
+
// Check if it's a direct file path
|
|
421
|
+
if (parentPath.endsWith('.prmd') && await fs.exists(parentPath)) {
|
|
422
|
+
parentFile = parentPath;
|
|
423
|
+
}
|
|
424
|
+
else if (await fs.exists(parentPath) && await fs.isDirectory(parentPath)) {
|
|
425
|
+
// Package directory - find main .prmd file
|
|
426
|
+
const files = (await fs.readdir(parentPath)).filter((f) => f.endsWith('.prmd'));
|
|
427
|
+
if (files.length > 0) {
|
|
428
|
+
// Look for main.prmd first
|
|
429
|
+
const mainFile = files.find((f) => f === 'main.prmd');
|
|
430
|
+
parentFile = fs.join(parentPath, mainFile || files[0]);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (!parentFile) {
|
|
434
|
+
// Find location of 'inherits:' in source for accurate error positioning
|
|
435
|
+
const location = context.findLocation(/inherits:/);
|
|
436
|
+
context.addDiagnostic({
|
|
437
|
+
message: `Could not find inherited file: ${parentPath}`,
|
|
438
|
+
severity: 'error',
|
|
439
|
+
source: 'template',
|
|
440
|
+
code: 'INHERITED_FILE_NOT_FOUND',
|
|
441
|
+
...location
|
|
442
|
+
});
|
|
443
|
+
return content;
|
|
444
|
+
}
|
|
445
|
+
// Parse parent file - read content from file system
|
|
446
|
+
const parentFileContent = await fs.readFile(parentFile);
|
|
447
|
+
const parentData = parser.parseContent(parentFileContent);
|
|
448
|
+
// Get overrides from child metadata
|
|
449
|
+
const overrides = context.metadata?.override || {};
|
|
450
|
+
// Process content with section-aware merging
|
|
451
|
+
if (parentData.content || content) {
|
|
452
|
+
if (Object.keys(overrides).length > 0) {
|
|
453
|
+
// Section-aware override processing
|
|
454
|
+
try {
|
|
455
|
+
// Extract sections from parent and child
|
|
456
|
+
const parentSections = parentData.content
|
|
457
|
+
? this.sectionProcessor.extractSections(parentData.content)
|
|
458
|
+
: new Map();
|
|
459
|
+
const childSections = content
|
|
460
|
+
? this.sectionProcessor.extractSections(content)
|
|
461
|
+
: new Map();
|
|
462
|
+
// Determine base directory for resolving override files
|
|
463
|
+
const baseDir = context.fileSystem.dirname(context.sourceFile);
|
|
464
|
+
// Apply overrides and merge content
|
|
465
|
+
const mergedContent = await this.sectionProcessor.applyOverrides(parentSections, childSections, overrides, baseDir, context.verbose, context.fileSystem);
|
|
466
|
+
content = mergedContent;
|
|
467
|
+
if (context.verbose) {
|
|
468
|
+
console.log(`✓ Applied ${Object.keys(overrides).length} section overrides from parent: ${parentPath}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
// Fallback to simple concatenation on error
|
|
473
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
474
|
+
context.addWarning(`Section override processing failed, using simple inheritance: ${errorMessage}`);
|
|
475
|
+
if (parentData.content) {
|
|
476
|
+
content = content ? `${parentData.content}\n\n${content}` : parentData.content;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// No overrides - use simple concatenation (backward compatibility)
|
|
482
|
+
if (parentData.content) {
|
|
483
|
+
content = content ? `${parentData.content}\n\n${content}` : parentData.content;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Merge parent parameters (child parameters override)
|
|
488
|
+
if (parentData.metadata?.parameters && context.metadata) {
|
|
489
|
+
for (const parentParam of parentData.metadata.parameters) {
|
|
490
|
+
// Only add parent parameter if not defined in child
|
|
491
|
+
const paramExists = context.metadata.parameters?.some(childParam => childParam.name === parentParam.name);
|
|
492
|
+
if (!paramExists) {
|
|
493
|
+
if (!context.metadata.parameters) {
|
|
494
|
+
context.metadata.parameters = [];
|
|
495
|
+
}
|
|
496
|
+
context.metadata.parameters.push(parentParam);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (context.verbose) {
|
|
501
|
+
console.log(`✓ Template inherits from: ${parentPath}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch (error) {
|
|
505
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
506
|
+
context.addWarning(`Failed to process inheritance from ${parentPath}: ${errorMessage}`);
|
|
507
|
+
}
|
|
508
|
+
return content;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Process standalone overrides (without inheritance).
|
|
512
|
+
*/
|
|
513
|
+
async processStandaloneOverrides(context, content) {
|
|
514
|
+
const overrides = context.metadata?.override;
|
|
515
|
+
if (!overrides) {
|
|
516
|
+
return content;
|
|
517
|
+
}
|
|
518
|
+
if (context.verbose) {
|
|
519
|
+
console.log(`Processing ${Object.keys(overrides).length} standalone section overrides...`);
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
// Extract sections from current content
|
|
523
|
+
const currentSections = this.sectionProcessor.extractSections(content);
|
|
524
|
+
// Determine base directory for resolving override files
|
|
525
|
+
const baseDir = context.fileSystem.dirname(context.sourceFile);
|
|
526
|
+
// Apply each override to replace sections in current content
|
|
527
|
+
for (const [sectionId, overridePath] of Object.entries(overrides)) {
|
|
528
|
+
if (currentSections.has(sectionId)) {
|
|
529
|
+
if (overridePath === null) {
|
|
530
|
+
// Remove section
|
|
531
|
+
currentSections.delete(sectionId);
|
|
532
|
+
if (context.verbose) {
|
|
533
|
+
console.log(` - Removing section '${sectionId}'`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else if (typeof overridePath === 'string') {
|
|
537
|
+
// Replace section content
|
|
538
|
+
try {
|
|
539
|
+
const overrideContent = await this.sectionProcessor.loadOverrideContent(overridePath, baseDir, context.fileSystem);
|
|
540
|
+
// Update section with override content
|
|
541
|
+
const originalSection = currentSections.get(sectionId);
|
|
542
|
+
currentSections.set(sectionId, {
|
|
543
|
+
...originalSection,
|
|
544
|
+
content: overrideContent
|
|
545
|
+
});
|
|
546
|
+
if (context.verbose) {
|
|
547
|
+
console.log(` - Replacing section '${sectionId}' with content from ${overridePath}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
552
|
+
context.addWarning(`Failed to apply override for section '${sectionId}': ${errorMessage}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
context.addWarning(`Override section '${sectionId}' not found in current content`);
|
|
558
|
+
if (context.verbose) {
|
|
559
|
+
const available = Array.from(currentSections.keys()).sort().join(', ');
|
|
560
|
+
console.log(`Warning: Override section '${sectionId}' not found. Available: ${available}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Reconstruct content from modified sections
|
|
565
|
+
const contentParts = [];
|
|
566
|
+
for (const section of currentSections.values()) {
|
|
567
|
+
contentParts.push(section.content);
|
|
568
|
+
}
|
|
569
|
+
content = contentParts.join('\n\n');
|
|
570
|
+
if (context.verbose) {
|
|
571
|
+
console.log(`✓ Applied standalone overrides, final content has ${currentSections.size} sections`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
576
|
+
context.addWarning(`Standalone override processing failed: ${errorMessage}`);
|
|
577
|
+
}
|
|
578
|
+
return content;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Filter code blocks in content based on context file types.
|
|
582
|
+
*
|
|
583
|
+
* When context files are attached (e.g., .ts files), this method filters
|
|
584
|
+
* the prompt content to only keep code blocks that match those file types.
|
|
585
|
+
*
|
|
586
|
+
* Example:
|
|
587
|
+
* - If context contains "typescript-examples.ts", keep only ```typescript blocks
|
|
588
|
+
* - Non-code content and unmatched code blocks are preserved as plain text
|
|
589
|
+
*
|
|
590
|
+
* @param context - The compilation context with extracted contexts
|
|
591
|
+
* @param content - The markdown content to filter
|
|
592
|
+
* @returns Filtered content with only matching code blocks
|
|
593
|
+
*/
|
|
594
|
+
filterCodeBlocksByContext(context, content) {
|
|
595
|
+
// Extract file extensions from context metadata
|
|
596
|
+
const contextExtensions = this.extractContextExtensions(context.metadata?.context);
|
|
597
|
+
if (contextExtensions.size === 0) {
|
|
598
|
+
// No recognizable file extensions in context, return content unchanged
|
|
599
|
+
return content;
|
|
600
|
+
}
|
|
601
|
+
// Build set of allowed language identifiers from context extensions
|
|
602
|
+
const allowedLanguages = new Set();
|
|
603
|
+
for (const ext of contextExtensions) {
|
|
604
|
+
const languages = (0, language_map_1.getLanguageAliasesForExtension)(ext);
|
|
605
|
+
languages.forEach(lang => allowedLanguages.add(lang.toLowerCase()));
|
|
606
|
+
}
|
|
607
|
+
if (allowedLanguages.size === 0) {
|
|
608
|
+
// No mapped languages found, return content unchanged
|
|
609
|
+
return content;
|
|
610
|
+
}
|
|
611
|
+
if (context.verbose) {
|
|
612
|
+
console.log(`Filtering code blocks for languages: ${Array.from(allowedLanguages).join(', ')}`);
|
|
613
|
+
}
|
|
614
|
+
// Parse and filter code blocks
|
|
615
|
+
const result = this.filterCodeBlocks(content, allowedLanguages, context.verbose);
|
|
616
|
+
if (context.verbose && result.removedCount > 0) {
|
|
617
|
+
console.log(`Removed ${result.removedCount} non-matching code block(s)`);
|
|
618
|
+
}
|
|
619
|
+
return result.content;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Extract file extensions from context metadata.
|
|
623
|
+
* Context can be a single file path string or an array of file paths.
|
|
624
|
+
*/
|
|
625
|
+
extractContextExtensions(contextValue) {
|
|
626
|
+
const extensions = new Set();
|
|
627
|
+
if (!contextValue) {
|
|
628
|
+
return extensions;
|
|
629
|
+
}
|
|
630
|
+
// Normalize to array
|
|
631
|
+
const contextFiles = Array.isArray(contextValue) ? contextValue : [contextValue];
|
|
632
|
+
for (const filePath of contextFiles) {
|
|
633
|
+
if (typeof filePath === 'string') {
|
|
634
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
635
|
+
if (ext) {
|
|
636
|
+
extensions.add(ext);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return extensions;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Filter code blocks in markdown content, keeping only those with matching languages.
|
|
644
|
+
*
|
|
645
|
+
* @param content - Markdown content with code blocks
|
|
646
|
+
* @param allowedLanguages - Set of allowed language identifiers (lowercase)
|
|
647
|
+
* @param verbose - Whether to log filtering actions
|
|
648
|
+
* @returns Object with filtered content and count of removed blocks
|
|
649
|
+
*/
|
|
650
|
+
filterCodeBlocks(content, allowedLanguages, verbose) {
|
|
651
|
+
// Regex to match fenced code blocks: ```language\n...content...\n```
|
|
652
|
+
const codeBlockRegex = /```(\w*)\n([\s\S]*?)```/g;
|
|
653
|
+
let removedCount = 0;
|
|
654
|
+
const filteredContent = content.replace(codeBlockRegex, (match, language, blockContent) => {
|
|
655
|
+
const lang = (language || '').toLowerCase().trim();
|
|
656
|
+
// If no language specified, keep the block (could be plain text/output)
|
|
657
|
+
if (!lang) {
|
|
658
|
+
return match;
|
|
659
|
+
}
|
|
660
|
+
// Check if language is in allowed set
|
|
661
|
+
if (allowedLanguages.has(lang)) {
|
|
662
|
+
return match; // Keep matching blocks
|
|
663
|
+
}
|
|
664
|
+
// Remove non-matching code blocks
|
|
665
|
+
removedCount++;
|
|
666
|
+
if (verbose) {
|
|
667
|
+
const preview = blockContent.substring(0, 50).replace(/\n/g, '\\n');
|
|
668
|
+
console.log(` - Removing \`\`\`${lang} block: "${preview}..."`);
|
|
669
|
+
}
|
|
670
|
+
// Remove the entire code block (return empty string)
|
|
671
|
+
return '';
|
|
672
|
+
});
|
|
673
|
+
// Clean up multiple consecutive blank lines that may result from removal
|
|
674
|
+
const cleanedContent = filteredContent.replace(/\n{3,}/g, '\n\n');
|
|
675
|
+
return { content: cleanedContent, removedCount };
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Process template with Jinja2/Nunjucks.
|
|
679
|
+
* Uses a context-aware environment with PrompdLoader to support {% include %}.
|
|
680
|
+
*/
|
|
681
|
+
async processTemplate(context, content) {
|
|
682
|
+
if (!content) {
|
|
683
|
+
return content;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
// Create a context-aware Nunjucks environment with the PrompdLoader
|
|
687
|
+
// This enables {% include "file.prmd" %} directives
|
|
688
|
+
const baseDir = context.fileSystem.dirname(context.sourceFile);
|
|
689
|
+
const loader = new prompd_loader_1.PrompdLoader({
|
|
690
|
+
fileSystem: context.fileSystem,
|
|
691
|
+
baseDir: baseDir,
|
|
692
|
+
verbose: context.verbose,
|
|
693
|
+
maxDepth: 10
|
|
694
|
+
});
|
|
695
|
+
const env = new nunjucks.Environment(loader, {
|
|
696
|
+
autoescape: false,
|
|
697
|
+
throwOnUndefined: false,
|
|
698
|
+
trimBlocks: true,
|
|
699
|
+
lstripBlocks: true,
|
|
700
|
+
tags: {
|
|
701
|
+
blockStart: '{%',
|
|
702
|
+
blockEnd: '%}',
|
|
703
|
+
variableStart: '{{',
|
|
704
|
+
variableEnd: '}}',
|
|
705
|
+
commentStart: '{#',
|
|
706
|
+
commentEnd: '#}'
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
// Register custom filters on this environment
|
|
710
|
+
this.registerFiltersOnEnv(env);
|
|
711
|
+
// Security: Set timeout for template rendering (prevent DoS)
|
|
712
|
+
const renderTimeout = 5000; // 5 seconds
|
|
713
|
+
const renderPromise = new Promise((resolve, reject) => {
|
|
714
|
+
try {
|
|
715
|
+
const result = env.renderString(content, context.parameters);
|
|
716
|
+
resolve(result);
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
reject(error);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
723
|
+
setTimeout(() => reject(new Error('Template rendering timeout')), renderTimeout);
|
|
724
|
+
});
|
|
725
|
+
content = await Promise.race([renderPromise, timeoutPromise]);
|
|
726
|
+
}
|
|
727
|
+
catch (error) {
|
|
728
|
+
// Report template syntax errors properly
|
|
729
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
730
|
+
// Try to extract line/column from Nunjucks error message
|
|
731
|
+
// Format: "(unknown path) [Line X, Column Y]" or similar
|
|
732
|
+
const lineMatch = errorMessage.match(/\[Line (\d+), Column (\d+)\]/i) ||
|
|
733
|
+
errorMessage.match(/line (\d+)/i);
|
|
734
|
+
const line = lineMatch ? parseInt(lineMatch[1], 10) : undefined;
|
|
735
|
+
const column = lineMatch && lineMatch[2] ? parseInt(lineMatch[2], 10) : undefined;
|
|
736
|
+
// Add as a compilation error (not just a warning)
|
|
737
|
+
context.addDiagnostic({
|
|
738
|
+
message: `Template syntax error: ${errorMessage}`,
|
|
739
|
+
severity: 'error',
|
|
740
|
+
source: 'template',
|
|
741
|
+
code: 'TEMPLATE_SYNTAX_ERROR',
|
|
742
|
+
line,
|
|
743
|
+
column
|
|
744
|
+
});
|
|
745
|
+
// Still fall back to simple substitution so user sees partial output
|
|
746
|
+
// but the error is now properly reported
|
|
747
|
+
content = this.enhancedSimpleSubstitution(content, context.parameters);
|
|
748
|
+
}
|
|
749
|
+
return content;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Register custom filters on a Nunjucks environment.
|
|
753
|
+
*/
|
|
754
|
+
registerFiltersOnEnv(env) {
|
|
755
|
+
// Override built-in string filters to handle undefined/null safely
|
|
756
|
+
// This prevents "Cannot read properties of undefined" errors when template
|
|
757
|
+
// variables are undefined during validation (e.g., package creation)
|
|
758
|
+
// trim - safe version
|
|
759
|
+
env.addFilter('trim', (str) => {
|
|
760
|
+
if (str === undefined || str === null)
|
|
761
|
+
return '';
|
|
762
|
+
return String(str).trim();
|
|
763
|
+
});
|
|
764
|
+
// lower - safe version
|
|
765
|
+
env.addFilter('lower', (str) => {
|
|
766
|
+
if (str === undefined || str === null)
|
|
767
|
+
return '';
|
|
768
|
+
return String(str).toLowerCase();
|
|
769
|
+
});
|
|
770
|
+
// upper - safe version
|
|
771
|
+
env.addFilter('upper', (str) => {
|
|
772
|
+
if (str === undefined || str === null)
|
|
773
|
+
return '';
|
|
774
|
+
return String(str).toUpperCase();
|
|
775
|
+
});
|
|
776
|
+
// capitalize - safe version
|
|
777
|
+
env.addFilter('capitalize', (str) => {
|
|
778
|
+
if (str === undefined || str === null)
|
|
779
|
+
return '';
|
|
780
|
+
const s = String(str);
|
|
781
|
+
return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
|
|
782
|
+
});
|
|
783
|
+
// title - safe version (capitalize each word)
|
|
784
|
+
env.addFilter('title', (str) => {
|
|
785
|
+
if (str === undefined || str === null)
|
|
786
|
+
return '';
|
|
787
|
+
return String(str).replace(/\b\w/g, c => c.toUpperCase());
|
|
788
|
+
});
|
|
789
|
+
// replace - safe version
|
|
790
|
+
env.addFilter('replace', (str, old, newStr, count) => {
|
|
791
|
+
if (str === undefined || str === null)
|
|
792
|
+
return '';
|
|
793
|
+
const s = String(str);
|
|
794
|
+
if (count !== undefined) {
|
|
795
|
+
// Replace only 'count' occurrences
|
|
796
|
+
let result = s;
|
|
797
|
+
let replaced = 0;
|
|
798
|
+
while (replaced < count) {
|
|
799
|
+
const idx = result.indexOf(old);
|
|
800
|
+
if (idx === -1)
|
|
801
|
+
break;
|
|
802
|
+
result = result.slice(0, idx) + newStr + result.slice(idx + old.length);
|
|
803
|
+
replaced++;
|
|
804
|
+
}
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
return s.split(old).join(newStr);
|
|
808
|
+
});
|
|
809
|
+
// striptags - safe version (remove HTML tags)
|
|
810
|
+
env.addFilter('striptags', (str) => {
|
|
811
|
+
if (str === undefined || str === null)
|
|
812
|
+
return '';
|
|
813
|
+
return String(str).replace(/<[^>]*>/g, '');
|
|
814
|
+
});
|
|
815
|
+
// urlencode - safe version
|
|
816
|
+
env.addFilter('urlencode', (str) => {
|
|
817
|
+
if (str === undefined || str === null)
|
|
818
|
+
return '';
|
|
819
|
+
return encodeURIComponent(String(str));
|
|
820
|
+
});
|
|
821
|
+
// fromcsv - Parse CSV string into array of objects
|
|
822
|
+
env.addFilter('fromcsv', (csvString) => {
|
|
823
|
+
if (!csvString || typeof csvString !== 'string') {
|
|
824
|
+
return [];
|
|
825
|
+
}
|
|
826
|
+
return this.parseCsv(csvString);
|
|
827
|
+
});
|
|
828
|
+
// fromjson - Parse JSON string into object/array
|
|
829
|
+
env.addFilter('fromjson', (jsonString) => {
|
|
830
|
+
if (!jsonString || typeof jsonString !== 'string') {
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
return JSON.parse(jsonString);
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
return null;
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
// tojson - Convert object to JSON string
|
|
841
|
+
env.addFilter('tojson', (obj, indent) => {
|
|
842
|
+
try {
|
|
843
|
+
return JSON.stringify(obj, null, indent);
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
return '{}';
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
// lines - Split string into array of lines
|
|
850
|
+
env.addFilter('lines', (str) => {
|
|
851
|
+
if (!str || typeof str !== 'string') {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
return str.split(/\r?\n/);
|
|
855
|
+
});
|
|
856
|
+
// dedent - Remove common leading whitespace from text
|
|
857
|
+
env.addFilter('dedent', (str) => {
|
|
858
|
+
if (!str || typeof str !== 'string') {
|
|
859
|
+
return '';
|
|
860
|
+
}
|
|
861
|
+
const lines = str.split(/\r?\n/);
|
|
862
|
+
let minIndent = Infinity;
|
|
863
|
+
for (const line of lines) {
|
|
864
|
+
if (line.trim().length === 0)
|
|
865
|
+
continue;
|
|
866
|
+
const match = line.match(/^(\s*)/);
|
|
867
|
+
if (match && match[1].length < minIndent) {
|
|
868
|
+
minIndent = match[1].length;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (minIndent === Infinity)
|
|
872
|
+
minIndent = 0;
|
|
873
|
+
return lines.map(line => line.slice(minIndent)).join('\n');
|
|
874
|
+
});
|
|
875
|
+
// truncate - Truncate string with ellipsis
|
|
876
|
+
env.addFilter('truncate', (str, length = 80, suffix = '...') => {
|
|
877
|
+
if (!str || typeof str !== 'string') {
|
|
878
|
+
return '';
|
|
879
|
+
}
|
|
880
|
+
if (str.length <= length) {
|
|
881
|
+
return str;
|
|
882
|
+
}
|
|
883
|
+
return str.slice(0, length - suffix.length) + suffix;
|
|
884
|
+
});
|
|
885
|
+
// codeblock - Wrap content in fenced code block
|
|
886
|
+
env.addFilter('codeblock', (str, language = '') => {
|
|
887
|
+
if (!str || typeof str !== 'string') {
|
|
888
|
+
return '';
|
|
889
|
+
}
|
|
890
|
+
return '```' + language + '\n' + str + '\n```';
|
|
891
|
+
});
|
|
892
|
+
// unique - Remove duplicate items from array
|
|
893
|
+
env.addFilter('unique', (arr) => {
|
|
894
|
+
if (!Array.isArray(arr)) {
|
|
895
|
+
return [];
|
|
896
|
+
}
|
|
897
|
+
return [...new Set(arr)];
|
|
898
|
+
});
|
|
899
|
+
// pluck - Extract single field from array of objects
|
|
900
|
+
env.addFilter('pluck', (arr, field) => {
|
|
901
|
+
if (!Array.isArray(arr)) {
|
|
902
|
+
return [];
|
|
903
|
+
}
|
|
904
|
+
return arr.map(item => item?.[field]).filter(v => v !== undefined);
|
|
905
|
+
});
|
|
906
|
+
// where - Filter objects by field value
|
|
907
|
+
env.addFilter('where', (arr, field, value) => {
|
|
908
|
+
if (!Array.isArray(arr)) {
|
|
909
|
+
return [];
|
|
910
|
+
}
|
|
911
|
+
return arr.filter(item => item?.[field] === value);
|
|
912
|
+
});
|
|
913
|
+
// groupby - Group array items by field value
|
|
914
|
+
env.addFilter('groupby', (arr, field) => {
|
|
915
|
+
if (!Array.isArray(arr)) {
|
|
916
|
+
return {};
|
|
917
|
+
}
|
|
918
|
+
const result = {};
|
|
919
|
+
for (const item of arr) {
|
|
920
|
+
const key = String(item?.[field] ?? 'undefined');
|
|
921
|
+
if (!result[key]) {
|
|
922
|
+
result[key] = [];
|
|
923
|
+
}
|
|
924
|
+
result[key].push(item);
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
927
|
+
});
|
|
928
|
+
// shuffle - Randomize array order
|
|
929
|
+
env.addFilter('shuffle', (arr) => {
|
|
930
|
+
if (!Array.isArray(arr)) {
|
|
931
|
+
return [];
|
|
932
|
+
}
|
|
933
|
+
const shuffled = [...arr];
|
|
934
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
935
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
936
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
937
|
+
}
|
|
938
|
+
return shuffled;
|
|
939
|
+
});
|
|
940
|
+
// sample - Pick random N items from array
|
|
941
|
+
env.addFilter('sample', (arr, count = 1) => {
|
|
942
|
+
if (!Array.isArray(arr)) {
|
|
943
|
+
return [];
|
|
944
|
+
}
|
|
945
|
+
const shuffled = [...arr];
|
|
946
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
947
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
948
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
949
|
+
}
|
|
950
|
+
return shuffled.slice(0, Math.min(count, shuffled.length));
|
|
951
|
+
});
|
|
952
|
+
// wordwrap - Wrap text at specified width
|
|
953
|
+
env.addFilter('wordwrap', (str, width = 80) => {
|
|
954
|
+
if (!str || typeof str !== 'string') {
|
|
955
|
+
return '';
|
|
956
|
+
}
|
|
957
|
+
const words = str.split(/\s+/);
|
|
958
|
+
const lines = [];
|
|
959
|
+
let currentLine = '';
|
|
960
|
+
for (const word of words) {
|
|
961
|
+
if (currentLine.length + word.length + 1 <= width) {
|
|
962
|
+
currentLine += (currentLine ? ' ' : '') + word;
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
if (currentLine)
|
|
966
|
+
lines.push(currentLine);
|
|
967
|
+
currentLine = word;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (currentLine)
|
|
971
|
+
lines.push(currentLine);
|
|
972
|
+
return lines.join('\n');
|
|
973
|
+
});
|
|
974
|
+
// bulletlist - Convert array/lines to bullet list
|
|
975
|
+
env.addFilter('bulletlist', (input) => {
|
|
976
|
+
const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
|
|
977
|
+
return items.filter(item => String(item).trim()).map(item => `- ${item}`).join('\n');
|
|
978
|
+
});
|
|
979
|
+
// numberedlist - Convert array/lines to numbered list
|
|
980
|
+
env.addFilter('numberedlist', (input) => {
|
|
981
|
+
const items = Array.isArray(input) ? input : String(input).split(/\r?\n/);
|
|
982
|
+
return items.filter(item => String(item).trim()).map((item, i) => `${i + 1}. ${item}`).join('\n');
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Enhanced simple substitution with nested property access.
|
|
987
|
+
* Supports both single brace {var} and double brace {{var}} syntax for backward compatibility.
|
|
988
|
+
*/
|
|
989
|
+
enhancedSimpleSubstitution(content, parameters) {
|
|
990
|
+
// Guard against undefined/null content
|
|
991
|
+
if (content === undefined || content === null) {
|
|
992
|
+
return '';
|
|
993
|
+
}
|
|
994
|
+
// Helper function to resolve nested property path
|
|
995
|
+
const resolveValue = (fullPath) => {
|
|
996
|
+
const parts = fullPath.split('.');
|
|
997
|
+
let value = parameters[parts[0]];
|
|
998
|
+
if (value === undefined || value === null) {
|
|
999
|
+
return null;
|
|
1000
|
+
}
|
|
1001
|
+
try {
|
|
1002
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1003
|
+
if (typeof value === 'object' && value !== null) {
|
|
1004
|
+
value = value[parts[i]];
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
if (value === undefined || value === null) {
|
|
1010
|
+
return null;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return String(value);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
return null;
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
// First pass: Handle double braces {{ obj.prop }} (Jinja2/Nunjucks standard)
|
|
1020
|
+
// Supports optional whitespace and whitespace trimming markers (-~)
|
|
1021
|
+
content = content.replace(/\{\{[-~]?\s*([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*[-~]?\}\}/g, (_match, fullPath) => {
|
|
1022
|
+
const resolved = resolveValue(fullPath);
|
|
1023
|
+
return resolved !== null ? resolved : `[Missing: ${fullPath}]`;
|
|
1024
|
+
});
|
|
1025
|
+
// Second pass: Handle single braces {obj.prop} (legacy syntax)
|
|
1026
|
+
// Only match single braces that aren't part of {{ }} or {% %}
|
|
1027
|
+
content = content.replace(/(?<!\{)\{([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\}(?!\})/g, (_match, fullPath) => {
|
|
1028
|
+
const resolved = resolveValue(fullPath);
|
|
1029
|
+
return resolved !== null ? resolved : `[Missing: ${fullPath}]`;
|
|
1030
|
+
});
|
|
1031
|
+
return content;
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Escape special regex characters.
|
|
1035
|
+
*/
|
|
1036
|
+
escapeRegex(str) {
|
|
1037
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1038
|
+
}
|
|
1039
|
+
getName() {
|
|
1040
|
+
return 'Template Processing';
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
exports.TemplateProcessingStage = TemplateProcessingStage;
|
|
1044
|
+
//# sourceMappingURL=template.js.map
|