@safetnsr/md-pipe 0.2.0 → 0.3.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.
@@ -18,6 +18,61 @@ function findConfigFile(dir) {
18
18
  }
19
19
  return null;
20
20
  }
21
+ function parseTriggerMatch(raw, name) {
22
+ const match = {};
23
+ if (raw.path)
24
+ match.path = String(raw.path);
25
+ if (raw.frontmatter)
26
+ match.frontmatter = raw.frontmatter;
27
+ if (raw.frontmatter_changed) {
28
+ if (!Array.isArray(raw.frontmatter_changed)) {
29
+ throw new Error(`Config error: trigger '${name}' frontmatter_changed must be an array`);
30
+ }
31
+ match.frontmatter_changed = raw.frontmatter_changed.map(String);
32
+ }
33
+ if (raw.tags) {
34
+ if (!Array.isArray(raw.tags)) {
35
+ throw new Error(`Config error: trigger '${name}' tags must be an array`);
36
+ }
37
+ match.tags = raw.tags.map(String);
38
+ }
39
+ if (raw.content !== undefined)
40
+ match.content = String(raw.content);
41
+ if (raw.content_regex !== undefined)
42
+ match.content_regex = String(raw.content_regex);
43
+ return match;
44
+ }
45
+ function parsePipelineStep(raw, pipelineName, index) {
46
+ const step = {};
47
+ if (raw.run !== undefined)
48
+ step.run = String(raw.run);
49
+ if (raw['update-frontmatter'] !== undefined)
50
+ step['update-frontmatter'] = raw['update-frontmatter'];
51
+ if (raw.webhook !== undefined)
52
+ step.webhook = raw.webhook;
53
+ if (raw.copy !== undefined) {
54
+ if (typeof raw.copy === 'string') {
55
+ step.copy = { to: raw.copy };
56
+ }
57
+ else {
58
+ step.copy = raw.copy;
59
+ }
60
+ }
61
+ if (raw.template !== undefined)
62
+ step.template = raw.template;
63
+ if (raw.continue_on_error !== undefined)
64
+ step.continue_on_error = Boolean(raw.continue_on_error);
65
+ // Validate at least one step type is present
66
+ const hasType = step.run !== undefined ||
67
+ step['update-frontmatter'] !== undefined ||
68
+ step.webhook !== undefined ||
69
+ step.copy !== undefined ||
70
+ step.template !== undefined;
71
+ if (!hasType) {
72
+ throw new Error(`Config error: pipeline '${pipelineName}' step[${index}] has no recognized step type (run, update-frontmatter, webhook, copy, template)`);
73
+ }
74
+ return step;
75
+ }
21
76
  function loadConfig(configPath) {
22
77
  const raw = (0, fs_1.readFileSync)(configPath, 'utf-8');
23
78
  const parsed = yaml_1.default.parse(raw);
@@ -27,46 +82,54 @@ function loadConfig(configPath) {
27
82
  if (!parsed.watch || typeof parsed.watch !== 'string') {
28
83
  throw new Error(`Config error: 'watch' must be a string path (e.g. "./docs")`);
29
84
  }
30
- if (!Array.isArray(parsed.triggers) || parsed.triggers.length === 0) {
31
- throw new Error(`Config error: 'triggers' must be a non-empty array`);
85
+ const hasTriggers = Array.isArray(parsed.triggers) && parsed.triggers.length > 0;
86
+ const hasPipelines = Array.isArray(parsed.pipelines) && parsed.pipelines.length > 0;
87
+ if (!hasTriggers && !hasPipelines) {
88
+ throw new Error(`Config error: must have at least one 'triggers' entry or 'pipelines' entry`);
32
89
  }
33
90
  const configDir = (0, path_1.dirname)(configPath);
34
91
  const triggers = [];
35
- for (let i = 0; i < parsed.triggers.length; i++) {
36
- const t = parsed.triggers[i];
37
- if (!t.name || typeof t.name !== 'string') {
38
- throw new Error(`Config error: trigger[${i}] must have a 'name' string`);
39
- }
40
- if (!t.match || typeof t.match !== 'object') {
41
- throw new Error(`Config error: trigger '${t.name}' must have a 'match' object`);
42
- }
43
- if (!t.run || typeof t.run !== 'string') {
44
- throw new Error(`Config error: trigger '${t.name}' must have a 'run' string`);
45
- }
46
- const match = {};
47
- if (t.match.path)
48
- match.path = String(t.match.path);
49
- if (t.match.frontmatter)
50
- match.frontmatter = t.match.frontmatter;
51
- if (t.match.frontmatter_changed) {
52
- if (!Array.isArray(t.match.frontmatter_changed)) {
53
- throw new Error(`Config error: trigger '${t.name}' frontmatter_changed must be an array`);
92
+ const pipelines = [];
93
+ // Parse legacy triggers
94
+ if (hasTriggers) {
95
+ for (let i = 0; i < parsed.triggers.length; i++) {
96
+ const t = parsed.triggers[i];
97
+ if (!t.name || typeof t.name !== 'string') {
98
+ throw new Error(`Config error: trigger[${i}] must have a 'name' string`);
99
+ }
100
+ if (!t.match || typeof t.match !== 'object') {
101
+ throw new Error(`Config error: trigger '${t.name}' must have a 'match' object`);
54
102
  }
55
- match.frontmatter_changed = t.match.frontmatter_changed.map(String);
103
+ if (!t.run || typeof t.run !== 'string') {
104
+ throw new Error(`Config error: trigger '${t.name}' must have a 'run' string`);
105
+ }
106
+ const match = parseTriggerMatch(t.match, t.name);
107
+ const cwd = t.cwd === 'project' ? 'project' : (t.cwd === 'file' ? 'file' : undefined);
108
+ triggers.push({ name: t.name, match, run: t.run, ...(cwd ? { cwd } : {}) });
56
109
  }
57
- if (t.match.tags) {
58
- if (!Array.isArray(t.match.tags)) {
59
- throw new Error(`Config error: trigger '${t.name}' tags must be an array`);
110
+ }
111
+ // Parse pipelines
112
+ if (hasPipelines) {
113
+ for (let i = 0; i < parsed.pipelines.length; i++) {
114
+ const p = parsed.pipelines[i];
115
+ if (!p.name || typeof p.name !== 'string') {
116
+ throw new Error(`Config error: pipeline[${i}] must have a 'name' string`);
60
117
  }
61
- match.tags = t.match.tags.map(String);
118
+ if (!p.trigger || typeof p.trigger !== 'object') {
119
+ throw new Error(`Config error: pipeline '${p.name}' must have a 'trigger' object`);
120
+ }
121
+ if (!Array.isArray(p.steps) || p.steps.length === 0) {
122
+ throw new Error(`Config error: pipeline '${p.name}' must have a non-empty 'steps' array`);
123
+ }
124
+ const trigger = parseTriggerMatch(p.trigger, p.name);
125
+ const steps = p.steps.map((s, j) => parsePipelineStep(s, p.name, j));
126
+ pipelines.push({
127
+ name: p.name,
128
+ trigger,
129
+ steps,
130
+ ...(p.continue_on_error ? { continue_on_error: true } : {}),
131
+ });
62
132
  }
63
- if (t.match.content !== undefined)
64
- match.content = String(t.match.content);
65
- if (t.match.content_regex !== undefined)
66
- match.content_regex = String(t.match.content_regex);
67
- // cwd option
68
- const cwd = t.cwd === 'project' ? 'project' : (t.cwd === 'file' ? 'file' : undefined);
69
- triggers.push({ name: t.name, match, run: t.run, ...(cwd ? { cwd } : {}) });
70
133
  }
71
134
  // Resolve watch path relative to config file directory
72
135
  const watchPath = (0, path_1.resolve)(configDir, parsed.watch);
@@ -74,7 +137,6 @@ function loadConfig(configPath) {
74
137
  let debounce;
75
138
  if (parsed.debounce !== undefined) {
76
139
  if (typeof parsed.debounce === 'string') {
77
- // Parse "500ms" or "1s"
78
140
  const ms = parsed.debounce.match(/^(\d+)\s*ms$/i);
79
141
  const s = parsed.debounce.match(/^(\d+)\s*s$/i);
80
142
  if (ms)
@@ -88,7 +150,7 @@ function loadConfig(configPath) {
88
150
  debounce = parsed.debounce;
89
151
  }
90
152
  }
91
- return { watch: watchPath, configDir, triggers, ...(debounce ? { debounce } : {}) };
153
+ return { watch: watchPath, configDir, triggers, pipelines, ...(debounce ? { debounce } : {}) };
92
154
  }
93
155
  function generateDefaultConfig() {
94
156
  return `# md-pipe configuration
@@ -96,6 +158,7 @@ function generateDefaultConfig() {
96
158
 
97
159
  watch: ./docs
98
160
 
161
+ # Legacy triggers (simple: match + run command)
99
162
  triggers:
100
163
  - name: publish
101
164
  match:
@@ -104,18 +167,17 @@ triggers:
104
167
  status: publish
105
168
  run: "echo Publishing $FILE"
106
169
 
107
- - name: reindex
108
- match:
109
- frontmatter_changed:
110
- - title
111
- - tags
112
- - category
113
- run: "echo Reindexing $FILE — changed fields: $DIFF"
114
-
115
- - name: urgent-notify
116
- match:
117
- tags:
118
- - urgent
119
- run: "echo URGENT: $FILE needs attention"
170
+ # Pipelines (v0.3+): multi-step content pipelines
171
+ # pipelines:
172
+ # - name: publish-post
173
+ # trigger:
174
+ # path: "posts/**"
175
+ # frontmatter: { status: publish }
176
+ # frontmatter_changed: [status]
177
+ # steps:
178
+ # - run: "echo Publishing {{fm.title}}"
179
+ # - update-frontmatter: { published_at: "{{now}}", published: true }
180
+ # - copy: { to: "./_site/posts" }
181
+ # - webhook: { url: "$WEBHOOK_URL" }
120
182
  `;
121
183
  }
@@ -0,0 +1,48 @@
1
+ import { TriggerMatch } from './config.js';
2
+ import { MatchResult } from './matcher.js';
3
+ import { type StepResult } from './steps/run.js';
4
+ export interface PipelineStepDef {
5
+ run?: string;
6
+ 'update-frontmatter'?: Record<string, unknown>;
7
+ webhook?: {
8
+ url: string;
9
+ method?: string;
10
+ headers?: Record<string, string>;
11
+ body?: unknown;
12
+ };
13
+ copy?: {
14
+ to: string;
15
+ flatten?: boolean;
16
+ };
17
+ template?: {
18
+ src: string;
19
+ out: string;
20
+ };
21
+ continue_on_error?: boolean;
22
+ }
23
+ export interface PipelineDef {
24
+ name: string;
25
+ trigger: TriggerMatch;
26
+ steps: PipelineStepDef[];
27
+ continue_on_error?: boolean;
28
+ }
29
+ export interface PipelineResult {
30
+ pipelineName: string;
31
+ filePath: string;
32
+ steps: StepResult[];
33
+ success: boolean;
34
+ durationMs: number;
35
+ }
36
+ /**
37
+ * Execute a full pipeline: run steps in order, build context as we go.
38
+ */
39
+ export declare function executePipeline(pipeline: PipelineDef, match: MatchResult, configDir: string, dryRun?: boolean): Promise<PipelineResult>;
40
+ /**
41
+ * Convert a legacy trigger (with `run` string) into a single-step pipeline.
42
+ * This enables backward compatibility.
43
+ */
44
+ export declare function triggerToPipeline(trigger: {
45
+ name: string;
46
+ match: TriggerMatch;
47
+ run: string;
48
+ }): PipelineDef;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executePipeline = executePipeline;
4
+ exports.triggerToPipeline = triggerToPipeline;
5
+ const path_1 = require("path");
6
+ const template_vars_js_1 = require("./template-vars.js");
7
+ const run_js_1 = require("./steps/run.js");
8
+ const update_frontmatter_js_1 = require("./steps/update-frontmatter.js");
9
+ const webhook_js_1 = require("./steps/webhook.js");
10
+ const copy_js_1 = require("./steps/copy.js");
11
+ const template_js_1 = require("./steps/template.js");
12
+ /**
13
+ * Detect the step type from a step definition.
14
+ */
15
+ function getStepType(step) {
16
+ if (step.run !== undefined)
17
+ return 'run';
18
+ if (step['update-frontmatter'] !== undefined)
19
+ return 'update-frontmatter';
20
+ if (step.webhook !== undefined)
21
+ return 'webhook';
22
+ if (step.copy !== undefined)
23
+ return 'copy';
24
+ if (step.template !== undefined)
25
+ return 'template';
26
+ return null;
27
+ }
28
+ /**
29
+ * Execute a single pipeline step.
30
+ */
31
+ async function executeStep(step, ctx, configDir, dryRun) {
32
+ const type = getStepType(step);
33
+ switch (type) {
34
+ case 'run':
35
+ return (0, run_js_1.executeRunStep)({ run: step.run }, ctx, (0, path_1.dirname)(ctx.file), dryRun);
36
+ case 'update-frontmatter':
37
+ return (0, update_frontmatter_js_1.executeUpdateFrontmatterStep)({ 'update-frontmatter': step['update-frontmatter'] }, ctx, dryRun);
38
+ case 'webhook':
39
+ return await (0, webhook_js_1.executeWebhookStep)({ webhook: step.webhook }, ctx, dryRun);
40
+ case 'copy':
41
+ return (0, copy_js_1.executeCopyStep)({ copy: step.copy }, ctx, configDir, dryRun);
42
+ case 'template':
43
+ return (0, template_js_1.executeTemplateStep)({ template: step.template }, ctx, configDir, dryRun);
44
+ default:
45
+ return {
46
+ type: 'unknown',
47
+ success: false,
48
+ stdout: '',
49
+ stderr: `Unknown step type in: ${JSON.stringify(step)}`,
50
+ exitCode: 1,
51
+ durationMs: 0,
52
+ };
53
+ }
54
+ }
55
+ /**
56
+ * Execute a full pipeline: run steps in order, build context as we go.
57
+ */
58
+ async function executePipeline(pipeline, match, configDir, dryRun = false) {
59
+ const { file, diff } = match;
60
+ const ctx = (0, template_vars_js_1.buildContext)(file.filePath, file.relativePath, (0, path_1.dirname)(file.filePath), { ...file.frontmatter }, [...file.tags], file.content, file.body, diff);
61
+ const stepResults = [];
62
+ const pipelineStart = Date.now();
63
+ let allSuccess = true;
64
+ for (const step of pipeline.steps) {
65
+ const result = await executeStep(step, ctx, configDir, dryRun);
66
+ stepResults.push(result);
67
+ // Add step output to context for subsequent steps
68
+ const stepOutput = {
69
+ stdout: result.stdout,
70
+ stderr: result.stderr,
71
+ exitCode: result.exitCode,
72
+ };
73
+ ctx.steps.push(stepOutput);
74
+ if (!result.success) {
75
+ allSuccess = false;
76
+ const continueOnError = step.continue_on_error ?? pipeline.continue_on_error ?? false;
77
+ if (!continueOnError) {
78
+ break;
79
+ }
80
+ }
81
+ }
82
+ return {
83
+ pipelineName: pipeline.name,
84
+ filePath: file.filePath,
85
+ steps: stepResults,
86
+ success: allSuccess,
87
+ durationMs: Date.now() - pipelineStart,
88
+ };
89
+ }
90
+ /**
91
+ * Convert a legacy trigger (with `run` string) into a single-step pipeline.
92
+ * This enables backward compatibility.
93
+ */
94
+ function triggerToPipeline(trigger) {
95
+ return {
96
+ name: trigger.name,
97
+ trigger: trigger.match,
98
+ steps: [{ run: trigger.run }],
99
+ };
100
+ }
@@ -0,0 +1,11 @@
1
+ import { MdPipeConfig } from './config.js';
2
+ import { PipelineResult } from './pipeline.js';
3
+ export interface RunCommandResult {
4
+ success: boolean;
5
+ results: PipelineResult[];
6
+ errors: string[];
7
+ }
8
+ /**
9
+ * Run a named pipeline manually on a specific file or all matching files.
10
+ */
11
+ export declare function runPipelineCommand(config: MdPipeConfig, pipelineName: string, filePath?: string, dryRun?: boolean): Promise<RunCommandResult>;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runPipelineCommand = runPipelineCommand;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const matcher_js_1 = require("./matcher.js");
7
+ const pipeline_js_1 = require("./pipeline.js");
8
+ function findMarkdownFiles(dir) {
9
+ const results = [];
10
+ function walk(current) {
11
+ const entries = (0, fs_1.readdirSync)(current, { withFileTypes: true });
12
+ for (const entry of entries) {
13
+ const full = (0, path_1.resolve)(current, entry.name);
14
+ if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
15
+ walk(full);
16
+ }
17
+ else if (entry.isFile() && entry.name.endsWith('.md')) {
18
+ results.push(full);
19
+ }
20
+ }
21
+ }
22
+ walk(dir);
23
+ return results;
24
+ }
25
+ /**
26
+ * Run a named pipeline manually on a specific file or all matching files.
27
+ */
28
+ async function runPipelineCommand(config, pipelineName, filePath, dryRun = false) {
29
+ // Find the pipeline (check both pipelines and legacy triggers)
30
+ let pipeline;
31
+ pipeline = config.pipelines.find(p => p.name === pipelineName);
32
+ if (!pipeline) {
33
+ const trigger = config.triggers.find(t => t.name === pipelineName);
34
+ if (trigger) {
35
+ pipeline = (0, pipeline_js_1.triggerToPipeline)(trigger);
36
+ }
37
+ }
38
+ if (!pipeline) {
39
+ const available = [
40
+ ...config.pipelines.map(p => p.name),
41
+ ...config.triggers.map(t => t.name),
42
+ ];
43
+ return {
44
+ success: false,
45
+ results: [],
46
+ errors: [`Pipeline '${pipelineName}' not found. Available: ${available.join(', ') || 'none'}`],
47
+ };
48
+ }
49
+ const results = [];
50
+ const errors = [];
51
+ if (filePath) {
52
+ // Run on specific file
53
+ const absPath = (0, path_1.resolve)(filePath);
54
+ const relPath = (0, path_1.relative)(config.watch, absPath);
55
+ try {
56
+ const file = (0, matcher_js_1.parseMarkdownFile)(absPath, relPath);
57
+ const match = { trigger: { name: pipeline.name, match: pipeline.trigger, run: '' }, file, diff: null };
58
+ const result = await (0, pipeline_js_1.executePipeline)(pipeline, match, config.configDir, dryRun);
59
+ results.push(result);
60
+ }
61
+ catch (err) {
62
+ errors.push(`Error processing ${filePath}: ${err.message}`);
63
+ }
64
+ }
65
+ else {
66
+ // Run on all matching files
67
+ const files = findMarkdownFiles(config.watch);
68
+ for (const fp of files) {
69
+ const relPath = (0, path_1.relative)(config.watch, fp);
70
+ try {
71
+ const file = (0, matcher_js_1.parseMarkdownFile)(fp, relPath);
72
+ const triggerDef = { name: pipeline.name, match: pipeline.trigger, run: '' };
73
+ const match = (0, matcher_js_1.evaluateTrigger)(triggerDef, file);
74
+ if (match) {
75
+ const result = await (0, pipeline_js_1.executePipeline)(pipeline, match, config.configDir, dryRun);
76
+ results.push(result);
77
+ }
78
+ }
79
+ catch (err) {
80
+ errors.push(`Error processing ${fp}: ${err.message}`);
81
+ }
82
+ }
83
+ }
84
+ const success = errors.length === 0 && results.every(r => r.success);
85
+ return { success, results, errors };
86
+ }
@@ -0,0 +1,10 @@
1
+ import { TemplateContext } from '../template-vars.js';
2
+ import type { StepResult } from './run.js';
3
+ export interface CopyStepConfig {
4
+ copy: {
5
+ to: string;
6
+ /** If true, preserve directory structure relative to watch dir. Default: false */
7
+ flatten?: boolean;
8
+ };
9
+ }
10
+ export declare function executeCopyStep(config: CopyStepConfig, ctx: TemplateContext, configDir: string, dryRun: boolean): StepResult;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeCopyStep = executeCopyStep;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const template_vars_js_1 = require("../template-vars.js");
7
+ function executeCopyStep(config, ctx, configDir, dryRun) {
8
+ const start = Date.now();
9
+ try {
10
+ const destDir = (0, path_1.resolve)(configDir, (0, template_vars_js_1.expandTemplate)(config.copy.to, ctx));
11
+ const flatten = config.copy.flatten ?? false;
12
+ let destPath;
13
+ if (flatten) {
14
+ destPath = (0, path_1.join)(destDir, ctx.basename);
15
+ }
16
+ else {
17
+ destPath = (0, path_1.join)(destDir, ctx.relative);
18
+ }
19
+ if (!dryRun) {
20
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(destPath), { recursive: true });
21
+ (0, fs_1.copyFileSync)(ctx.file, destPath);
22
+ }
23
+ return {
24
+ type: 'copy',
25
+ success: true,
26
+ stdout: `Copied to ${destPath}${dryRun ? ' [dry run]' : ''}`,
27
+ stderr: '',
28
+ exitCode: 0,
29
+ durationMs: Date.now() - start,
30
+ };
31
+ }
32
+ catch (err) {
33
+ return {
34
+ type: 'copy',
35
+ success: false,
36
+ stdout: '',
37
+ stderr: err.message || String(err),
38
+ exitCode: 1,
39
+ durationMs: Date.now() - start,
40
+ };
41
+ }
42
+ }
@@ -0,0 +1,13 @@
1
+ import { TemplateContext } from '../template-vars.js';
2
+ export interface RunStepConfig {
3
+ run: string;
4
+ }
5
+ export interface StepResult {
6
+ type: string;
7
+ success: boolean;
8
+ stdout: string;
9
+ stderr: string;
10
+ exitCode: number;
11
+ durationMs: number;
12
+ }
13
+ export declare function executeRunStep(config: RunStepConfig, ctx: TemplateContext, cwd: string, dryRun: boolean): StepResult;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeRunStep = executeRunStep;
4
+ const child_process_1 = require("child_process");
5
+ const template_vars_js_1 = require("../template-vars.js");
6
+ function executeRunStep(config, ctx, cwd, dryRun) {
7
+ const command = (0, template_vars_js_1.expandTemplate)(config.run, ctx);
8
+ if (dryRun) {
9
+ return {
10
+ type: 'run',
11
+ success: true,
12
+ stdout: '[dry run]',
13
+ stderr: '',
14
+ exitCode: 0,
15
+ durationMs: 0,
16
+ };
17
+ }
18
+ const env = (0, template_vars_js_1.buildEnvVars)(ctx);
19
+ const start = Date.now();
20
+ try {
21
+ const stdout = (0, child_process_1.execSync)(command, {
22
+ cwd,
23
+ timeout: 30000,
24
+ encoding: 'utf-8',
25
+ env: { ...process.env, ...env },
26
+ });
27
+ return {
28
+ type: 'run',
29
+ success: true,
30
+ stdout: stdout.trim(),
31
+ stderr: '',
32
+ exitCode: 0,
33
+ durationMs: Date.now() - start,
34
+ };
35
+ }
36
+ catch (err) {
37
+ return {
38
+ type: 'run',
39
+ success: false,
40
+ stdout: err.stdout?.trim() ?? '',
41
+ stderr: err.stderr?.trim() ?? '',
42
+ exitCode: err.status ?? 1,
43
+ durationMs: Date.now() - start,
44
+ };
45
+ }
46
+ }
@@ -0,0 +1,11 @@
1
+ import { TemplateContext } from '../template-vars.js';
2
+ import type { StepResult } from './run.js';
3
+ export interface TemplateStepConfig {
4
+ template: {
5
+ /** Path to template file (relative to config dir) */
6
+ src: string;
7
+ /** Output path (relative to config dir). Supports template vars. */
8
+ out: string;
9
+ };
10
+ }
11
+ export declare function executeTemplateStep(config: TemplateStepConfig, ctx: TemplateContext, configDir: string, dryRun: boolean): StepResult;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeTemplateStep = executeTemplateStep;
4
+ const fs_1 = require("fs");
5
+ const path_1 = require("path");
6
+ const template_vars_js_1 = require("../template-vars.js");
7
+ function executeTemplateStep(config, ctx, configDir, dryRun) {
8
+ const start = Date.now();
9
+ try {
10
+ const srcPath = (0, path_1.resolve)(configDir, (0, template_vars_js_1.expandTemplate)(config.template.src, ctx));
11
+ const outPath = (0, path_1.resolve)(configDir, (0, template_vars_js_1.expandTemplate)(config.template.out, ctx));
12
+ const templateContent = (0, fs_1.readFileSync)(srcPath, 'utf-8');
13
+ const rendered = (0, template_vars_js_1.expandTemplate)(templateContent, ctx);
14
+ if (!dryRun) {
15
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(outPath), { recursive: true });
16
+ (0, fs_1.writeFileSync)(outPath, rendered, 'utf-8');
17
+ }
18
+ return {
19
+ type: 'template',
20
+ success: true,
21
+ stdout: `Rendered ${srcPath} → ${outPath}${dryRun ? ' [dry run]' : ''}`,
22
+ stderr: '',
23
+ exitCode: 0,
24
+ durationMs: Date.now() - start,
25
+ };
26
+ }
27
+ catch (err) {
28
+ return {
29
+ type: 'template',
30
+ success: false,
31
+ stdout: '',
32
+ stderr: err.message || String(err),
33
+ exitCode: 1,
34
+ durationMs: Date.now() - start,
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,6 @@
1
+ import { TemplateContext } from '../template-vars.js';
2
+ import type { StepResult } from './run.js';
3
+ export interface UpdateFrontmatterStepConfig {
4
+ 'update-frontmatter': Record<string, unknown>;
5
+ }
6
+ export declare function executeUpdateFrontmatterStep(config: UpdateFrontmatterStepConfig, ctx: TemplateContext, dryRun: boolean): StepResult;