@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.
- package/README.md +172 -147
- package/dist/cli.js +108 -30
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.js +110 -48
- package/dist/core/pipeline.d.ts +48 -0
- package/dist/core/pipeline.js +100 -0
- package/dist/core/run-command.d.ts +11 -0
- package/dist/core/run-command.js +86 -0
- package/dist/core/steps/copy.d.ts +10 -0
- package/dist/core/steps/copy.js +42 -0
- package/dist/core/steps/run.d.ts +13 -0
- package/dist/core/steps/run.js +46 -0
- package/dist/core/steps/template.d.ts +11 -0
- package/dist/core/steps/template.js +37 -0
- package/dist/core/steps/update-frontmatter.d.ts +6 -0
- package/dist/core/steps/update-frontmatter.js +66 -0
- package/dist/core/steps/webhook.d.ts +11 -0
- package/dist/core/steps/webhook.js +81 -0
- package/dist/core/template-vars.d.ts +43 -0
- package/dist/core/template-vars.js +143 -0
- package/dist/core/test-file.d.ts +1 -0
- package/dist/core/test-file.js +49 -21
- package/dist/core/watcher.d.ts +2 -0
- package/dist/core/watcher.js +19 -3
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.executeUpdateFrontmatterStep = executeUpdateFrontmatterStep;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
9
|
+
const template_vars_js_1 = require("../template-vars.js");
|
|
10
|
+
function executeUpdateFrontmatterStep(config, ctx, dryRun) {
|
|
11
|
+
const updates = (0, template_vars_js_1.expandDeep)(config['update-frontmatter'], ctx);
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
try {
|
|
14
|
+
const raw = (0, fs_1.readFileSync)(ctx.file, 'utf-8');
|
|
15
|
+
const parsed = (0, gray_matter_1.default)(raw);
|
|
16
|
+
const fm = parsed.data || {};
|
|
17
|
+
// Apply updates
|
|
18
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
19
|
+
fm[key] = coerceValue(value);
|
|
20
|
+
}
|
|
21
|
+
// Rebuild file: use gray-matter stringify
|
|
22
|
+
const output = gray_matter_1.default.stringify(parsed.content, fm);
|
|
23
|
+
if (!dryRun) {
|
|
24
|
+
(0, fs_1.writeFileSync)(ctx.file, output, 'utf-8');
|
|
25
|
+
}
|
|
26
|
+
// Update the context's frontmatter so subsequent steps see changes
|
|
27
|
+
Object.assign(ctx.frontmatter, fm);
|
|
28
|
+
const updatedFields = Object.keys(updates).join(', ');
|
|
29
|
+
return {
|
|
30
|
+
type: 'update-frontmatter',
|
|
31
|
+
success: true,
|
|
32
|
+
stdout: `Updated frontmatter: ${updatedFields}${dryRun ? ' [dry run]' : ''}`,
|
|
33
|
+
stderr: '',
|
|
34
|
+
exitCode: 0,
|
|
35
|
+
durationMs: Date.now() - start,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
type: 'update-frontmatter',
|
|
41
|
+
success: false,
|
|
42
|
+
stdout: '',
|
|
43
|
+
stderr: err.message || String(err),
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
durationMs: Date.now() - start,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Coerce string values to appropriate types.
|
|
51
|
+
* "true"/"false" → boolean, numeric strings → numbers.
|
|
52
|
+
*/
|
|
53
|
+
function coerceValue(value) {
|
|
54
|
+
if (typeof value !== 'string')
|
|
55
|
+
return value;
|
|
56
|
+
if (value === 'true')
|
|
57
|
+
return true;
|
|
58
|
+
if (value === 'false')
|
|
59
|
+
return false;
|
|
60
|
+
// Don't coerce ISO dates or other strings that look numeric-ish
|
|
61
|
+
if (/^\d+$/.test(value) && value.length < 16)
|
|
62
|
+
return parseInt(value, 10);
|
|
63
|
+
if (/^\d+\.\d+$/.test(value) && value.length < 16)
|
|
64
|
+
return parseFloat(value);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { TemplateContext } from '../template-vars.js';
|
|
2
|
+
import type { StepResult } from './run.js';
|
|
3
|
+
export interface WebhookStepConfig {
|
|
4
|
+
webhook: {
|
|
5
|
+
url: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function executeWebhookStep(config: WebhookStepConfig, ctx: TemplateContext, dryRun: boolean): Promise<StepResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWebhookStep = executeWebhookStep;
|
|
4
|
+
const template_vars_js_1 = require("../template-vars.js");
|
|
5
|
+
async function executeWebhookStep(config, ctx, dryRun) {
|
|
6
|
+
const start = Date.now();
|
|
7
|
+
const wh = config.webhook;
|
|
8
|
+
// Expand env vars in URL (e.g. $WEBHOOK_URL)
|
|
9
|
+
let url = (0, template_vars_js_1.expandTemplate)(wh.url, ctx);
|
|
10
|
+
url = expandEnvVars(url);
|
|
11
|
+
const method = (wh.method || 'POST').toUpperCase();
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
...(wh.headers ? (0, template_vars_js_1.expandDeep)(wh.headers, ctx) : {}),
|
|
15
|
+
};
|
|
16
|
+
// Expand body
|
|
17
|
+
let body;
|
|
18
|
+
if (wh.body) {
|
|
19
|
+
const expanded = (0, template_vars_js_1.expandDeep)(wh.body, ctx);
|
|
20
|
+
// Also expand $ENV_VAR style in string values
|
|
21
|
+
body = JSON.stringify(expanded, (_key, val) => {
|
|
22
|
+
if (typeof val === 'string')
|
|
23
|
+
return expandEnvVars(val);
|
|
24
|
+
return val;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// Default body: file info + frontmatter
|
|
29
|
+
body = JSON.stringify({
|
|
30
|
+
file: ctx.file,
|
|
31
|
+
relative: ctx.relative,
|
|
32
|
+
slug: ctx.slug,
|
|
33
|
+
frontmatter: ctx.frontmatter,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (dryRun) {
|
|
37
|
+
return {
|
|
38
|
+
type: 'webhook',
|
|
39
|
+
success: true,
|
|
40
|
+
stdout: `[dry run] ${method} ${url}`,
|
|
41
|
+
stderr: '',
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
durationMs: Date.now() - start,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(url, {
|
|
48
|
+
method,
|
|
49
|
+
headers,
|
|
50
|
+
body: method !== 'GET' ? body : undefined,
|
|
51
|
+
});
|
|
52
|
+
const responseText = await response.text();
|
|
53
|
+
const success = response.ok;
|
|
54
|
+
return {
|
|
55
|
+
type: 'webhook',
|
|
56
|
+
success,
|
|
57
|
+
stdout: responseText.slice(0, 4096),
|
|
58
|
+
stderr: success ? '' : `HTTP ${response.status} ${response.statusText}`,
|
|
59
|
+
exitCode: success ? 0 : 1,
|
|
60
|
+
durationMs: Date.now() - start,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'webhook',
|
|
66
|
+
success: false,
|
|
67
|
+
stdout: '',
|
|
68
|
+
stderr: err.message || String(err),
|
|
69
|
+
exitCode: 1,
|
|
70
|
+
durationMs: Date.now() - start,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Expand $ENV_VAR and ${ENV_VAR} style variables from process.env
|
|
76
|
+
*/
|
|
77
|
+
function expandEnvVars(str) {
|
|
78
|
+
return str.replace(/\$\{?([A-Z_][A-Z0-9_]*)\}?/g, (_match, name) => {
|
|
79
|
+
return process.env[name] || '';
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface TemplateContext {
|
|
2
|
+
file: string;
|
|
3
|
+
dir: string;
|
|
4
|
+
basename: string;
|
|
5
|
+
relative: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
frontmatter: Record<string, unknown>;
|
|
8
|
+
tags: string[];
|
|
9
|
+
content: string;
|
|
10
|
+
body: string;
|
|
11
|
+
diff: Record<string, {
|
|
12
|
+
old: unknown;
|
|
13
|
+
new: unknown;
|
|
14
|
+
}> | null;
|
|
15
|
+
steps: StepOutput[];
|
|
16
|
+
}
|
|
17
|
+
export interface StepOutput {
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
exitCode: number;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Expand template variables in a string.
|
|
25
|
+
* Supports: {{now}}, {{date}}, {{slug}}, {{file}}, {{basename}}, {{relative}},
|
|
26
|
+
* {{dir}}, {{tags}}, {{fm.<field>}}, {{step.<index>.<field>}}
|
|
27
|
+
*/
|
|
28
|
+
export declare function expandTemplate(template: string, ctx: TemplateContext): string;
|
|
29
|
+
/**
|
|
30
|
+
* Deep-expand all string values in an object/array/primitive.
|
|
31
|
+
*/
|
|
32
|
+
export declare function expandDeep(value: unknown, ctx: TemplateContext): unknown;
|
|
33
|
+
/**
|
|
34
|
+
* Build a TemplateContext from file state and match info.
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildContext(filePath: string, relativePath: string, dir: string, frontmatter: Record<string, unknown>, tags: string[], content: string, body: string, diff: Record<string, {
|
|
37
|
+
old: unknown;
|
|
38
|
+
new: unknown;
|
|
39
|
+
}> | null): TemplateContext;
|
|
40
|
+
/**
|
|
41
|
+
* Build env vars for shell commands (backward compatible + new).
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildEnvVars(ctx: TemplateContext): Record<string, string>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.expandTemplate = expandTemplate;
|
|
4
|
+
exports.expandDeep = expandDeep;
|
|
5
|
+
exports.buildContext = buildContext;
|
|
6
|
+
exports.buildEnvVars = buildEnvVars;
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
/**
|
|
9
|
+
* Expand template variables in a string.
|
|
10
|
+
* Supports: {{now}}, {{date}}, {{slug}}, {{file}}, {{basename}}, {{relative}},
|
|
11
|
+
* {{dir}}, {{tags}}, {{fm.<field>}}, {{step.<index>.<field>}}
|
|
12
|
+
*/
|
|
13
|
+
function expandTemplate(template, ctx) {
|
|
14
|
+
return template.replace(/\{\{([^}]+)\}\}/g, (_match, key) => {
|
|
15
|
+
const k = key.trim();
|
|
16
|
+
// Built-in variables
|
|
17
|
+
switch (k) {
|
|
18
|
+
case 'now':
|
|
19
|
+
return new Date().toISOString();
|
|
20
|
+
case 'date':
|
|
21
|
+
return new Date().toISOString().split('T')[0];
|
|
22
|
+
case 'timestamp':
|
|
23
|
+
return String(Date.now());
|
|
24
|
+
case 'slug':
|
|
25
|
+
return ctx.slug;
|
|
26
|
+
case 'file':
|
|
27
|
+
return ctx.file;
|
|
28
|
+
case 'basename':
|
|
29
|
+
return ctx.basename;
|
|
30
|
+
case 'relative':
|
|
31
|
+
return ctx.relative;
|
|
32
|
+
case 'dir':
|
|
33
|
+
return ctx.dir;
|
|
34
|
+
case 'tags':
|
|
35
|
+
return ctx.tags.join(',');
|
|
36
|
+
case 'content':
|
|
37
|
+
return ctx.body;
|
|
38
|
+
}
|
|
39
|
+
// Frontmatter access: {{fm.title}}, {{fm.status}}
|
|
40
|
+
if (k.startsWith('fm.')) {
|
|
41
|
+
const field = k.slice(3);
|
|
42
|
+
const val = ctx.frontmatter[field];
|
|
43
|
+
if (val === undefined || val === null)
|
|
44
|
+
return '';
|
|
45
|
+
if (typeof val === 'object')
|
|
46
|
+
return JSON.stringify(val);
|
|
47
|
+
return String(val);
|
|
48
|
+
}
|
|
49
|
+
// Step output access: {{step.0.stdout}}, {{step.1.exitCode}}
|
|
50
|
+
if (k.startsWith('step.')) {
|
|
51
|
+
const parts = k.slice(5).split('.');
|
|
52
|
+
const idx = parseInt(parts[0], 10);
|
|
53
|
+
const field = parts.slice(1).join('.');
|
|
54
|
+
if (isNaN(idx) || idx < 0 || idx >= ctx.steps.length)
|
|
55
|
+
return '';
|
|
56
|
+
const step = ctx.steps[idx];
|
|
57
|
+
if (!step)
|
|
58
|
+
return '';
|
|
59
|
+
const val = step[field];
|
|
60
|
+
if (val === undefined || val === null)
|
|
61
|
+
return '';
|
|
62
|
+
if (typeof val === 'object')
|
|
63
|
+
return JSON.stringify(val);
|
|
64
|
+
return String(val);
|
|
65
|
+
}
|
|
66
|
+
// Unknown variable — leave as-is
|
|
67
|
+
return `{{${k}}}`;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Deep-expand all string values in an object/array/primitive.
|
|
72
|
+
*/
|
|
73
|
+
function expandDeep(value, ctx) {
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
return expandTemplate(value, ctx);
|
|
76
|
+
}
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
return value.map(v => expandDeep(v, ctx));
|
|
79
|
+
}
|
|
80
|
+
if (value !== null && typeof value === 'object') {
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const [k, v] of Object.entries(value)) {
|
|
83
|
+
result[k] = expandDeep(v, ctx);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build a TemplateContext from file state and match info.
|
|
91
|
+
*/
|
|
92
|
+
function buildContext(filePath, relativePath, dir, frontmatter, tags, content, body, diff) {
|
|
93
|
+
const ext = (0, path_1.extname)(relativePath);
|
|
94
|
+
const slug = (0, path_1.basename)(relativePath, ext);
|
|
95
|
+
return {
|
|
96
|
+
file: filePath,
|
|
97
|
+
dir,
|
|
98
|
+
basename: (0, path_1.basename)(filePath),
|
|
99
|
+
relative: relativePath,
|
|
100
|
+
slug,
|
|
101
|
+
frontmatter,
|
|
102
|
+
tags,
|
|
103
|
+
content,
|
|
104
|
+
body,
|
|
105
|
+
diff,
|
|
106
|
+
steps: [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build env vars for shell commands (backward compatible + new).
|
|
111
|
+
*/
|
|
112
|
+
function buildEnvVars(ctx) {
|
|
113
|
+
const env = {
|
|
114
|
+
FILE: ctx.file,
|
|
115
|
+
DIR: ctx.dir,
|
|
116
|
+
BASENAME: ctx.basename,
|
|
117
|
+
RELATIVE: ctx.relative,
|
|
118
|
+
SLUG: ctx.slug,
|
|
119
|
+
FRONTMATTER: JSON.stringify(ctx.frontmatter),
|
|
120
|
+
DIFF: ctx.diff ? JSON.stringify(ctx.diff) : '{}',
|
|
121
|
+
TAGS: ctx.tags.join(','),
|
|
122
|
+
};
|
|
123
|
+
// FM_ env vars
|
|
124
|
+
for (const [key, val] of Object.entries(ctx.frontmatter)) {
|
|
125
|
+
if (val === null || val === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
env[`FM_${key}`] = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
128
|
+
}
|
|
129
|
+
// Step outputs
|
|
130
|
+
for (let i = 0; i < ctx.steps.length; i++) {
|
|
131
|
+
const step = ctx.steps[i];
|
|
132
|
+
if (step.stdout)
|
|
133
|
+
env[`STEP_${i}_STDOUT`] = step.stdout;
|
|
134
|
+
if (step.stderr)
|
|
135
|
+
env[`STEP_${i}_STDERR`] = step.stderr;
|
|
136
|
+
}
|
|
137
|
+
// Last step output shortcut
|
|
138
|
+
if (ctx.steps.length > 0) {
|
|
139
|
+
const last = ctx.steps[ctx.steps.length - 1];
|
|
140
|
+
env.STEP_OUTPUT = last.stdout || '';
|
|
141
|
+
}
|
|
142
|
+
return env;
|
|
143
|
+
}
|
package/dist/core/test-file.d.ts
CHANGED
package/dist/core/test-file.js
CHANGED
|
@@ -12,8 +12,8 @@ function testFile(config, filePath) {
|
|
|
12
12
|
const relativePath = (0, path_1.relative)(config.watch, absPath);
|
|
13
13
|
const file = (0, matcher_js_1.parseMarkdownFile)(absPath, relativePath);
|
|
14
14
|
const matches = [];
|
|
15
|
+
// Check legacy triggers
|
|
15
16
|
for (const trigger of config.triggers) {
|
|
16
|
-
// For frontmatter_changed triggers, still check other conditions first
|
|
17
17
|
if (trigger.match.frontmatter_changed) {
|
|
18
18
|
const staticMatch = { ...trigger.match };
|
|
19
19
|
delete staticMatch.frontmatter_changed;
|
|
@@ -26,34 +26,62 @@ function testFile(config, filePath) {
|
|
|
26
26
|
}
|
|
27
27
|
matches.push({
|
|
28
28
|
triggerName: trigger.name,
|
|
29
|
-
|
|
29
|
+
type: 'trigger',
|
|
30
|
+
reason: `frontmatter_changed: would fire on changes to [${trigger.match.frontmatter_changed.join(', ')}]`,
|
|
30
31
|
});
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
34
|
const result = (0, matcher_js_1.evaluateTrigger)(trigger, file);
|
|
34
35
|
if (result) {
|
|
35
|
-
const reasons = [];
|
|
36
|
-
if (trigger.match.path)
|
|
37
|
-
reasons.push(`path matches "${trigger.match.path}"`);
|
|
38
|
-
if (trigger.match.frontmatter)
|
|
39
|
-
reasons.push(`frontmatter matches ${JSON.stringify(trigger.match.frontmatter)}`);
|
|
40
|
-
if (trigger.match.tags)
|
|
41
|
-
reasons.push(`has tags [${trigger.match.tags.join(', ')}]`);
|
|
42
|
-
if (trigger.match.content)
|
|
43
|
-
reasons.push(`body contains "${trigger.match.content}"`);
|
|
44
|
-
if (trigger.match.content_regex)
|
|
45
|
-
reasons.push(`body matches /${trigger.match.content_regex}/`);
|
|
46
36
|
matches.push({
|
|
47
37
|
triggerName: trigger.name,
|
|
48
|
-
|
|
38
|
+
type: 'trigger',
|
|
39
|
+
reason: buildMatchReason(trigger.match),
|
|
49
40
|
});
|
|
50
41
|
}
|
|
51
42
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
43
|
+
// Check pipelines
|
|
44
|
+
for (const pipeline of config.pipelines) {
|
|
45
|
+
const triggerDef = { name: pipeline.name, match: pipeline.trigger, run: '' };
|
|
46
|
+
if (pipeline.trigger.frontmatter_changed) {
|
|
47
|
+
const staticMatch = { ...pipeline.trigger };
|
|
48
|
+
delete staticMatch.frontmatter_changed;
|
|
49
|
+
const hasStaticConditions = staticMatch.path || staticMatch.frontmatter || staticMatch.tags || staticMatch.content || staticMatch.content_regex;
|
|
50
|
+
if (hasStaticConditions) {
|
|
51
|
+
const staticTrigger = { ...triggerDef, match: staticMatch };
|
|
52
|
+
const staticResult = (0, matcher_js_1.evaluateTrigger)(staticTrigger, file);
|
|
53
|
+
if (!staticResult)
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
matches.push({
|
|
57
|
+
triggerName: pipeline.name,
|
|
58
|
+
type: 'pipeline',
|
|
59
|
+
reason: `frontmatter_changed: would fire on changes to [${pipeline.trigger.frontmatter_changed.join(', ')}] (${pipeline.steps.length} steps)`,
|
|
60
|
+
});
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const result = (0, matcher_js_1.evaluateTrigger)(triggerDef, file);
|
|
64
|
+
if (result) {
|
|
65
|
+
matches.push({
|
|
66
|
+
triggerName: pipeline.name,
|
|
67
|
+
type: 'pipeline',
|
|
68
|
+
reason: `${buildMatchReason(pipeline.trigger)} (${pipeline.steps.length} steps)`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { filePath: absPath, relativePath, frontmatter: file.frontmatter, tags: file.tags, matches };
|
|
73
|
+
}
|
|
74
|
+
function buildMatchReason(match) {
|
|
75
|
+
const reasons = [];
|
|
76
|
+
if (match.path)
|
|
77
|
+
reasons.push(`path matches "${match.path}"`);
|
|
78
|
+
if (match.frontmatter)
|
|
79
|
+
reasons.push(`frontmatter matches ${JSON.stringify(match.frontmatter)}`);
|
|
80
|
+
if (match.tags)
|
|
81
|
+
reasons.push(`has tags [${match.tags.join(', ')}]`);
|
|
82
|
+
if (match.content)
|
|
83
|
+
reasons.push(`body contains "${match.content}"`);
|
|
84
|
+
if (match.content_regex)
|
|
85
|
+
reasons.push(`body matches /${match.content_regex}/`);
|
|
86
|
+
return reasons.join(' + ') || 'all conditions matched';
|
|
59
87
|
}
|
package/dist/core/watcher.d.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { EventEmitter } from 'events';
|
|
|
2
2
|
import { MdPipeConfig } from './config.js';
|
|
3
3
|
import { MatchResult } from './matcher.js';
|
|
4
4
|
import { RunResult } from './runner.js';
|
|
5
|
+
import { PipelineResult } from './pipeline.js';
|
|
5
6
|
export interface WatcherEvents {
|
|
6
7
|
match: (result: MatchResult) => void;
|
|
7
8
|
action: (result: RunResult) => void;
|
|
9
|
+
pipeline: (result: PipelineResult) => void;
|
|
8
10
|
error: (error: Error, filePath?: string) => void;
|
|
9
11
|
ready: () => void;
|
|
10
12
|
}
|
package/dist/core/watcher.js
CHANGED
|
@@ -6,6 +6,7 @@ const path_1 = require("path");
|
|
|
6
6
|
const events_1 = require("events");
|
|
7
7
|
const matcher_js_1 = require("./matcher.js");
|
|
8
8
|
const runner_js_1 = require("./runner.js");
|
|
9
|
+
const pipeline_js_1 = require("./pipeline.js");
|
|
9
10
|
class MdPipeWatcher extends events_1.EventEmitter {
|
|
10
11
|
config;
|
|
11
12
|
fsWatcher = null;
|
|
@@ -33,7 +34,6 @@ class MdPipeWatcher extends events_1.EventEmitter {
|
|
|
33
34
|
if (!absPath.endsWith('.md'))
|
|
34
35
|
return;
|
|
35
36
|
const relPath = (0, path_1.relative)(watchDir, absPath);
|
|
36
|
-
// Debounce: cancel any pending timer for this file
|
|
37
37
|
const existing = this.debounceTimers.get(relPath);
|
|
38
38
|
if (existing)
|
|
39
39
|
clearTimeout(existing);
|
|
@@ -48,7 +48,6 @@ class MdPipeWatcher extends events_1.EventEmitter {
|
|
|
48
48
|
this.fsWatcher.on('error', (err) => this.emit('error', err instanceof Error ? err : new Error(String(err))));
|
|
49
49
|
}
|
|
50
50
|
async stop() {
|
|
51
|
-
// Clear pending debounce timers
|
|
52
51
|
for (const timer of this.debounceTimers.values()) {
|
|
53
52
|
clearTimeout(timer);
|
|
54
53
|
}
|
|
@@ -69,7 +68,7 @@ class MdPipeWatcher extends events_1.EventEmitter {
|
|
|
69
68
|
return;
|
|
70
69
|
}
|
|
71
70
|
const previousFm = this.frontmatterCache.get(relativePath);
|
|
72
|
-
// Run
|
|
71
|
+
// Run legacy triggers
|
|
73
72
|
for (const trigger of this.config.triggers) {
|
|
74
73
|
const match = (0, matcher_js_1.evaluateTrigger)(trigger, file, previousFm);
|
|
75
74
|
if (match) {
|
|
@@ -78,6 +77,23 @@ class MdPipeWatcher extends events_1.EventEmitter {
|
|
|
78
77
|
this.emit('action', result);
|
|
79
78
|
}
|
|
80
79
|
}
|
|
80
|
+
// Run pipelines
|
|
81
|
+
for (const pipeline of this.config.pipelines) {
|
|
82
|
+
// Create a synthetic trigger def for evaluation
|
|
83
|
+
const triggerDef = {
|
|
84
|
+
name: pipeline.name,
|
|
85
|
+
match: pipeline.trigger,
|
|
86
|
+
run: '', // not used, just for type compat
|
|
87
|
+
};
|
|
88
|
+
const match = (0, matcher_js_1.evaluateTrigger)(triggerDef, file, previousFm);
|
|
89
|
+
if (match) {
|
|
90
|
+
this.emit('match', match);
|
|
91
|
+
// Execute pipeline async
|
|
92
|
+
(0, pipeline_js_1.executePipeline)(pipeline, match, this.config.configDir, this.dryRun)
|
|
93
|
+
.then(result => this.emit('pipeline', result))
|
|
94
|
+
.catch(err => this.emit('error', err instanceof Error ? err : new Error(String(err)), absolutePath));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
81
97
|
// Cache frontmatter for diff tracking
|
|
82
98
|
this.frontmatterCache.set(relativePath, { ...file.frontmatter });
|
|
83
99
|
}
|