@safetnsr/md-pipe 0.1.1 → 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 +194 -85
- package/dist/cli.js +142 -32
- package/dist/core/config.d.ts +7 -0
- package/dist/core/config.js +128 -42
- package/dist/core/matcher.d.ts +3 -0
- package/dist/core/matcher.js +36 -1
- package/dist/core/once.d.ts +2 -1
- package/dist/core/once.js +23 -3
- 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/runner.d.ts +1 -1
- package/dist/core/runner.js +27 -2
- package/dist/core/state.d.ts +17 -0
- package/dist/core/state.js +47 -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 +58 -17
- package/dist/core/watcher.d.ts +4 -0
- package/dist/core/watcher.js +35 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# md-pipe
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Markdown content pipelines.
|
|
4
|
+
Write markdown → auto-publish everywhere.
|
|
5
5
|
|
|
6
6
|
12 lines of YAML → your markdown folder auto-publishes, auto-indexes, and auto-archives based on frontmatter changes. Works with any `.md` directory — Obsidian vaults, agent workspaces, docs-as-code repos, digital gardens.
|
|
7
7
|
|
|
@@ -23,13 +23,94 @@ npm install -g @safetnsr/md-pipe
|
|
|
23
23
|
# 1. Create config
|
|
24
24
|
md-pipe init
|
|
25
25
|
|
|
26
|
-
# 2. Edit .md-pipe.yml with your
|
|
26
|
+
# 2. Edit .md-pipe.yml with your pipelines
|
|
27
27
|
|
|
28
28
|
# 3. Start watching
|
|
29
29
|
md-pipe watch
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## Pipelines (v0.3+)
|
|
33
|
+
|
|
34
|
+
Pipelines are multi-step content workflows that fire on frontmatter changes:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
watch: ./docs
|
|
38
|
+
|
|
39
|
+
pipelines:
|
|
40
|
+
- name: publish-post
|
|
41
|
+
trigger:
|
|
42
|
+
path: "posts/**"
|
|
43
|
+
frontmatter: { status: publish }
|
|
44
|
+
frontmatter_changed: [status]
|
|
45
|
+
steps:
|
|
46
|
+
- run: "echo Publishing {{fm.title}}"
|
|
47
|
+
- copy: { to: "./_site/posts" }
|
|
48
|
+
- update-frontmatter:
|
|
49
|
+
published_at: "{{now}}"
|
|
50
|
+
published: "true"
|
|
51
|
+
- webhook:
|
|
52
|
+
url: "$WEBHOOK_URL"
|
|
53
|
+
body: { title: "{{fm.title}}", slug: "{{slug}}" }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Steps execute in order. If one fails, the pipeline stops (unless `continue_on_error: true`).
|
|
57
|
+
|
|
58
|
+
### Step Types
|
|
59
|
+
|
|
60
|
+
| Step | Description |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `run` | Shell command. Supports template vars and `$ENV` vars |
|
|
63
|
+
| `update-frontmatter` | Write fields back to the source file's YAML frontmatter |
|
|
64
|
+
| `copy` | Copy file to a destination directory |
|
|
65
|
+
| `webhook` | POST JSON to a URL |
|
|
66
|
+
| `template` | Render a template file with context data |
|
|
67
|
+
|
|
68
|
+
### Template Variables
|
|
69
|
+
|
|
70
|
+
Available in all step configs:
|
|
71
|
+
|
|
72
|
+
| Variable | Description |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `{{now}}` | ISO timestamp |
|
|
75
|
+
| `{{date}}` | YYYY-MM-DD |
|
|
76
|
+
| `{{slug}}` | Filename without extension |
|
|
77
|
+
| `{{file}}` | Absolute path |
|
|
78
|
+
| `{{basename}}` | Filename |
|
|
79
|
+
| `{{relative}}` | Path relative to watch dir |
|
|
80
|
+
| `{{dir}}` | Parent directory |
|
|
81
|
+
| `{{tags}}` | Comma-separated tags |
|
|
82
|
+
| `{{fm.title}}` | Frontmatter field |
|
|
83
|
+
| `{{step.0.stdout}}` | Output from step 0 |
|
|
84
|
+
|
|
85
|
+
### Pipeline Context
|
|
86
|
+
|
|
87
|
+
Each step's output is available to subsequent steps:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
steps:
|
|
91
|
+
- run: "deploy.sh $FILE" # stdout captured
|
|
92
|
+
- update-frontmatter:
|
|
93
|
+
url: "{{step.0.stdout}}" # use output from step 0
|
|
94
|
+
deployed_at: "{{now}}"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Manual Run
|
|
98
|
+
|
|
99
|
+
Test a pipeline without watching:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
md-pipe run publish-post posts/hello.md
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Run against all matching files:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
md-pipe run publish-post
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Legacy Triggers
|
|
112
|
+
|
|
113
|
+
Simple triggers with a single `run` command still work (backward compatible):
|
|
33
114
|
|
|
34
115
|
```yaml
|
|
35
116
|
watch: ./docs
|
|
@@ -40,23 +121,11 @@ triggers:
|
|
|
40
121
|
path: "posts/**"
|
|
41
122
|
frontmatter:
|
|
42
123
|
status: publish
|
|
43
|
-
run: "
|
|
44
|
-
|
|
45
|
-
- name: reindex
|
|
46
|
-
match:
|
|
47
|
-
frontmatter_changed:
|
|
48
|
-
- title
|
|
49
|
-
- tags
|
|
50
|
-
- category
|
|
51
|
-
run: "./scripts/reindex.sh $FILE"
|
|
52
|
-
|
|
53
|
-
- name: urgent-notify
|
|
54
|
-
match:
|
|
55
|
-
tags:
|
|
56
|
-
- urgent
|
|
57
|
-
run: "curl -X POST $WEBHOOK -d '{\"file\": \"$FILE\"}'"
|
|
124
|
+
run: "echo Publishing $FILE"
|
|
58
125
|
```
|
|
59
126
|
|
|
127
|
+
Triggers and pipelines can coexist in the same config.
|
|
128
|
+
|
|
60
129
|
## Commands
|
|
61
130
|
|
|
62
131
|
| Command | Description |
|
|
@@ -64,7 +133,8 @@ triggers:
|
|
|
64
133
|
| `md-pipe init` | Scaffold a `.md-pipe.yml` config file |
|
|
65
134
|
| `md-pipe watch` | Start watching for changes and trigger actions |
|
|
66
135
|
| `md-pipe once` | Run triggers against current files (CI/batch mode) |
|
|
67
|
-
| `md-pipe
|
|
136
|
+
| `md-pipe run <pipeline> [file]` | Manually trigger a pipeline |
|
|
137
|
+
| `md-pipe test <file>` | Show which triggers/pipelines match a file |
|
|
68
138
|
|
|
69
139
|
## Flags
|
|
70
140
|
|
|
@@ -73,105 +143,144 @@ triggers:
|
|
|
73
143
|
| `--config, -c <path>` | Path to config file |
|
|
74
144
|
| `--dry-run` | Show matches without executing actions |
|
|
75
145
|
| `--json` | Output in JSON format |
|
|
76
|
-
| `--verbose` | Show
|
|
77
|
-
| `--
|
|
78
|
-
| `--
|
|
146
|
+
| `--verbose` | Show trigger + file + first line of command |
|
|
147
|
+
| `--debug` | Show full interpolated commands |
|
|
148
|
+
| `--state <path>` | State file for idempotent `once` mode |
|
|
79
149
|
|
|
80
150
|
## Trigger Matchers
|
|
81
151
|
|
|
152
|
+
All matchers work in both `triggers` (via `match:`) and `pipelines` (via `trigger:`).
|
|
153
|
+
|
|
82
154
|
### `path` — glob pattern matching
|
|
83
155
|
```yaml
|
|
84
|
-
|
|
85
|
-
path: "posts/**/*.md"
|
|
156
|
+
path: "posts/**/*.md"
|
|
86
157
|
```
|
|
87
158
|
|
|
88
|
-
### `frontmatter` — match specific
|
|
159
|
+
### `frontmatter` — match specific values
|
|
89
160
|
```yaml
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
161
|
+
frontmatter:
|
|
162
|
+
status: publish
|
|
163
|
+
type: blog
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Negation
|
|
167
|
+
```yaml
|
|
168
|
+
frontmatter:
|
|
169
|
+
status: "!draft" # matches anything except draft
|
|
94
170
|
```
|
|
95
171
|
|
|
96
172
|
### `frontmatter_changed` — fires when specific fields change
|
|
97
173
|
```yaml
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
- tags
|
|
174
|
+
frontmatter_changed:
|
|
175
|
+
- title
|
|
176
|
+
- tags
|
|
102
177
|
```
|
|
103
|
-
This is the key differentiator
|
|
178
|
+
This is the key differentiator. md-pipe tracks frontmatter state and only triggers when the fields you care about actually change.
|
|
104
179
|
|
|
105
180
|
### `tags` — match files with specific tags
|
|
106
181
|
```yaml
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
- urgent
|
|
110
|
-
- review
|
|
182
|
+
tags:
|
|
183
|
+
- urgent
|
|
111
184
|
```
|
|
112
185
|
|
|
113
|
-
|
|
186
|
+
### `content` / `content_regex` — match body text
|
|
187
|
+
```yaml
|
|
188
|
+
content: "TODO"
|
|
189
|
+
content_regex: "\\[ \\]" # unchecked checkboxes
|
|
190
|
+
```
|
|
114
191
|
|
|
115
|
-
|
|
192
|
+
Matchers can be combined — all conditions must match.
|
|
116
193
|
|
|
117
|
-
|
|
194
|
+
## Step Details
|
|
118
195
|
|
|
119
|
-
|
|
120
|
-
|---|---|
|
|
121
|
-
| `$FILE` | Absolute path to the changed file |
|
|
122
|
-
| `$DIR` | Directory containing the file |
|
|
123
|
-
| `$BASENAME` | Filename without directory |
|
|
124
|
-
| `$RELATIVE` | Path relative to watch directory |
|
|
125
|
-
| `$FRONTMATTER` | JSON string of all frontmatter |
|
|
126
|
-
| `$DIFF` | JSON string of changed frontmatter fields |
|
|
127
|
-
| `$TAGS` | Comma-separated list of tags |
|
|
196
|
+
### `update-frontmatter`
|
|
128
197
|
|
|
129
|
-
|
|
198
|
+
Write fields back to the source markdown file:
|
|
130
199
|
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
"total": 42,
|
|
138
|
-
"matched": 3,
|
|
139
|
-
"actions": [
|
|
140
|
-
{
|
|
141
|
-
"triggerName": "publish",
|
|
142
|
-
"filePath": "/docs/posts/hello.md",
|
|
143
|
-
"command": "./scripts/deploy.sh /docs/posts/hello.md",
|
|
144
|
-
"exitCode": 0,
|
|
145
|
-
"stdout": "deployed",
|
|
146
|
-
"stderr": "",
|
|
147
|
-
"durationMs": 150
|
|
148
|
-
}
|
|
149
|
-
],
|
|
150
|
-
"errors": []
|
|
151
|
-
}
|
|
200
|
+
```yaml
|
|
201
|
+
- update-frontmatter:
|
|
202
|
+
published_at: "{{now}}"
|
|
203
|
+
published: "true" # coerced to boolean
|
|
204
|
+
url: "{{step.0.stdout}}" # from previous step
|
|
152
205
|
```
|
|
153
206
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
207
|
+
Values are coerced: `"true"` → `true`, `"false"` → `false`, numeric strings → numbers.
|
|
208
|
+
|
|
209
|
+
### `copy`
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
- copy: { to: "./_site/posts" } # preserves directory structure
|
|
213
|
+
- copy: { to: "./_site", flatten: true } # flat copy
|
|
157
214
|
```
|
|
158
215
|
|
|
159
|
-
|
|
216
|
+
### `webhook`
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
- webhook:
|
|
220
|
+
url: "$WEBHOOK_URL" # env var expansion
|
|
221
|
+
method: POST # default
|
|
222
|
+
headers:
|
|
223
|
+
Authorization: "Bearer $API_KEY"
|
|
224
|
+
body:
|
|
225
|
+
title: "{{fm.title}}"
|
|
226
|
+
file: "{{relative}}"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `template`
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
- template:
|
|
233
|
+
src: "./templates/post.html"
|
|
234
|
+
out: "./_site/{{slug}}.html"
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Template files use the same `{{variable}}` syntax.
|
|
238
|
+
|
|
239
|
+
### Error Handling
|
|
240
|
+
|
|
241
|
+
```yaml
|
|
242
|
+
pipelines:
|
|
243
|
+
- name: resilient
|
|
244
|
+
continue_on_error: true # pipeline-level
|
|
245
|
+
steps:
|
|
246
|
+
- run: "might-fail.sh"
|
|
247
|
+
continue_on_error: true # step-level
|
|
248
|
+
- run: "always-runs.sh"
|
|
249
|
+
```
|
|
160
250
|
|
|
161
|
-
|
|
251
|
+
## Environment Variables
|
|
162
252
|
|
|
163
|
-
|
|
253
|
+
Shell commands (`run` steps and legacy triggers) receive:
|
|
164
254
|
|
|
165
|
-
|
|
255
|
+
| Variable | Description |
|
|
256
|
+
|---|---|
|
|
257
|
+
| `$FILE` | Absolute path |
|
|
258
|
+
| `$DIR` | Directory |
|
|
259
|
+
| `$BASENAME` | Filename |
|
|
260
|
+
| `$RELATIVE` | Relative path |
|
|
261
|
+
| `$SLUG` | Filename without extension |
|
|
262
|
+
| `$FRONTMATTER` | JSON string of all frontmatter |
|
|
263
|
+
| `$DIFF` | JSON of changed fields |
|
|
264
|
+
| `$TAGS` | Comma-separated tags |
|
|
265
|
+
| `$FM_<field>` | Direct frontmatter access |
|
|
266
|
+
| `$STEP_OUTPUT` | stdout from the previous step |
|
|
267
|
+
| `$STEP_N_STDOUT` | stdout from step N |
|
|
166
268
|
|
|
167
|
-
|
|
269
|
+
## Idempotent Mode (--state)
|
|
168
270
|
|
|
169
|
-
|
|
271
|
+
```bash
|
|
272
|
+
md-pipe once --state .md-pipe-state.json
|
|
273
|
+
# Only processes files that changed since last run
|
|
274
|
+
```
|
|
170
275
|
|
|
171
|
-
##
|
|
276
|
+
## Use Cases
|
|
172
277
|
|
|
173
|
-
-
|
|
174
|
-
-
|
|
278
|
+
- **Content pipeline** — write markdown, auto-publish to static site + CMS
|
|
279
|
+
- **Docs-as-code** — deploy pages when `status: publish` is set
|
|
280
|
+
- **Agent workspace** — trigger reindexing when agents update files
|
|
281
|
+
- **Obsidian vault** — auto-archive, generate backlinks, push to git
|
|
282
|
+
- **Digital garden** — auto-generate RSS, index pages, deploy
|
|
283
|
+
- **CI/CD** — `md-pipe once --state .state.json` for idempotent processing
|
|
175
284
|
|
|
176
285
|
## License
|
|
177
286
|
|
package/dist/cli.js
CHANGED
|
@@ -11,28 +11,34 @@ const config_js_1 = require("./core/config.js");
|
|
|
11
11
|
const watcher_js_1 = require("./core/watcher.js");
|
|
12
12
|
const once_js_1 = require("./core/once.js");
|
|
13
13
|
const test_file_js_1 = require("./core/test-file.js");
|
|
14
|
-
const
|
|
14
|
+
const run_command_js_1 = require("./core/run-command.js");
|
|
15
|
+
const VERSION = '0.3.0';
|
|
15
16
|
function printHelp() {
|
|
16
17
|
console.log(`
|
|
17
|
-
${chalk_1.default.bold('md-pipe')} —
|
|
18
|
-
${chalk_1.default.dim('
|
|
18
|
+
${chalk_1.default.bold('md-pipe')} — markdown content pipelines
|
|
19
|
+
${chalk_1.default.dim('Write markdown → auto-publish everywhere.')}
|
|
19
20
|
|
|
20
21
|
${chalk_1.default.bold('Usage:')}
|
|
21
|
-
md-pipe init
|
|
22
|
-
md-pipe watch
|
|
23
|
-
md-pipe once
|
|
24
|
-
md-pipe
|
|
22
|
+
md-pipe init Scaffold a .md-pipe.yml config file
|
|
23
|
+
md-pipe watch Start watching for changes
|
|
24
|
+
md-pipe once Run triggers against current files (CI/batch)
|
|
25
|
+
md-pipe run <pipeline> [file] Manually trigger a pipeline on a file
|
|
26
|
+
md-pipe test <file> Show which triggers/pipelines match a file
|
|
25
27
|
|
|
26
28
|
${chalk_1.default.bold('Flags:')}
|
|
27
29
|
--config, -c <path> Path to config file (default: .md-pipe.yml)
|
|
28
30
|
--dry-run Show matches without executing actions
|
|
29
31
|
--json Output in JSON format
|
|
30
|
-
--verbose Show
|
|
32
|
+
--verbose Show trigger + file + first line of command
|
|
33
|
+
--debug Show full interpolated commands
|
|
34
|
+
--state <path> State file for idempotent once mode
|
|
31
35
|
--version, -v Show version
|
|
32
36
|
--help, -h Show this help
|
|
33
37
|
|
|
34
38
|
${chalk_1.default.bold('Config (.md-pipe.yml):')}
|
|
35
39
|
watch: ./docs
|
|
40
|
+
|
|
41
|
+
# Legacy triggers (simple match + run)
|
|
36
42
|
triggers:
|
|
37
43
|
- name: publish
|
|
38
44
|
match:
|
|
@@ -40,14 +46,32 @@ ${chalk_1.default.bold('Config (.md-pipe.yml):')}
|
|
|
40
46
|
frontmatter: { status: publish }
|
|
41
47
|
run: "echo Publishing $FILE"
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
# Pipelines (v0.3+) — multi-step content pipelines
|
|
50
|
+
pipelines:
|
|
51
|
+
- name: publish-post
|
|
52
|
+
trigger:
|
|
53
|
+
path: "posts/**"
|
|
54
|
+
frontmatter: { status: publish }
|
|
55
|
+
frontmatter_changed: [status]
|
|
56
|
+
steps:
|
|
57
|
+
- run: "echo Publishing {{fm.title}}"
|
|
58
|
+
- update-frontmatter: { published_at: "{{now}}" }
|
|
59
|
+
- copy: { to: "./_site/posts" }
|
|
60
|
+
- webhook: { url: "$WEBHOOK_URL" }
|
|
61
|
+
|
|
62
|
+
${chalk_1.default.bold('Step types:')}
|
|
63
|
+
run Shell command
|
|
64
|
+
update-frontmatter Write back to source file frontmatter
|
|
65
|
+
webhook POST JSON to a URL
|
|
66
|
+
copy Copy file to destination directory
|
|
67
|
+
template Render a template with file data
|
|
68
|
+
|
|
69
|
+
${chalk_1.default.bold('Template variables:')}
|
|
70
|
+
{{now}} ISO timestamp
|
|
71
|
+
{{date}} YYYY-MM-DD
|
|
72
|
+
{{slug}} Filename without extension
|
|
73
|
+
{{fm.title}} Frontmatter field
|
|
74
|
+
{{step.0.stdout}} Output from step 0
|
|
51
75
|
|
|
52
76
|
${chalk_1.default.bold('More:')} https://github.com/safetnsr/md-pipe
|
|
53
77
|
`);
|
|
@@ -55,10 +79,14 @@ ${chalk_1.default.bold('More:')} https://github.com/safetnsr/md-pipe
|
|
|
55
79
|
function parseArgs(args) {
|
|
56
80
|
let command = '';
|
|
57
81
|
let file;
|
|
82
|
+
let pipelineName;
|
|
58
83
|
let config;
|
|
59
84
|
let dryRun = false;
|
|
60
85
|
let json = false;
|
|
61
86
|
let verbose = false;
|
|
87
|
+
let debug = false;
|
|
88
|
+
let state;
|
|
89
|
+
let positionals = [];
|
|
62
90
|
for (let i = 0; i < args.length; i++) {
|
|
63
91
|
const arg = args[i];
|
|
64
92
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -81,6 +109,14 @@ function parseArgs(args) {
|
|
|
81
109
|
verbose = true;
|
|
82
110
|
continue;
|
|
83
111
|
}
|
|
112
|
+
if (arg === '--debug') {
|
|
113
|
+
debug = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (arg === '--state') {
|
|
117
|
+
state = args[++i];
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
84
120
|
if (arg === '--config' || arg === '-c') {
|
|
85
121
|
config = args[++i];
|
|
86
122
|
continue;
|
|
@@ -89,12 +125,17 @@ function parseArgs(args) {
|
|
|
89
125
|
command = arg;
|
|
90
126
|
continue;
|
|
91
127
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
128
|
+
positionals.push(arg);
|
|
129
|
+
}
|
|
130
|
+
// Parse positionals based on command
|
|
131
|
+
if (command === 'run') {
|
|
132
|
+
pipelineName = positionals[0];
|
|
133
|
+
file = positionals[1];
|
|
134
|
+
}
|
|
135
|
+
else if (command === 'test') {
|
|
136
|
+
file = positionals[0];
|
|
96
137
|
}
|
|
97
|
-
return { command: command || 'help', file, config, dryRun, json, verbose };
|
|
138
|
+
return { command: command || 'help', file, pipelineName, config, dryRun, json, verbose, debug, state };
|
|
98
139
|
}
|
|
99
140
|
function getConfig(configPath) {
|
|
100
141
|
const cwd = process.cwd();
|
|
@@ -105,6 +146,31 @@ function getConfig(configPath) {
|
|
|
105
146
|
}
|
|
106
147
|
return (0, config_js_1.loadConfig)(cfgPath);
|
|
107
148
|
}
|
|
149
|
+
function formatCommand(command, debug) {
|
|
150
|
+
if (debug)
|
|
151
|
+
return command;
|
|
152
|
+
const firstLine = command.split('\n')[0].trim();
|
|
153
|
+
const truncated = firstLine.length > 80 ? firstLine.slice(0, 77) + '...' : firstLine;
|
|
154
|
+
return command.includes('\n') ? truncated + ' …' : truncated;
|
|
155
|
+
}
|
|
156
|
+
function formatPipelineResult(result, debug) {
|
|
157
|
+
const overallStatus = result.success
|
|
158
|
+
? chalk_1.default.green('✓')
|
|
159
|
+
: chalk_1.default.red('✗');
|
|
160
|
+
console.log(` ${overallStatus} Pipeline ${chalk_1.default.cyan(result.pipelineName)} ` +
|
|
161
|
+
chalk_1.default.dim(`(${result.durationMs}ms)`));
|
|
162
|
+
for (let i = 0; i < result.steps.length; i++) {
|
|
163
|
+
const step = result.steps[i];
|
|
164
|
+
const status = step.success
|
|
165
|
+
? chalk_1.default.green(' ✓')
|
|
166
|
+
: chalk_1.default.red(' ✗');
|
|
167
|
+
const typeLabel = chalk_1.default.dim(`[${step.type}]`);
|
|
168
|
+
console.log(` ${status} ${typeLabel} ${chalk_1.default.dim(step.stdout.split('\n')[0].slice(0, 80))}`);
|
|
169
|
+
if (step.stderr) {
|
|
170
|
+
console.log(chalk_1.default.red(` ${step.stderr.split('\n')[0]}`));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
108
174
|
async function cmdInit() {
|
|
109
175
|
const target = (0, path_1.resolve)(process.cwd(), '.md-pipe.yml');
|
|
110
176
|
if ((0, fs_1.existsSync)(target)) {
|
|
@@ -122,16 +188,22 @@ async function cmdWatch(opts) {
|
|
|
122
188
|
console.error(chalk_1.default.red(`Watch directory not found: ${config.watch}`));
|
|
123
189
|
process.exit(1);
|
|
124
190
|
}
|
|
191
|
+
const totalTriggers = config.triggers.length + config.pipelines.length;
|
|
125
192
|
watcher.on('ready', () => {
|
|
126
193
|
console.log(chalk_1.default.green('✓') + ` Watching ${config.watch}`);
|
|
127
|
-
|
|
194
|
+
if (config.triggers.length > 0)
|
|
195
|
+
console.log(chalk_1.default.dim(` ${config.triggers.length} trigger(s)`));
|
|
196
|
+
if (config.pipelines.length > 0)
|
|
197
|
+
console.log(chalk_1.default.dim(` ${config.pipelines.length} pipeline(s)`));
|
|
198
|
+
if (config.debounce)
|
|
199
|
+
console.log(chalk_1.default.dim(` debounce: ${config.debounce}ms`));
|
|
128
200
|
if (opts.dryRun)
|
|
129
201
|
console.log(chalk_1.default.yellow(' [dry-run mode — actions will not execute]'));
|
|
130
202
|
console.log(chalk_1.default.dim(' Press Ctrl+C to stop\n'));
|
|
131
203
|
});
|
|
132
204
|
watcher.on('match', (result) => {
|
|
133
205
|
if (opts.json)
|
|
134
|
-
return;
|
|
206
|
+
return;
|
|
135
207
|
const ts = new Date().toLocaleTimeString();
|
|
136
208
|
console.log(chalk_1.default.dim(`[${ts}]`) + ' ' +
|
|
137
209
|
chalk_1.default.cyan(`▸ ${result.trigger.name}`) + ' ' +
|
|
@@ -146,18 +218,25 @@ async function cmdWatch(opts) {
|
|
|
146
218
|
const status = result.exitCode === 0
|
|
147
219
|
? chalk_1.default.green('✓')
|
|
148
220
|
: chalk_1.default.red(`✗ exit ${result.exitCode}`);
|
|
149
|
-
|
|
150
|
-
|
|
221
|
+
const cmdDisplay = formatCommand(result.command, opts.debug);
|
|
222
|
+
console.log(` ${status} ${chalk_1.default.dim(cmdDisplay)}`);
|
|
223
|
+
if (result.stdout && (opts.verbose || opts.debug)) {
|
|
151
224
|
console.log(chalk_1.default.dim(' ' + result.stdout.replace(/\n/g, '\n ')));
|
|
152
225
|
}
|
|
153
226
|
if (result.stderr) {
|
|
154
227
|
console.log(chalk_1.default.red(' ' + result.stderr.replace(/\n/g, '\n ')));
|
|
155
228
|
}
|
|
156
229
|
});
|
|
230
|
+
watcher.on('pipeline', (result) => {
|
|
231
|
+
if (opts.json) {
|
|
232
|
+
console.log(JSON.stringify(result));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
formatPipelineResult(result, opts.debug);
|
|
236
|
+
});
|
|
157
237
|
watcher.on('error', (err, filePath) => {
|
|
158
238
|
console.error(chalk_1.default.red(`Error${filePath ? ` (${filePath})` : ''}: ${err.message}`));
|
|
159
239
|
});
|
|
160
|
-
// Graceful shutdown
|
|
161
240
|
process.on('SIGINT', async () => {
|
|
162
241
|
console.log(chalk_1.default.dim('\nStopping watcher...'));
|
|
163
242
|
await watcher.stop();
|
|
@@ -171,13 +250,16 @@ function cmdOnce(opts) {
|
|
|
171
250
|
console.error(chalk_1.default.red(`Watch directory not found: ${config.watch}`));
|
|
172
251
|
process.exit(1);
|
|
173
252
|
}
|
|
174
|
-
const result = (0, once_js_1.runOnce)(config, opts.dryRun);
|
|
253
|
+
const result = (0, once_js_1.runOnce)(config, opts.dryRun, opts.state);
|
|
175
254
|
if (opts.json) {
|
|
176
255
|
console.log(JSON.stringify(result, null, 2));
|
|
177
256
|
return;
|
|
178
257
|
}
|
|
179
258
|
console.log(chalk_1.default.bold('md-pipe once'));
|
|
180
|
-
|
|
259
|
+
let summary = ` Scanned ${result.total} files, ${result.matched} trigger match(es)`;
|
|
260
|
+
if (result.skipped > 0)
|
|
261
|
+
summary += `, ${result.skipped} skipped (unchanged)`;
|
|
262
|
+
console.log(chalk_1.default.dim(summary + '\n'));
|
|
181
263
|
if (opts.dryRun) {
|
|
182
264
|
console.log(chalk_1.default.yellow(' [dry-run mode — actions were not executed]\n'));
|
|
183
265
|
}
|
|
@@ -185,8 +267,9 @@ function cmdOnce(opts) {
|
|
|
185
267
|
const status = action.exitCode === 0
|
|
186
268
|
? chalk_1.default.green('✓')
|
|
187
269
|
: chalk_1.default.red(`✗ exit ${action.exitCode}`);
|
|
188
|
-
|
|
189
|
-
|
|
270
|
+
const cmdDisplay = formatCommand(action.command, opts.debug);
|
|
271
|
+
console.log(`${status} ${chalk_1.default.cyan(action.triggerName)} → ${chalk_1.default.dim(cmdDisplay)}`);
|
|
272
|
+
if (action.stdout && (opts.verbose || opts.debug)) {
|
|
190
273
|
console.log(chalk_1.default.dim(' ' + action.stdout));
|
|
191
274
|
}
|
|
192
275
|
if (action.stderr) {
|
|
@@ -199,6 +282,29 @@ function cmdOnce(opts) {
|
|
|
199
282
|
if (result.errors.length > 0)
|
|
200
283
|
process.exit(1);
|
|
201
284
|
}
|
|
285
|
+
async function cmdRun(opts) {
|
|
286
|
+
if (!opts.pipelineName) {
|
|
287
|
+
console.error(chalk_1.default.red('Usage: md-pipe run <pipeline-name> [file]'));
|
|
288
|
+
console.error(chalk_1.default.dim(' Run a specific pipeline manually.'));
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
const config = getConfig(opts.config);
|
|
292
|
+
const result = await (0, run_command_js_1.runPipelineCommand)(config, opts.pipelineName, opts.file, opts.dryRun);
|
|
293
|
+
if (opts.json) {
|
|
294
|
+
console.log(JSON.stringify(result, null, 2));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
console.log(chalk_1.default.bold(`md-pipe run ${opts.pipelineName}`) + (opts.file ? ` ${opts.file}` : ''));
|
|
298
|
+
console.log();
|
|
299
|
+
for (const pr of result.results) {
|
|
300
|
+
formatPipelineResult(pr, opts.debug);
|
|
301
|
+
}
|
|
302
|
+
for (const err of result.errors) {
|
|
303
|
+
console.error(chalk_1.default.red(err));
|
|
304
|
+
}
|
|
305
|
+
if (!result.success)
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
202
308
|
function cmdTest(opts) {
|
|
203
309
|
if (!opts.file) {
|
|
204
310
|
console.error(chalk_1.default.red('Usage: md-pipe test <file>'));
|
|
@@ -214,11 +320,12 @@ function cmdTest(opts) {
|
|
|
214
320
|
console.log(chalk_1.default.dim(` Frontmatter: ${JSON.stringify(result.frontmatter)}`));
|
|
215
321
|
console.log(chalk_1.default.dim(` Tags: [${result.tags.join(', ')}]\n`));
|
|
216
322
|
if (result.matches.length === 0) {
|
|
217
|
-
console.log(chalk_1.default.yellow(' No triggers matched this file.'));
|
|
323
|
+
console.log(chalk_1.default.yellow(' No triggers or pipelines matched this file.'));
|
|
218
324
|
return;
|
|
219
325
|
}
|
|
220
326
|
for (const m of result.matches) {
|
|
221
|
-
|
|
327
|
+
const label = m.type === 'pipeline' ? chalk_1.default.magenta('[pipeline]') : chalk_1.default.blue('[trigger]');
|
|
328
|
+
console.log(chalk_1.default.green(' ✓') + ` ${label} ${chalk_1.default.cyan(m.triggerName)}: ${m.reason}`);
|
|
222
329
|
}
|
|
223
330
|
}
|
|
224
331
|
async function main() {
|
|
@@ -235,6 +342,9 @@ async function main() {
|
|
|
235
342
|
case 'once':
|
|
236
343
|
cmdOnce(opts);
|
|
237
344
|
break;
|
|
345
|
+
case 'run':
|
|
346
|
+
await cmdRun(opts);
|
|
347
|
+
break;
|
|
238
348
|
case 'test':
|
|
239
349
|
cmdTest(opts);
|
|
240
350
|
break;
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
|
+
import type { PipelineDef } from './pipeline.js';
|
|
1
2
|
export interface TriggerMatch {
|
|
2
3
|
path?: string;
|
|
3
4
|
frontmatter?: Record<string, unknown>;
|
|
4
5
|
frontmatter_changed?: string[];
|
|
5
6
|
tags?: string[];
|
|
7
|
+
content?: string;
|
|
8
|
+
content_regex?: string;
|
|
6
9
|
}
|
|
7
10
|
export interface TriggerDef {
|
|
8
11
|
name: string;
|
|
9
12
|
match: TriggerMatch;
|
|
10
13
|
run: string;
|
|
14
|
+
cwd?: 'project' | 'file';
|
|
11
15
|
}
|
|
12
16
|
export interface MdPipeConfig {
|
|
13
17
|
watch: string;
|
|
18
|
+
configDir: string;
|
|
14
19
|
triggers: TriggerDef[];
|
|
20
|
+
pipelines: PipelineDef[];
|
|
21
|
+
debounce?: number;
|
|
15
22
|
}
|
|
16
23
|
export declare function findConfigFile(dir: string): string | null;
|
|
17
24
|
export declare function loadConfig(configPath: string): MdPipeConfig;
|