@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
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,17 +23,97 @@ 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
|
|
36
|
-
debounce: 500 # ms, for watch mode (default: 200)
|
|
37
117
|
|
|
38
118
|
triggers:
|
|
39
119
|
- name: publish
|
|
@@ -41,24 +121,11 @@ triggers:
|
|
|
41
121
|
path: "posts/**"
|
|
42
122
|
frontmatter:
|
|
43
123
|
status: publish
|
|
44
|
-
|
|
45
|
-
run: "./scripts/deploy.sh $FILE"
|
|
46
|
-
|
|
47
|
-
- name: reindex
|
|
48
|
-
match:
|
|
49
|
-
frontmatter_changed:
|
|
50
|
-
- title
|
|
51
|
-
- tags
|
|
52
|
-
- category
|
|
53
|
-
run: "./scripts/reindex.sh $FILE"
|
|
54
|
-
|
|
55
|
-
- name: urgent-notify
|
|
56
|
-
match:
|
|
57
|
-
tags:
|
|
58
|
-
- urgent
|
|
59
|
-
run: "curl -X POST $WEBHOOK -d '{\"file\": \"$FILE\"}'"
|
|
124
|
+
run: "echo Publishing $FILE"
|
|
60
125
|
```
|
|
61
126
|
|
|
127
|
+
Triggers and pipelines can coexist in the same config.
|
|
128
|
+
|
|
62
129
|
## Commands
|
|
63
130
|
|
|
64
131
|
| Command | Description |
|
|
@@ -66,7 +133,8 @@ triggers:
|
|
|
66
133
|
| `md-pipe init` | Scaffold a `.md-pipe.yml` config file |
|
|
67
134
|
| `md-pipe watch` | Start watching for changes and trigger actions |
|
|
68
135
|
| `md-pipe once` | Run triggers against current files (CI/batch mode) |
|
|
69
|
-
| `md-pipe
|
|
136
|
+
| `md-pipe run <pipeline> [file]` | Manually trigger a pipeline |
|
|
137
|
+
| `md-pipe test <file>` | Show which triggers/pipelines match a file |
|
|
70
138
|
|
|
71
139
|
## Flags
|
|
72
140
|
|
|
@@ -78,184 +146,141 @@ triggers:
|
|
|
78
146
|
| `--verbose` | Show trigger + file + first line of command |
|
|
79
147
|
| `--debug` | Show full interpolated commands |
|
|
80
148
|
| `--state <path>` | State file for idempotent `once` mode |
|
|
81
|
-
| `--version, -v` | Show version |
|
|
82
|
-
| `--help, -h` | Show help |
|
|
83
149
|
|
|
84
150
|
## Trigger Matchers
|
|
85
151
|
|
|
152
|
+
All matchers work in both `triggers` (via `match:`) and `pipelines` (via `trigger:`).
|
|
153
|
+
|
|
86
154
|
### `path` — glob pattern matching
|
|
87
155
|
```yaml
|
|
88
|
-
|
|
89
|
-
path: "posts/**/*.md"
|
|
156
|
+
path: "posts/**/*.md"
|
|
90
157
|
```
|
|
91
158
|
|
|
92
|
-
### `frontmatter` — match specific
|
|
159
|
+
### `frontmatter` — match specific values
|
|
93
160
|
```yaml
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
type: blog
|
|
161
|
+
frontmatter:
|
|
162
|
+
status: publish
|
|
163
|
+
type: blog
|
|
98
164
|
```
|
|
99
165
|
|
|
100
|
-
### Negation
|
|
166
|
+
### Negation
|
|
101
167
|
```yaml
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
status: "!draft" # matches publish, review, etc — NOT draft
|
|
168
|
+
frontmatter:
|
|
169
|
+
status: "!draft" # matches anything except draft
|
|
105
170
|
```
|
|
106
171
|
|
|
107
172
|
### `frontmatter_changed` — fires when specific fields change
|
|
108
173
|
```yaml
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- tags
|
|
174
|
+
frontmatter_changed:
|
|
175
|
+
- title
|
|
176
|
+
- tags
|
|
113
177
|
```
|
|
114
|
-
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.
|
|
115
179
|
|
|
116
180
|
### `tags` — match files with specific tags
|
|
117
181
|
```yaml
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- urgent
|
|
121
|
-
- review
|
|
182
|
+
tags:
|
|
183
|
+
- urgent
|
|
122
184
|
```
|
|
123
185
|
|
|
124
|
-
### `content` — match body text
|
|
186
|
+
### `content` / `content_regex` — match body text
|
|
125
187
|
```yaml
|
|
126
|
-
|
|
127
|
-
|
|
188
|
+
content: "TODO"
|
|
189
|
+
content_regex: "\\[ \\]" # unchecked checkboxes
|
|
128
190
|
```
|
|
129
191
|
|
|
130
|
-
|
|
131
|
-
```yaml
|
|
132
|
-
match:
|
|
133
|
-
content_regex: "\\[ \\]" # unchecked checkboxes
|
|
134
|
-
```
|
|
192
|
+
Matchers can be combined — all conditions must match.
|
|
135
193
|
|
|
136
|
-
|
|
194
|
+
## Step Details
|
|
137
195
|
|
|
138
|
-
|
|
196
|
+
### `update-frontmatter`
|
|
197
|
+
|
|
198
|
+
Write fields back to the source markdown file:
|
|
139
199
|
|
|
140
|
-
### `cwd` — working directory for commands
|
|
141
200
|
```yaml
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
cwd: project # "project" = config dir, "file" = file dir (default)
|
|
147
|
-
run: bash scripts/build.sh $FILE
|
|
201
|
+
- update-frontmatter:
|
|
202
|
+
published_at: "{{now}}"
|
|
203
|
+
published: "true" # coerced to boolean
|
|
204
|
+
url: "{{step.0.stdout}}" # from previous step
|
|
148
205
|
```
|
|
149
206
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
Scripts receive these env vars:
|
|
153
|
-
|
|
154
|
-
| Variable | Description |
|
|
155
|
-
|---|---|
|
|
156
|
-
| `$FILE` | Absolute path to the changed file |
|
|
157
|
-
| `$DIR` | Directory containing the file |
|
|
158
|
-
| `$BASENAME` | Filename without directory |
|
|
159
|
-
| `$RELATIVE` | Path relative to watch directory |
|
|
160
|
-
| `$FRONTMATTER` | JSON string of all frontmatter |
|
|
161
|
-
| `$DIFF` | JSON string of changed frontmatter fields |
|
|
162
|
-
| `$TAGS` | Comma-separated list of tags |
|
|
163
|
-
| `$FM_<field>` | Direct access to frontmatter fields |
|
|
164
|
-
|
|
165
|
-
### `$FM_` variables
|
|
207
|
+
Values are coerced: `"true"` → `true`, `"false"` → `false`, numeric strings → numbers.
|
|
166
208
|
|
|
167
|
-
|
|
209
|
+
### `copy`
|
|
168
210
|
|
|
169
211
|
```yaml
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
status: publish
|
|
173
|
-
version: "2.1"
|
|
174
|
-
---
|
|
212
|
+
- copy: { to: "./_site/posts" } # preserves directory structure
|
|
213
|
+
- copy: { to: "./_site", flatten: true } # flat copy
|
|
175
214
|
```
|
|
176
215
|
|
|
177
|
-
|
|
178
|
-
echo $FM_title # → Hello World
|
|
179
|
-
echo $FM_status # → publish
|
|
180
|
-
echo $FM_version # → 2.1
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
No more `node -pe "JSON.parse(...)"` to extract a single field!
|
|
184
|
-
|
|
185
|
-
## Idempotent Mode (--state)
|
|
216
|
+
### `webhook`
|
|
186
217
|
|
|
187
|
-
|
|
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
|
+
```
|
|
188
228
|
|
|
189
|
-
|
|
190
|
-
# First run: processes all matching files
|
|
191
|
-
md-pipe once --state .md-pipe-state.json
|
|
229
|
+
### `template`
|
|
192
230
|
|
|
193
|
-
|
|
194
|
-
|
|
231
|
+
```yaml
|
|
232
|
+
- template:
|
|
233
|
+
src: "./templates/post.html"
|
|
234
|
+
out: "./_site/{{slug}}.html"
|
|
195
235
|
```
|
|
196
236
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
## Watch Mode Debouncing
|
|
237
|
+
Template files use the same `{{variable}}` syntax.
|
|
200
238
|
|
|
201
|
-
|
|
239
|
+
### Error Handling
|
|
202
240
|
|
|
203
241
|
```yaml
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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"
|
|
208
249
|
```
|
|
209
250
|
|
|
210
|
-
##
|
|
251
|
+
## Environment Variables
|
|
211
252
|
|
|
212
|
-
|
|
213
|
-
md-pipe once --json
|
|
214
|
-
```
|
|
253
|
+
Shell commands (`run` steps and legacy triggers) receive:
|
|
215
254
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
],
|
|
232
|
-
"errors": []
|
|
233
|
-
}
|
|
234
|
-
```
|
|
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 |
|
|
268
|
+
|
|
269
|
+
## Idempotent Mode (--state)
|
|
235
270
|
|
|
236
271
|
```bash
|
|
237
|
-
md-pipe
|
|
238
|
-
#
|
|
272
|
+
md-pipe once --state .md-pipe-state.json
|
|
273
|
+
# Only processes files that changed since last run
|
|
239
274
|
```
|
|
240
275
|
|
|
241
276
|
## Use Cases
|
|
242
277
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
**Agent workspace
|
|
246
|
-
|
|
247
|
-
**
|
|
248
|
-
|
|
249
|
-
**Digital garden** — auto-generate RSS feed when new posts are added
|
|
250
|
-
|
|
251
|
-
**CI/CD for markdown** — `md-pipe once --state .state.json` in your CI pipeline for idempotent processing
|
|
252
|
-
|
|
253
|
-
**TODO tracking** — find files with `content: "TODO"` or `content_regex: "\\[ \\]"` for unchecked tasks
|
|
254
|
-
|
|
255
|
-
## Pair With
|
|
256
|
-
|
|
257
|
-
- [`@safetnsr/md-kit`](https://github.com/safetnsr/md-kit) — markdown workspace toolkit (wikilinks, broken links, backlink maps)
|
|
258
|
-
- [`@safetnsr/ai-ready`](https://github.com/safetnsr/ai-ready) — AI-compatibility score for your codebase
|
|
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
|
|
259
284
|
|
|
260
285
|
## License
|
|
261
286
|
|
package/dist/cli.js
CHANGED
|
@@ -11,17 +11,19 @@ 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)
|
|
@@ -35,26 +37,41 @@ ${chalk_1.default.bold('Flags:')}
|
|
|
35
37
|
|
|
36
38
|
${chalk_1.default.bold('Config (.md-pipe.yml):')}
|
|
37
39
|
watch: ./docs
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
# Legacy triggers (simple match + run)
|
|
39
42
|
triggers:
|
|
40
43
|
- name: publish
|
|
41
44
|
match:
|
|
42
45
|
path: "posts/**"
|
|
43
46
|
frontmatter: { status: publish }
|
|
44
|
-
content: "TODO" # body substring
|
|
45
|
-
content_regex: "\\\\[\\\\s\\\\]" # body regex
|
|
46
|
-
cwd: project # "project" or "file" (default)
|
|
47
47
|
run: "echo Publishing $FILE"
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
58
75
|
|
|
59
76
|
${chalk_1.default.bold('More:')} https://github.com/safetnsr/md-pipe
|
|
60
77
|
`);
|
|
@@ -62,12 +79,14 @@ ${chalk_1.default.bold('More:')} https://github.com/safetnsr/md-pipe
|
|
|
62
79
|
function parseArgs(args) {
|
|
63
80
|
let command = '';
|
|
64
81
|
let file;
|
|
82
|
+
let pipelineName;
|
|
65
83
|
let config;
|
|
66
84
|
let dryRun = false;
|
|
67
85
|
let json = false;
|
|
68
86
|
let verbose = false;
|
|
69
87
|
let debug = false;
|
|
70
88
|
let state;
|
|
89
|
+
let positionals = [];
|
|
71
90
|
for (let i = 0; i < args.length; i++) {
|
|
72
91
|
const arg = args[i];
|
|
73
92
|
if (arg === '--help' || arg === '-h') {
|
|
@@ -106,12 +125,17 @@ function parseArgs(args) {
|
|
|
106
125
|
command = arg;
|
|
107
126
|
continue;
|
|
108
127
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
128
|
+
positionals.push(arg);
|
|
129
|
+
}
|
|
130
|
+
// Parse positionals based on command
|
|
131
|
+
if (command === 'run') {
|
|
132
|
+
pipelineName = positionals[0];
|
|
133
|
+
file = positionals[1];
|
|
113
134
|
}
|
|
114
|
-
|
|
135
|
+
else if (command === 'test') {
|
|
136
|
+
file = positionals[0];
|
|
137
|
+
}
|
|
138
|
+
return { command: command || 'help', file, pipelineName, config, dryRun, json, verbose, debug, state };
|
|
115
139
|
}
|
|
116
140
|
function getConfig(configPath) {
|
|
117
141
|
const cwd = process.cwd();
|
|
@@ -122,7 +146,6 @@ function getConfig(configPath) {
|
|
|
122
146
|
}
|
|
123
147
|
return (0, config_js_1.loadConfig)(cfgPath);
|
|
124
148
|
}
|
|
125
|
-
/** Format command for display: verbose shows first line, debug shows full */
|
|
126
149
|
function formatCommand(command, debug) {
|
|
127
150
|
if (debug)
|
|
128
151
|
return command;
|
|
@@ -130,6 +153,24 @@ function formatCommand(command, debug) {
|
|
|
130
153
|
const truncated = firstLine.length > 80 ? firstLine.slice(0, 77) + '...' : firstLine;
|
|
131
154
|
return command.includes('\n') ? truncated + ' …' : truncated;
|
|
132
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
|
+
}
|
|
133
174
|
async function cmdInit() {
|
|
134
175
|
const target = (0, path_1.resolve)(process.cwd(), '.md-pipe.yml');
|
|
135
176
|
if ((0, fs_1.existsSync)(target)) {
|
|
@@ -147,9 +188,13 @@ async function cmdWatch(opts) {
|
|
|
147
188
|
console.error(chalk_1.default.red(`Watch directory not found: ${config.watch}`));
|
|
148
189
|
process.exit(1);
|
|
149
190
|
}
|
|
191
|
+
const totalTriggers = config.triggers.length + config.pipelines.length;
|
|
150
192
|
watcher.on('ready', () => {
|
|
151
193
|
console.log(chalk_1.default.green('✓') + ` Watching ${config.watch}`);
|
|
152
|
-
|
|
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)`));
|
|
153
198
|
if (config.debounce)
|
|
154
199
|
console.log(chalk_1.default.dim(` debounce: ${config.debounce}ms`));
|
|
155
200
|
if (opts.dryRun)
|
|
@@ -182,10 +227,16 @@ async function cmdWatch(opts) {
|
|
|
182
227
|
console.log(chalk_1.default.red(' ' + result.stderr.replace(/\n/g, '\n ')));
|
|
183
228
|
}
|
|
184
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
|
+
});
|
|
185
237
|
watcher.on('error', (err, filePath) => {
|
|
186
238
|
console.error(chalk_1.default.red(`Error${filePath ? ` (${filePath})` : ''}: ${err.message}`));
|
|
187
239
|
});
|
|
188
|
-
// Graceful shutdown
|
|
189
240
|
process.on('SIGINT', async () => {
|
|
190
241
|
console.log(chalk_1.default.dim('\nStopping watcher...'));
|
|
191
242
|
await watcher.stop();
|
|
@@ -231,6 +282,29 @@ function cmdOnce(opts) {
|
|
|
231
282
|
if (result.errors.length > 0)
|
|
232
283
|
process.exit(1);
|
|
233
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
|
+
}
|
|
234
308
|
function cmdTest(opts) {
|
|
235
309
|
if (!opts.file) {
|
|
236
310
|
console.error(chalk_1.default.red('Usage: md-pipe test <file>'));
|
|
@@ -246,11 +320,12 @@ function cmdTest(opts) {
|
|
|
246
320
|
console.log(chalk_1.default.dim(` Frontmatter: ${JSON.stringify(result.frontmatter)}`));
|
|
247
321
|
console.log(chalk_1.default.dim(` Tags: [${result.tags.join(', ')}]\n`));
|
|
248
322
|
if (result.matches.length === 0) {
|
|
249
|
-
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.'));
|
|
250
324
|
return;
|
|
251
325
|
}
|
|
252
326
|
for (const m of result.matches) {
|
|
253
|
-
|
|
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}`);
|
|
254
329
|
}
|
|
255
330
|
}
|
|
256
331
|
async function main() {
|
|
@@ -267,6 +342,9 @@ async function main() {
|
|
|
267
342
|
case 'once':
|
|
268
343
|
cmdOnce(opts);
|
|
269
344
|
break;
|
|
345
|
+
case 'run':
|
|
346
|
+
await cmdRun(opts);
|
|
347
|
+
break;
|
|
270
348
|
case 'test':
|
|
271
349
|
cmdTest(opts);
|
|
272
350
|
break;
|
package/dist/core/config.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PipelineDef } from './pipeline.js';
|
|
1
2
|
export interface TriggerMatch {
|
|
2
3
|
path?: string;
|
|
3
4
|
frontmatter?: Record<string, unknown>;
|
|
@@ -16,6 +17,7 @@ export interface MdPipeConfig {
|
|
|
16
17
|
watch: string;
|
|
17
18
|
configDir: string;
|
|
18
19
|
triggers: TriggerDef[];
|
|
20
|
+
pipelines: PipelineDef[];
|
|
19
21
|
debounce?: number;
|
|
20
22
|
}
|
|
21
23
|
export declare function findConfigFile(dir: string): string | null;
|