@mistralys/persona-builder 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/cli.cjs +514 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +506 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +437 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +829 -0
- package/dist/index.d.ts +829 -0
- package/dist/index.js +404 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mistralys
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# AI Persona Builder
|
|
2
|
+
|
|
3
|
+
Build AI persona instruction files for **VS Code Chat** and **Claude Code** from YAML metadata and Markdown templates — with zero configuration friction.
|
|
4
|
+
|
|
5
|
+
Define your personas once as simple YAML + Markdown sources, and the library generates correctly formatted instruction files for both IDEs. A plugin system lets you inject custom frontmatter, run validators, or post-process output without touching the core engine.
|
|
6
|
+
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- **Dual-target output** — generates both `.agent.md` (VS Code) and `.md` (Claude Code) from a single source
|
|
10
|
+
- **YAML + Markdown templating** — separate metadata from content; merge them at build time with `{{variables}}`, `{{> partials}}`, and `{{#if}}` conditionals
|
|
11
|
+
- **Shared + per-suite partials** — reuse content fragments across personas with local overrides
|
|
12
|
+
- **Plugin architecture** — hook into context building, post-rendering, validation, and frontmatter generation
|
|
13
|
+
- **CI-friendly** — `--check` mode renders without writing; `--strict` exits non-zero on warnings
|
|
14
|
+
- **Programmatic & CLI** — use the `build()` API in scripts or run `persona-build` from the command line
|
|
15
|
+
- **Single dependency** — only `js-yaml` at runtime
|
|
16
|
+
|
|
17
|
+
## 📋 Requirements
|
|
18
|
+
|
|
19
|
+
- **Node.js** ≥ 18
|
|
20
|
+
|
|
21
|
+
## 🚀 Quick Start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @smor/persona-build
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Programmatic API
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { build } from '@smor/persona-build';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
const summary = await build({
|
|
34
|
+
suites: {
|
|
35
|
+
'my-suite': {
|
|
36
|
+
srcDir: path.resolve('./personas/my-suite'),
|
|
37
|
+
outVscode: path.resolve('./dist/vscode'),
|
|
38
|
+
outClaudeCode: path.resolve('./dist/claude-code'),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
sharedPartialsDir: path.resolve('./personas/shared/partials'),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(`Built ${summary.totalBuilt} persona(s), wrote ${summary.totalWritten} file(s).`);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Create a persona-build.config.js, then:
|
|
51
|
+
npx persona-build
|
|
52
|
+
|
|
53
|
+
# CI staleness check
|
|
54
|
+
npx persona-build --check --strict
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See the [CLI docs](docs/cli.md) for config file format and all flags.
|
|
58
|
+
|
|
59
|
+
## 📖 Documentation
|
|
60
|
+
|
|
61
|
+
| Guide | Description |
|
|
62
|
+
|-------|-------------|
|
|
63
|
+
| [Directory Convention](docs/directory-convention.md) | Expected source layout (`meta/`, `content/`, `partials/`) |
|
|
64
|
+
| [Template Syntax](docs/template-syntax.md) | Variables, partials, conditionals, and built-in context variables |
|
|
65
|
+
| [Configuration Reference](docs/configuration.md) | `BuildConfig`, `SuiteConfig`, and `BuildSummary` fields |
|
|
66
|
+
| [CLI Reference](docs/cli.md) | Command-line flags, config file format, and common patterns |
|
|
67
|
+
| [Plugins](docs/plugins.md) | `PersonaBuildPlugin` interface and examples |
|
|
68
|
+
| [Public API](docs/api.md) | All exported types and functions |
|
|
69
|
+
|
|
70
|
+
## 📄 License
|
|
71
|
+
|
|
72
|
+
MIT
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var module$1 = require('module');
|
|
5
|
+
var path2 = require('path');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var url = require('url');
|
|
8
|
+
var promises = require('fs/promises');
|
|
9
|
+
var yaml = require('js-yaml');
|
|
10
|
+
|
|
11
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
12
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
|
+
|
|
14
|
+
var path2__default = /*#__PURE__*/_interopDefault(path2);
|
|
15
|
+
var yaml__default = /*#__PURE__*/_interopDefault(yaml);
|
|
16
|
+
|
|
17
|
+
// src/engine/partials.ts
|
|
18
|
+
function resolvePartials(text, partialsMap, depth = 0) {
|
|
19
|
+
if (depth >= 2) return text;
|
|
20
|
+
return text.replace(/\{\{> ([\w-]+)\}\}/g, (match, name) => {
|
|
21
|
+
if (!(name in partialsMap)) {
|
|
22
|
+
console.warn(`[WARN] Partial not found: ${match}`);
|
|
23
|
+
return match;
|
|
24
|
+
}
|
|
25
|
+
return resolvePartials(partialsMap[name], partialsMap, depth + 1).trimEnd();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/engine/conditionals.ts
|
|
30
|
+
function resolveConditionals(text, context) {
|
|
31
|
+
return text.replace(
|
|
32
|
+
/\n*\{\{#if (\w+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}\n*/g,
|
|
33
|
+
(_match, flag, inner, elseInner) => {
|
|
34
|
+
if (context[flag]) {
|
|
35
|
+
return "\n" + inner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
|
|
36
|
+
}
|
|
37
|
+
if (elseInner !== void 0) {
|
|
38
|
+
return "\n" + elseInner.replace(/^\n+/, "").replace(/\n+$/, "") + "\n";
|
|
39
|
+
}
|
|
40
|
+
return "\n";
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/engine/variables.ts
|
|
46
|
+
function resolveVariables(text, context, filename) {
|
|
47
|
+
return text.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
|
48
|
+
if (varName in context && context[varName] !== void 0) {
|
|
49
|
+
return String(context[varName]);
|
|
50
|
+
}
|
|
51
|
+
console.warn(`[WARN] Unresolved variable: ${match} in ${filename}`);
|
|
52
|
+
return match;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/engine/postProcessor.ts
|
|
57
|
+
function collapseBlankLines(text) {
|
|
58
|
+
return text.replace(/\n{4,}/g, "\n\n\n");
|
|
59
|
+
}
|
|
60
|
+
function ensureBlankLineBeforeHeadings(text) {
|
|
61
|
+
let result = text.replace(/([^\n])\n(#{1,6} )/g, "$1\n\n$2");
|
|
62
|
+
result = result.replace(/([^\n])\n(---)\n/g, "$1\n\n$2\n");
|
|
63
|
+
result = result.replace(/\n(---)\n([^\n])/g, "\n$1\n\n$2");
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
function normalizeNewlines(text) {
|
|
67
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/engine/serializer.ts
|
|
71
|
+
function serializeTools(tools) {
|
|
72
|
+
return "[" + tools.map((t) => `'${t}'`).join(", ") + "]";
|
|
73
|
+
}
|
|
74
|
+
function serializeToolsList(tools) {
|
|
75
|
+
return tools.map((t) => `'${t}'`).join(", ");
|
|
76
|
+
}
|
|
77
|
+
async function loadPartials(dir) {
|
|
78
|
+
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
79
|
+
const mdFiles = entries.filter(
|
|
80
|
+
(entry) => entry.isFile() && entry.name.endsWith(".md")
|
|
81
|
+
);
|
|
82
|
+
const pairs = await Promise.all(
|
|
83
|
+
mdFiles.map(async (entry) => {
|
|
84
|
+
const stem = entry.name.slice(0, -".md".length);
|
|
85
|
+
const filePath = path2__default.default.join(dir, entry.name);
|
|
86
|
+
const content = await promises.readFile(filePath, "utf8");
|
|
87
|
+
return [stem, content];
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
return Object.fromEntries(pairs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/plugins/runner.ts
|
|
94
|
+
function runSuiteInit(plugins, suite, sharedMeta) {
|
|
95
|
+
for (const plugin of plugins) {
|
|
96
|
+
if (typeof plugin.onSuiteInit === "function") {
|
|
97
|
+
plugin.onSuiteInit(suite, sharedMeta);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function runBuildContext(plugins, ctx, persona, suite) {
|
|
102
|
+
let accumulated = ctx;
|
|
103
|
+
for (const plugin of plugins) {
|
|
104
|
+
if (typeof plugin.onBuildContext === "function") {
|
|
105
|
+
accumulated = plugin.onBuildContext(accumulated, persona, suite);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return accumulated;
|
|
109
|
+
}
|
|
110
|
+
function runPostRender(plugins, rendered, persona, target) {
|
|
111
|
+
let output = rendered;
|
|
112
|
+
for (const plugin of plugins) {
|
|
113
|
+
if (typeof plugin.onPostRender === "function") {
|
|
114
|
+
output = plugin.onPostRender(output, persona, target);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return output;
|
|
118
|
+
}
|
|
119
|
+
function runValidate(plugins, persona, suite) {
|
|
120
|
+
const results = [];
|
|
121
|
+
for (const plugin of plugins) {
|
|
122
|
+
if (typeof plugin.onValidate === "function") {
|
|
123
|
+
const pluginResults = plugin.onValidate(persona, suite);
|
|
124
|
+
results.push(...pluginResults);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return results;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/builders/frontmatter.ts
|
|
131
|
+
var DEFAULT_FRONTMATTER_VSCODE = `---
|
|
132
|
+
name: '{{name}} v{{version}}'
|
|
133
|
+
description: '{{description}}'
|
|
134
|
+
tools: [{{tools_list}}]
|
|
135
|
+
---`;
|
|
136
|
+
var DEFAULT_FRONTMATTER_CLAUDE_CODE = `---
|
|
137
|
+
name: {{cc_file_name_stem}}
|
|
138
|
+
permissionMode: {{cc_permission_mode}}
|
|
139
|
+
model: {{cc_model}}
|
|
140
|
+
memory: {{cc_memory}}
|
|
141
|
+
allowedTools: [{{cc_tools_list}}]
|
|
142
|
+
---`;
|
|
143
|
+
function resolveFrontmatterTemplate(target, plugins, configTemplates) {
|
|
144
|
+
for (const plugin of plugins) {
|
|
145
|
+
if (plugin.frontmatterTemplates && target in plugin.frontmatterTemplates) {
|
|
146
|
+
const tpl = plugin.frontmatterTemplates[target];
|
|
147
|
+
if (tpl !== void 0) return tpl;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (configTemplates && target in configTemplates) {
|
|
151
|
+
const tpl = configTemplates[target];
|
|
152
|
+
if (tpl !== void 0) return tpl;
|
|
153
|
+
}
|
|
154
|
+
return target === "vscode" ? DEFAULT_FRONTMATTER_VSCODE : DEFAULT_FRONTMATTER_CLAUDE_CODE;
|
|
155
|
+
}
|
|
156
|
+
function renderFrontmatter(template, context, filename) {
|
|
157
|
+
let rendered = resolveConditionals(template, context);
|
|
158
|
+
rendered = resolveVariables(rendered, context, filename);
|
|
159
|
+
return rendered;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/builders/persona-builder.ts
|
|
163
|
+
async function discoverSuitePersonaYamls(suiteConfig) {
|
|
164
|
+
const metaSubdir = suiteConfig.metaSubdir ?? "meta";
|
|
165
|
+
const metaDir = path2__default.default.join(suiteConfig.srcDir, metaSubdir);
|
|
166
|
+
const entries = await promises.readdir(metaDir, { withFileTypes: true });
|
|
167
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".yaml") && !e.name.startsWith("_")).map((e) => path2__default.default.join(metaDir, e.name)).sort();
|
|
168
|
+
}
|
|
169
|
+
async function loadRawYaml(filePath) {
|
|
170
|
+
if (!fs.existsSync(filePath)) return {};
|
|
171
|
+
const raw = await promises.readFile(filePath, "utf8");
|
|
172
|
+
const parsed = yaml__default.default.load(raw);
|
|
173
|
+
if (parsed === null || parsed === void 0) return {};
|
|
174
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
175
|
+
return parsed;
|
|
176
|
+
}
|
|
177
|
+
async function loadPersonaYaml(yamlPath) {
|
|
178
|
+
const raw = await promises.readFile(yamlPath, "utf8");
|
|
179
|
+
const parsed = yaml__default.default.load(raw);
|
|
180
|
+
if (parsed === null || parsed === void 0 || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
181
|
+
throw new Error(`buildPersona: expected a YAML object in "${yamlPath}"`);
|
|
182
|
+
}
|
|
183
|
+
const record = parsed;
|
|
184
|
+
if (!record["name"]) {
|
|
185
|
+
record["name"] = path2__default.default.basename(yamlPath, ".yaml");
|
|
186
|
+
}
|
|
187
|
+
return record;
|
|
188
|
+
}
|
|
189
|
+
function buildContext(personaMeta, sharedMeta) {
|
|
190
|
+
const version = typeof personaMeta["version"] === "string" ? personaMeta["version"] : typeof sharedMeta["default_version"] === "string" ? sharedMeta["default_version"] : "0.0.0";
|
|
191
|
+
const merged = {
|
|
192
|
+
...sharedMeta,
|
|
193
|
+
...personaMeta,
|
|
194
|
+
version
|
|
195
|
+
};
|
|
196
|
+
const tools = Array.isArray(merged["tools"]) ? merged["tools"] : [];
|
|
197
|
+
if (!("tools_list" in merged)) {
|
|
198
|
+
merged["tools_list"] = serializeToolsList(tools);
|
|
199
|
+
}
|
|
200
|
+
if (!("tools_json" in merged)) {
|
|
201
|
+
merged["tools_json"] = serializeTools(tools);
|
|
202
|
+
}
|
|
203
|
+
const ccTools = Array.isArray(merged["cc_tools"]) ? merged["cc_tools"] : tools;
|
|
204
|
+
if (!("cc_tools_list" in merged)) {
|
|
205
|
+
merged["cc_tools_list"] = serializeToolsList(ccTools);
|
|
206
|
+
}
|
|
207
|
+
if (!("cc_tools_json" in merged)) {
|
|
208
|
+
merged["cc_tools_json"] = serializeTools(ccTools);
|
|
209
|
+
}
|
|
210
|
+
if (!("cc_file_name_stem" in merged) && typeof merged["cc_file_name"] === "string") {
|
|
211
|
+
const ccFileName = merged["cc_file_name"];
|
|
212
|
+
merged["cc_file_name_stem"] = ccFileName.replace(/\.md$/, "");
|
|
213
|
+
}
|
|
214
|
+
return merged;
|
|
215
|
+
}
|
|
216
|
+
async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target) {
|
|
217
|
+
const personaMeta = await loadPersonaYaml(personaYamlPath);
|
|
218
|
+
let context = buildContext(personaMeta, sharedMeta);
|
|
219
|
+
const personaMetaTyped = personaMeta;
|
|
220
|
+
context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig);
|
|
221
|
+
const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter);
|
|
222
|
+
const contentBasename = path2__default.default.basename(personaYamlPath, ".yaml") + ".md";
|
|
223
|
+
const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);
|
|
224
|
+
const contentSubdir = suiteConfig.contentSubdir ?? "content";
|
|
225
|
+
const contentPath = path2__default.default.join(suiteConfig.srcDir, contentSubdir, contentBasename);
|
|
226
|
+
const bodyTemplate = normalizeNewlines(await promises.readFile(contentPath, "utf8"));
|
|
227
|
+
let body = resolvePartials(bodyTemplate, partialsMap);
|
|
228
|
+
body = resolveConditionals(body, context);
|
|
229
|
+
body = resolveVariables(body, context, contentBasename);
|
|
230
|
+
body = collapseBlankLines(body);
|
|
231
|
+
body = ensureBlankLineBeforeHeadings(body);
|
|
232
|
+
body = body.trimEnd();
|
|
233
|
+
let output = normalizeNewlines(`${frontmatter}
|
|
234
|
+
|
|
235
|
+
${body}
|
|
236
|
+
`);
|
|
237
|
+
output = runPostRender(plugins, output, personaMetaTyped, target);
|
|
238
|
+
const validationResults = runValidate(plugins, personaMetaTyped, suiteConfig);
|
|
239
|
+
const outputDir = target === "vscode" ? suiteConfig.outVscode : suiteConfig.outClaudeCode;
|
|
240
|
+
let outputBasename;
|
|
241
|
+
if (target === "vscode" && typeof context["vs_file_name"] === "string") {
|
|
242
|
+
outputBasename = context["vs_file_name"];
|
|
243
|
+
} else if (target === "claude-code" && typeof context["cc_file_name"] === "string") {
|
|
244
|
+
outputBasename = context["cc_file_name"];
|
|
245
|
+
} else {
|
|
246
|
+
outputBasename = contentBasename;
|
|
247
|
+
}
|
|
248
|
+
const outputPath = path2__default.default.join(outputDir, outputBasename);
|
|
249
|
+
const check = config.check ?? false;
|
|
250
|
+
let written = false;
|
|
251
|
+
if (!check) {
|
|
252
|
+
await promises.mkdir(outputDir, { recursive: true });
|
|
253
|
+
await promises.writeFile(outputPath, output, "utf8");
|
|
254
|
+
written = true;
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
suite: suiteName,
|
|
258
|
+
target,
|
|
259
|
+
personaYamlPath,
|
|
260
|
+
outputPath,
|
|
261
|
+
content: output,
|
|
262
|
+
validationResults,
|
|
263
|
+
written
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async function buildSuite(suiteName, suiteConfig, config, plugins, target) {
|
|
267
|
+
const metaSubdir = suiteConfig.metaSubdir ?? "meta";
|
|
268
|
+
const sharedYamlPath = path2__default.default.join(suiteConfig.srcDir, metaSubdir, "_shared.yaml");
|
|
269
|
+
const sharedMeta = await loadRawYaml(sharedYamlPath);
|
|
270
|
+
let partialsMap = {};
|
|
271
|
+
if (config.sharedPartialsDir && fs.existsSync(config.sharedPartialsDir)) {
|
|
272
|
+
partialsMap = { ...partialsMap, ...await loadPartials(config.sharedPartialsDir) };
|
|
273
|
+
}
|
|
274
|
+
const partialsSubdir = suiteConfig.partialsSubdir ?? "partials";
|
|
275
|
+
const suitePartialsDir = path2__default.default.join(suiteConfig.srcDir, partialsSubdir);
|
|
276
|
+
if (fs.existsSync(suitePartialsDir)) {
|
|
277
|
+
partialsMap = { ...partialsMap, ...await loadPartials(suitePartialsDir) };
|
|
278
|
+
}
|
|
279
|
+
runSuiteInit(plugins, suiteConfig, sharedMeta);
|
|
280
|
+
const personaYamlPaths = await discoverSuitePersonaYamls(suiteConfig);
|
|
281
|
+
const results = [];
|
|
282
|
+
for (const yamlPath of personaYamlPaths) {
|
|
283
|
+
const result = await buildPersona(
|
|
284
|
+
yamlPath,
|
|
285
|
+
suiteName,
|
|
286
|
+
suiteConfig,
|
|
287
|
+
sharedMeta,
|
|
288
|
+
partialsMap,
|
|
289
|
+
config,
|
|
290
|
+
plugins,
|
|
291
|
+
target
|
|
292
|
+
);
|
|
293
|
+
results.push(result);
|
|
294
|
+
}
|
|
295
|
+
return results;
|
|
296
|
+
}
|
|
297
|
+
async function build(config) {
|
|
298
|
+
const plugins = config.plugins ?? [];
|
|
299
|
+
const targets = config.targets ?? ["vscode", "claude-code"];
|
|
300
|
+
const allResults = [];
|
|
301
|
+
for (const [suiteName, suiteConfig] of Object.entries(config.suites)) {
|
|
302
|
+
for (const target of targets) {
|
|
303
|
+
const suiteResults = await buildSuite(suiteName, suiteConfig, config, plugins, target);
|
|
304
|
+
allResults.push(...suiteResults);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const strictFailures = config.strict ? allResults.flatMap(
|
|
308
|
+
(r) => r.validationResults.filter(
|
|
309
|
+
(v) => v.severity === "error" || v.severity === "warning"
|
|
310
|
+
)
|
|
311
|
+
) : [];
|
|
312
|
+
const success = !config.strict || strictFailures.length === 0;
|
|
313
|
+
const summary = {
|
|
314
|
+
success,
|
|
315
|
+
results: allResults,
|
|
316
|
+
strictFailures,
|
|
317
|
+
totalBuilt: allResults.length,
|
|
318
|
+
totalWritten: allResults.filter((r) => r.written).length
|
|
319
|
+
};
|
|
320
|
+
if (config.strict && !success) {
|
|
321
|
+
const messages = strictFailures.map((f) => `[${f.severity}] ${f.message}`).join("\n");
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Build failed in strict mode \u2014 ${strictFailures.length} validation issue(s):
|
|
324
|
+
${messages}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return summary;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/cli.ts
|
|
331
|
+
var _pkgRequire = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
332
|
+
var VERSION = _pkgRequire("../package.json").version;
|
|
333
|
+
var USAGE = `
|
|
334
|
+
@smor/persona-build v${VERSION}
|
|
335
|
+
|
|
336
|
+
Build AI persona documents from YAML metadata and Markdown content templates.
|
|
337
|
+
|
|
338
|
+
USAGE
|
|
339
|
+
persona-build [options]
|
|
340
|
+
|
|
341
|
+
OPTIONS
|
|
342
|
+
--config <path> Path to the build config file.
|
|
343
|
+
Supports .js (ESM), .cjs, and .json formats.
|
|
344
|
+
Default: persona-build.config.js in the current directory.
|
|
345
|
+
--check Render personas but skip writing output files.
|
|
346
|
+
Always exits 0 on its own. Combine with --strict to
|
|
347
|
+
exit 1 when validators report errors or warnings.
|
|
348
|
+
--strict Exit 1 if any validation result has severity 'error'
|
|
349
|
+
or 'warning'.
|
|
350
|
+
--help Show this help message and exit.
|
|
351
|
+
--version Print the package version and exit.
|
|
352
|
+
|
|
353
|
+
EXAMPLES
|
|
354
|
+
persona-build # Build with default config
|
|
355
|
+
persona-build --config ./my-config.js # Build with a custom config
|
|
356
|
+
persona-build --check # CI staleness check (no file writes)
|
|
357
|
+
persona-build --strict # Fail on warnings or errors
|
|
358
|
+
persona-build --check --strict # Safe CI check \u2014 no writes + strict
|
|
359
|
+
`.trim();
|
|
360
|
+
function parseArgs(argv) {
|
|
361
|
+
const args = argv.slice(2);
|
|
362
|
+
const result = {
|
|
363
|
+
configPath: void 0,
|
|
364
|
+
check: false,
|
|
365
|
+
strict: false,
|
|
366
|
+
help: false,
|
|
367
|
+
version: false
|
|
368
|
+
};
|
|
369
|
+
let i = 0;
|
|
370
|
+
while (i < args.length) {
|
|
371
|
+
const arg = args[i];
|
|
372
|
+
switch (arg) {
|
|
373
|
+
case "--help":
|
|
374
|
+
case "-h":
|
|
375
|
+
result.help = true;
|
|
376
|
+
break;
|
|
377
|
+
case "--version":
|
|
378
|
+
case "-v":
|
|
379
|
+
result.version = true;
|
|
380
|
+
break;
|
|
381
|
+
case "--check":
|
|
382
|
+
result.check = true;
|
|
383
|
+
break;
|
|
384
|
+
case "--strict":
|
|
385
|
+
result.strict = true;
|
|
386
|
+
break;
|
|
387
|
+
case "--config": {
|
|
388
|
+
const next = args[i + 1];
|
|
389
|
+
if (!next || next.startsWith("--")) {
|
|
390
|
+
console.error("Error: --config requires a path argument.");
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
result.configPath = next;
|
|
394
|
+
i++;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
default:
|
|
398
|
+
if (arg.startsWith("--")) {
|
|
399
|
+
console.warn(`Warning: Unknown flag "${arg}" \u2014 ignored.`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
i++;
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
function resolveConfigPath(cliValue) {
|
|
407
|
+
if (cliValue) {
|
|
408
|
+
const resolved = path2__default.default.resolve(cliValue);
|
|
409
|
+
if (!fs.existsSync(resolved)) {
|
|
410
|
+
console.error(`Error: Config file not found: ${resolved}`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
return resolved;
|
|
414
|
+
}
|
|
415
|
+
const candidates = [
|
|
416
|
+
"persona-build.config.js",
|
|
417
|
+
"persona-build.config.cjs",
|
|
418
|
+
"persona-build.config.json"
|
|
419
|
+
];
|
|
420
|
+
for (const name of candidates) {
|
|
421
|
+
const candidate = path2__default.default.resolve(name);
|
|
422
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
423
|
+
}
|
|
424
|
+
console.error(
|
|
425
|
+
"Error: No config file found. Create persona-build.config.js in the current directory or pass --config <path>."
|
|
426
|
+
);
|
|
427
|
+
process.exit(1);
|
|
428
|
+
}
|
|
429
|
+
async function loadConfig(configPath) {
|
|
430
|
+
const ext = path2__default.default.extname(configPath).toLowerCase();
|
|
431
|
+
let rawConfig;
|
|
432
|
+
if (ext === ".cjs" || ext === ".json") {
|
|
433
|
+
const require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)));
|
|
434
|
+
rawConfig = require2(configPath);
|
|
435
|
+
} else {
|
|
436
|
+
const fileUrl = url.pathToFileURL(configPath).href;
|
|
437
|
+
const mod = await import(fileUrl);
|
|
438
|
+
rawConfig = mod.default ?? mod;
|
|
439
|
+
}
|
|
440
|
+
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
|
|
441
|
+
console.error(
|
|
442
|
+
`Error: Config file "${configPath}" must export a plain object (BuildConfig).`
|
|
443
|
+
);
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
const config = rawConfig;
|
|
447
|
+
if (!config.suites || typeof config.suites !== "object") {
|
|
448
|
+
console.error(
|
|
449
|
+
`Error: Config file "${configPath}" must have a "suites" property (record of suite configs).`
|
|
450
|
+
);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
return config;
|
|
454
|
+
}
|
|
455
|
+
function printSummary(summary, check) {
|
|
456
|
+
const mode = check ? " [check mode \u2014 no files written]" : "";
|
|
457
|
+
const status = summary.success ? "\u2713 Build succeeded" : "\u2717 Build failed";
|
|
458
|
+
console.log(`${status}${mode}`);
|
|
459
|
+
console.log(` Personas processed : ${summary.totalBuilt}`);
|
|
460
|
+
if (!check) {
|
|
461
|
+
console.log(` Files written : ${summary.totalWritten}`);
|
|
462
|
+
}
|
|
463
|
+
if (summary.strictFailures.length > 0) {
|
|
464
|
+
console.log(`
|
|
465
|
+
Validation failures (${summary.strictFailures.length}):`);
|
|
466
|
+
for (const f of summary.strictFailures) {
|
|
467
|
+
console.log(` [${f.severity}] ${f.message}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function main() {
|
|
472
|
+
const args = parseArgs(process.argv);
|
|
473
|
+
if (args.help) {
|
|
474
|
+
console.log(USAGE);
|
|
475
|
+
process.exit(0);
|
|
476
|
+
}
|
|
477
|
+
if (args.version) {
|
|
478
|
+
console.log(VERSION);
|
|
479
|
+
process.exit(0);
|
|
480
|
+
}
|
|
481
|
+
const configPath = resolveConfigPath(args.configPath);
|
|
482
|
+
let config;
|
|
483
|
+
try {
|
|
484
|
+
config = await loadConfig(configPath);
|
|
485
|
+
} catch (err) {
|
|
486
|
+
console.error(`Error loading config: ${err instanceof Error ? err.message : String(err)}`);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
if (args.check) config.check = true;
|
|
490
|
+
if (args.strict) config.strict = true;
|
|
491
|
+
let summary;
|
|
492
|
+
try {
|
|
493
|
+
summary = await build(config);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
if (err instanceof Error) {
|
|
496
|
+
console.error(`
|
|
497
|
+
${err.message}`);
|
|
498
|
+
} else {
|
|
499
|
+
console.error("Build failed with an unexpected error:", err);
|
|
500
|
+
}
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
printSummary(summary, config.check ?? false);
|
|
504
|
+
if (!summary.success) {
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
process.exit(0);
|
|
508
|
+
}
|
|
509
|
+
main().catch((err) => {
|
|
510
|
+
console.error("Unexpected error:", err);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
});
|
|
513
|
+
//# sourceMappingURL=cli.cjs.map
|
|
514
|
+
//# sourceMappingURL=cli.cjs.map
|