mdpush 1.0.0 → 1.1.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 +23 -7
- package/assets/SKILL.md +103 -7
- package/assets/scripts/push.sh +327 -22
- package/package.json +2 -27
- package/src/commands/config-command.js +1 -4
- package/src/commands/push-command.js +63 -18
- package/src/index.js +6 -10
package/README.md
CHANGED
|
@@ -35,18 +35,33 @@ Interactive wizard to configure server, token, and default project. Validates yo
|
|
|
35
35
|
|
|
36
36
|
### `mdpush push [files...]`
|
|
37
37
|
|
|
38
|
-
Upload `.md
|
|
38
|
+
Upload `.md`, `.txt`, and `.html` files to a project. HTML files are hosted at `/v/:sid` as sandboxed artifacts with an optional `--type` tag.
|
|
39
39
|
|
|
40
40
|
| Flag | Description |
|
|
41
41
|
|------|-------------|
|
|
42
42
|
| `-p, --project <slug>` | Target project (falls back to default from setup) |
|
|
43
|
-
| `-f, --folder <path>` | Subfolder within the project |
|
|
43
|
+
| `-f, --folder <path>` | Subfolder within the project (md/txt only — HTML artifacts are flat) |
|
|
44
|
+
| `-t, --type <value>` | HTML artifact type: `spec` \| `review` \| `design` \| `report` \| `playground` (HTML files only) |
|
|
44
45
|
|
|
45
46
|
```bash
|
|
46
|
-
mdpush push README.md
|
|
47
|
-
mdpush push -p docs-site *.md
|
|
48
|
-
mdpush push -p docs-site -f guides plan.md
|
|
49
|
-
mdpush push docs/
|
|
47
|
+
mdpush push README.md # Uses default project
|
|
48
|
+
mdpush push -p docs-site *.md # Specific project
|
|
49
|
+
mdpush push -p docs-site -f guides plan.md # Into subfolder
|
|
50
|
+
mdpush push docs/ # Upload entire directory (md/txt only)
|
|
51
|
+
mdpush push design.html --type design # HTML artifact with type tag
|
|
52
|
+
mdpush push spec.html notes.md --type spec # Mixed batch — type applies only to .html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
HTML output includes the viewer URL and type badge:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
🔗 [design] https://docs.example.com/v/abc123 design.html
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`.md`/`.txt` output uses the project-relative direct link:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
🔗 https://docs.example.com/p/docs-site/f/xyz789 plan.md
|
|
50
65
|
```
|
|
51
66
|
|
|
52
67
|
### `mdpush login`
|
|
@@ -102,7 +117,8 @@ Generate tokens from the web UI: **Sidebar > API Tokens > Generate Token**.
|
|
|
102
117
|
| `401 Unauthorized` | Token expired or revoked. Generate a new one from the web UI. |
|
|
103
118
|
| `403 Forbidden` | No write access to this project. Ask the owner to invite you. |
|
|
104
119
|
| `404 Not Found` | Wrong project slug. Check it in the web UI URL: `/p/<slug>` |
|
|
105
|
-
| `413 Too Large` | File exceeds
|
|
120
|
+
| `413 Too Large` | File exceeds 10MB (.md/.txt) or 15MB (.html) limit. |
|
|
121
|
+
| `400 Invalid type` | `--type` value not in whitelist. Use one of: `spec`, `review`, `design`, `report`, `playground`. |
|
|
106
122
|
|
|
107
123
|
## Requirements
|
|
108
124
|
|
package/assets/SKILL.md
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mdpush
|
|
3
|
-
description: Push markdown files to Preview Reader server. Use when user wants to upload .md/.txt files to their docs portal.
|
|
3
|
+
description: Push markdown, text, and HTML artifact files to Preview Reader / sielay server. Use when user wants to upload .md/.txt files to their docs portal OR when user wants to host/share a self-contained HTML artifact (spec, review, design, report, playground).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# mdpush — Push Markdown to Preview Reader
|
|
6
|
+
# mdpush — Push Markdown and HTML Artifacts to Preview Reader
|
|
7
7
|
|
|
8
|
-
Push `.md
|
|
8
|
+
Push `.md`, `.txt`, and `.html` files from the current project to a Preview Reader server. HTML files are hosted at `/v/:sid` as sandboxed artifacts with an optional type tag.
|
|
9
9
|
|
|
10
10
|
## Usage
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
/mdpush <files...>
|
|
14
|
-
/mdpush -p <project-slug> <files...>
|
|
15
|
-
/mdpush -f <folder> <files...>
|
|
13
|
+
/mdpush <files...> # Push to default project
|
|
14
|
+
/mdpush -p <project-slug> <files...> # Push to specific project
|
|
15
|
+
/mdpush -f <folder> <files...> # Push to subfolder within project (md/txt only)
|
|
16
|
+
/mdpush design.html --type design # Upload HTML artifact with type tag
|
|
17
|
+
/mdpush spec.html report.md --type spec # Mix: HTML gets type, .md does not
|
|
16
18
|
```
|
|
17
19
|
|
|
20
|
+
### `--type` flag (HTML only)
|
|
21
|
+
|
|
22
|
+
Valid values: `spec | review | design | report | playground`
|
|
23
|
+
|
|
24
|
+
- Only meaningful for `.html` files — silently ignored for `.md`/`.txt`
|
|
25
|
+
- Rejected client-side before any network call if value is outside whitelist (exit 2)
|
|
26
|
+
- Output includes badge when type provided: `OK: design.html [design] → https://docs.sielay.cloud/v/abc123`
|
|
27
|
+
|
|
18
28
|
## Setup
|
|
19
29
|
|
|
20
30
|
Requires `~/.mdpush.json` config file:
|
|
@@ -39,6 +49,92 @@ Run the push script:
|
|
|
39
49
|
bash ~/.claude/skills/mdpush/scripts/push.sh [flags] <files...>
|
|
40
50
|
```
|
|
41
51
|
|
|
42
|
-
Flags: `-p <project>` (override project), `-f <folder>` (subfolder
|
|
52
|
+
Flags: `-p <project>` (override project), `-f <folder>` (subfolder, md/txt only), `--type <value>` (HTML artifact taxonomy).
|
|
43
53
|
|
|
44
54
|
Report the script output to the user. If config is missing, show the setup instructions above.
|
|
55
|
+
|
|
56
|
+
## HTML Quality Recommendations
|
|
57
|
+
|
|
58
|
+
Before generating an HTML artifact for upload, activate these skills to produce polished, non-generic output:
|
|
59
|
+
|
|
60
|
+
| Goal | Skill | When to use |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Dashboard, monitoring UI, data-rich layouts | `frontend-design` | Cards, sparklines, status pills, responsive grid |
|
|
63
|
+
| Landing page, admin panel, interactive UI | `ui-ux-pro-max` | 50+ styles, 21 palettes, 50 font pairings |
|
|
64
|
+
| Brand-consistent tokens, style guide, design system | `design` | Ensure visual consistency across artifacts |
|
|
65
|
+
|
|
66
|
+
**Recommended workflow:**
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
# Polished dashboard artifact
|
|
70
|
+
/frontend-design "Monitoring dashboard with cards, sparklines, status pills"
|
|
71
|
+
# → output: design.html
|
|
72
|
+
|
|
73
|
+
/mdpush design.html --type design
|
|
74
|
+
# → OK: design.html [design] → https://docs.sielay.cloud/v/abc123
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
# Interactive playground (sliders, live preview, copy-to-clipboard)
|
|
79
|
+
/ui-ux-pro-max "Easing curve tuner with sliders + animation preview + copy CSS output"
|
|
80
|
+
# → output: playground.html
|
|
81
|
+
|
|
82
|
+
/mdpush playground.html --type playground
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Context source: [HTML Effectiveness — Thariq](https://thariqs.github.io/html-effectiveness/) — taxonomy and patterns that inform the `--type` whitelist and quality bar for artifacts.
|
|
86
|
+
|
|
87
|
+
## Sandbox Limitations
|
|
88
|
+
|
|
89
|
+
HTML artifacts run inside an `<iframe>` with a restrictive sandbox (`sandbox="allow-scripts allow-forms allow-modals"` — **no** `allow-same-origin`). This gives the document a `null` origin. The following APIs are affected:
|
|
90
|
+
|
|
91
|
+
| API | Behavior inside sandbox | Workaround |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `localStorage` / `sessionStorage` | Throws `SecurityError` | Use in-memory state (plain JS variables/objects) |
|
|
94
|
+
| `document.cookie` | Returns empty string, writes silently ignored | Use in-memory state |
|
|
95
|
+
| `fetch()` cross-origin | Blocked by CSP — request never leaves browser | Inline all data; no runtime API calls |
|
|
96
|
+
| `fetch()` same-origin | Also blocked (null origin has no same-origin) | Inline all data |
|
|
97
|
+
| Clipboard write (`navigator.clipboard.writeText`) | Works with user gesture | Fine as-is |
|
|
98
|
+
| Form submit | Works (sandbox allows forms) | Fine as-is |
|
|
99
|
+
| DOM events, timers, animations | All work normally | Fine as-is |
|
|
100
|
+
| ES modules (`type="module"`) | Work — null origin does not block modules | Fine as-is |
|
|
101
|
+
|
|
102
|
+
**Artifact-author contract:**
|
|
103
|
+
- Self-contained HTML — all CSS and JS must be inlined or in `<style>`/`<script>` tags
|
|
104
|
+
- No external asset loads (CDN fonts, external images) — inline or omit
|
|
105
|
+
- File size < 15MB enforced server-side (HTTP 413 returned if exceeded)
|
|
106
|
+
- No cookies, no localStorage — design state to live only in memory for the session
|
|
107
|
+
|
|
108
|
+
## Future: `--inline-all` mode (not yet implemented)
|
|
109
|
+
|
|
110
|
+
Strict self-containment mode for compliance, airgap, or long-term archive scenarios. Pre-processes the HTML client-side **before upload**: compiles detected Tailwind utility classes into a static `<style>` block via Tailwind CLI, fetches Google Fonts / Fontshare CSS plus each `.woff2` and base64-inlines them as `data:font/woff2;base64,...`, and optionally inlines whitelisted CDN scripts. The resulting artifact has zero external network dependencies — survives any future CDN outage, CSP tightening, or sandbox change.
|
|
111
|
+
|
|
112
|
+
### Trade-offs
|
|
113
|
+
|
|
114
|
+
| Dimension | Current (CSP whitelist, shipped) | `--inline-all` (proposed) |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Author overhead | 0s — author writes normal HTML with CDN refs | 3–7s warm preprocess; 30–60s cold (Tailwind CLI install) |
|
|
117
|
+
| Artifact size | 30KB typical | 400–700KB typical (10–30x growth) |
|
|
118
|
+
| CDN dependency | Depends on 9-host whitelist staying available | Zero — fully self-contained |
|
|
119
|
+
| Server bandwidth | Low — artifacts stay small, browser fetches CDN | Higher — every artifact ships fonts/CSS in payload |
|
|
120
|
+
| Implementation effort | 0 (done) | ~1.5d (Tailwind detection + binary inlining + size-cap guard) |
|
|
121
|
+
|
|
122
|
+
### When to opt in
|
|
123
|
+
|
|
124
|
+
- **Compliance audits** — auditor needs to inspect the exact bytes that render
|
|
125
|
+
- **Airgap deployments** — environment has no internet egress, CDN unreachable
|
|
126
|
+
- **Long-term archive** — artifact must render identically in 5+ years regardless of CDN survival
|
|
127
|
+
- **Untrusted preview environments** — avoid even the metadata leak of CDN requests
|
|
128
|
+
|
|
129
|
+
### Activation surface (when implemented)
|
|
130
|
+
|
|
131
|
+
Two viable shapes — decide at implementation time:
|
|
132
|
+
|
|
133
|
+
1. **Strict opt-in flag**: `mdpush artifact.html --inline-all` — author explicit, default stays fast
|
|
134
|
+
2. **Smart auto-detect**: if HTML contains external `<link>`/`<script>` refs, auto-inline; opt out via `--no-inline`. Pro: no compliance footgun. Con: opaque size growth.
|
|
135
|
+
|
|
136
|
+
### Why not default now
|
|
137
|
+
|
|
138
|
+
Phase 1 CSP whitelist covers ~95% of artifacts that current Claude/agent workflows generate, with 0s author overhead. The 5–30x size penalty of full inlining is wasted bandwidth for the typical case. Document as roadmap; ship only when compliance or airgap demand surfaces.
|
|
139
|
+
|
|
140
|
+
**Reference**: `plans/260512-1704-html-fidelity-render-as-original/plan.md` — Option D row in the trade-off table that informed this deferral decision.
|
package/assets/scripts/push.sh
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# mdpush — push .md/.txt files to Preview Reader via API
|
|
2
|
+
# mdpush — push .md/.txt/.html files to Preview Reader via API
|
|
3
3
|
# Reads config from ~/.mdpush.json (server, token, defaultProject)
|
|
4
|
+
# Returns direct file links for each uploaded file
|
|
5
|
+
# HTML files upload to /v/:sid endpoint; .md/.txt to /p/:project/f/:sid
|
|
4
6
|
set -euo pipefail
|
|
5
7
|
|
|
6
8
|
CONFIG="$HOME/.mdpush.json"
|
|
@@ -33,13 +35,31 @@ fi
|
|
|
33
35
|
# Parse CLI args
|
|
34
36
|
PROJECT="${DEFAULT_PROJECT:-}"
|
|
35
37
|
FOLDER=""
|
|
38
|
+
TYPE=""
|
|
36
39
|
FILES=()
|
|
37
40
|
|
|
38
41
|
while [[ $# -gt 0 ]]; do
|
|
39
42
|
case "$1" in
|
|
40
43
|
-p) PROJECT="$2"; shift 2 ;;
|
|
41
44
|
-f) FOLDER="$2"; shift 2 ;;
|
|
42
|
-
|
|
45
|
+
--type)
|
|
46
|
+
if [[ $# -lt 2 ]]; then
|
|
47
|
+
echo "ERROR: --type requires a value" >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
TYPE="$2"
|
|
51
|
+
# Strict client-side whitelist — reject before any network call
|
|
52
|
+
case "$TYPE" in
|
|
53
|
+
spec|review|design|report|playground) ;;
|
|
54
|
+
*)
|
|
55
|
+
echo "ERROR: --type must be one of: spec|review|design|report|playground" >&2
|
|
56
|
+
echo " Got: $TYPE" >&2
|
|
57
|
+
exit 2
|
|
58
|
+
;;
|
|
59
|
+
esac
|
|
60
|
+
shift 2
|
|
61
|
+
;;
|
|
62
|
+
*) FILES+=("$1"); shift ;;
|
|
43
63
|
esac
|
|
44
64
|
done
|
|
45
65
|
|
|
@@ -50,22 +70,110 @@ fi
|
|
|
50
70
|
|
|
51
71
|
if [ ${#FILES[@]} -eq 0 ]; then
|
|
52
72
|
echo "ERROR: No files specified"
|
|
53
|
-
echo "Usage: push.sh [-p project] [-f folder] <files...>"
|
|
73
|
+
echo "Usage: push.sh [-p project] [-f folder] [--type spec|review|design|report|playground] <files...>"
|
|
54
74
|
exit 1
|
|
55
75
|
fi
|
|
56
76
|
|
|
57
|
-
# Build query
|
|
58
|
-
|
|
77
|
+
# Build folder query param (shared across non-HTML uploads)
|
|
78
|
+
FOLDER_QUERY=""
|
|
59
79
|
if [ -n "$FOLDER" ]; then
|
|
60
80
|
ENCODED_FOLDER=$(python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1]))" "$FOLDER")
|
|
61
|
-
|
|
81
|
+
FOLDER_QUERY="?folder=$ENCODED_FOLDER"
|
|
62
82
|
fi
|
|
63
83
|
|
|
84
|
+
# Extract server error message from JSON response body (best-effort)
|
|
85
|
+
extract_server_message() {
|
|
86
|
+
local body="$1"
|
|
87
|
+
local msg=""
|
|
88
|
+
if command -v jq &>/dev/null; then
|
|
89
|
+
msg=$(echo "$body" | jq -r '.message // .error // empty' 2>/dev/null || true)
|
|
90
|
+
else
|
|
91
|
+
msg=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('message',d.get('error','')))" "$body" 2>/dev/null || true)
|
|
92
|
+
fi
|
|
93
|
+
echo "$msg"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Millisecond-precision timestamp — bash 5+ date supports %3N;
|
|
97
|
+
# macOS with BSD date does NOT support %3N: fall back to python3.
|
|
98
|
+
_now_ms() {
|
|
99
|
+
local ms
|
|
100
|
+
ms=$(date +%s%3N 2>/dev/null)
|
|
101
|
+
# Detect failed %3N expansion: BSD date returns literal "%3N" suffix
|
|
102
|
+
if [[ "$ms" == *%3N* ]]; then
|
|
103
|
+
ms=$(python3 -c "import time; print(int(time.time()*1000))")
|
|
104
|
+
fi
|
|
105
|
+
echo "$ms"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# JSONL metrics log — created chmod 600 on first write
|
|
109
|
+
METRICS_FILE="$HOME/.mdpush-metrics.jsonl"
|
|
110
|
+
|
|
111
|
+
# Append one validated JSONL line to metrics file
|
|
112
|
+
# Args: ts file ext bytes ms_total ms_network ms_local sid url type status
|
|
113
|
+
_append_metric() {
|
|
114
|
+
local ts="$1" file="$2" ext="$3" bytes="$4"
|
|
115
|
+
local ms_total="$5" ms_network="$6" ms_local="$7"
|
|
116
|
+
local sid="$8" url="$9" type="${10}" status="${11}"
|
|
117
|
+
|
|
118
|
+
# Escape double-quotes in strings (basic safety; filenames rarely contain them)
|
|
119
|
+
file="${file//\"/\\\"}"
|
|
120
|
+
url="${url//\"/\\\"}"
|
|
121
|
+
|
|
122
|
+
# Build JSON with null for empty optional fields
|
|
123
|
+
local type_json="null"
|
|
124
|
+
[[ -n "$type" ]] && type_json="\"$type\""
|
|
125
|
+
|
|
126
|
+
local url_json="null"
|
|
127
|
+
[[ -n "$url" ]] && url_json="\"$url\""
|
|
128
|
+
|
|
129
|
+
local sid_json="null"
|
|
130
|
+
[[ -n "$sid" ]] && sid_json="\"$sid\""
|
|
131
|
+
|
|
132
|
+
local line
|
|
133
|
+
line=$(printf '{"ts":"%s","file":"%s","ext":"%s","bytes":%s,"ms_total":%s,"ms_network":%s,"ms_local":%s,"sid":%s,"url":%s,"type":%s,"status":"%s"}' \
|
|
134
|
+
"$ts" "$file" "$ext" "$bytes" \
|
|
135
|
+
"$ms_total" "$ms_network" "$ms_local" \
|
|
136
|
+
"$sid_json" "$url_json" "$type_json" "$status")
|
|
137
|
+
|
|
138
|
+
# Validate JSON before writing (jq -c '.' re-emits compacted valid JSON or fails)
|
|
139
|
+
if command -v jq &>/dev/null; then
|
|
140
|
+
local validated
|
|
141
|
+
validated=$(jq -c '.' <<< "$line" 2>/dev/null) || {
|
|
142
|
+
echo "METRICS: invalid JSON line — skipping write (line: $line)" >&2
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
line="$validated"
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# Create file with restricted permissions on first write
|
|
149
|
+
if [ ! -f "$METRICS_FILE" ]; then
|
|
150
|
+
install -m 600 /dev/null "$METRICS_FILE"
|
|
151
|
+
fi
|
|
152
|
+
printf '%s\n' "$line" >> "$METRICS_FILE"
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Optional Discord webhook notification (fire-and-forget)
|
|
156
|
+
_discord_notify() {
|
|
157
|
+
local file="$1" ms_total="$2" url="$3"
|
|
158
|
+
if [[ -n "${MDPUSH_METRICS_DISCORD_WEBHOOK:-}" ]]; then
|
|
159
|
+
local payload
|
|
160
|
+
payload=$(printf '{"content":"📤 %s → %dms → %s"}' "$file" "$ms_total" "$url")
|
|
161
|
+
curl -s -X POST "$MDPUSH_METRICS_DISCORD_WEBHOOK" \
|
|
162
|
+
-H "Content-Type: application/json" \
|
|
163
|
+
-A "mdpush-metrics/1.0 (preview-reader-pipeline)" \
|
|
164
|
+
-d "$payload" \
|
|
165
|
+
--max-time 3 &
|
|
166
|
+
fi
|
|
167
|
+
}
|
|
168
|
+
|
|
64
169
|
# Upload each file
|
|
65
170
|
SUCCESS=0
|
|
66
171
|
FAILED=0
|
|
67
172
|
|
|
68
173
|
for FILE in "${FILES[@]}"; do
|
|
174
|
+
# Per-file t_start reset so each JSONL row reflects one file's pipeline, not the batch
|
|
175
|
+
t_start=$(_now_ms)
|
|
176
|
+
|
|
69
177
|
# Check file exists
|
|
70
178
|
if [ ! -f "$FILE" ]; then
|
|
71
179
|
echo "SKIP: $FILE (not found)"
|
|
@@ -73,26 +181,223 @@ for FILE in "${FILES[@]}"; do
|
|
|
73
181
|
fi
|
|
74
182
|
|
|
75
183
|
BASENAME=$(basename "$FILE")
|
|
184
|
+
EXT="${BASENAME##*.}"
|
|
76
185
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
186
|
+
# Extension whitelist: md, txt, html
|
|
187
|
+
case "$EXT" in
|
|
188
|
+
md|txt|html) ;;
|
|
189
|
+
*)
|
|
190
|
+
echo "SKIP: $BASENAME (unsupported extension — use .md, .txt, or .html)"
|
|
191
|
+
continue
|
|
192
|
+
;;
|
|
193
|
+
esac
|
|
194
|
+
|
|
195
|
+
# Portable file size: wc -c avoids GNU/BSD stat flag differences
|
|
196
|
+
BYTES=$(wc -c < "$FILE" | tr -d ' ')
|
|
197
|
+
|
|
198
|
+
# --- HTML path: upload to /v/:sid endpoint ---
|
|
199
|
+
if [ "$EXT" = "html" ]; then
|
|
200
|
+
# Build query: always ?contentType=html; add &type=<value> if --type was given
|
|
201
|
+
HTML_QUERY="?contentType=html"
|
|
202
|
+
if [ -n "$TYPE" ]; then
|
|
203
|
+
HTML_QUERY="${HTML_QUERY}&type=${TYPE}"
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
t_pre_curl=$(_now_ms)
|
|
207
|
+
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
208
|
+
--max-time 60 \
|
|
209
|
+
-X POST \
|
|
210
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
211
|
+
-H "X-Requested-With: XMLHttpRequest" \
|
|
212
|
+
-F "files=@$FILE" \
|
|
213
|
+
"${SERVER}/api/projects/${PROJECT}/upload${HTML_QUERY}") || {
|
|
214
|
+
t_post_curl=$(_now_ms)
|
|
215
|
+
t_end=$(_now_ms)
|
|
216
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
217
|
+
ms_network=$((t_post_curl - t_pre_curl))
|
|
218
|
+
ms_local=$((t_pre_curl - t_start))
|
|
219
|
+
ms_total=$((t_end - t_start))
|
|
220
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
221
|
+
"$ms_total" "$ms_network" "$ms_local" "" "" "${TYPE}" "error_network"
|
|
222
|
+
echo "FAIL: $BASENAME — network error (curl failed)"
|
|
223
|
+
FAILED=$((FAILED + 1))
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
t_post_curl=$(_now_ms)
|
|
227
|
+
|
|
228
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
229
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
230
|
+
|
|
231
|
+
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
232
|
+
# Server response includes absolute 'url' field — read it directly
|
|
233
|
+
URL=""
|
|
234
|
+
SID=""
|
|
235
|
+
if command -v jq &>/dev/null; then
|
|
236
|
+
URL=$(echo "$BODY" | jq -r '.url // .uploaded[0].url // empty' 2>/dev/null || true)
|
|
237
|
+
SID=$(echo "$BODY" | jq -r '.uploaded[0].sid // .sid // empty' 2>/dev/null || true)
|
|
238
|
+
else
|
|
239
|
+
URL=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('url',d.get('uploaded',[{}])[0].get('url','')))" "$BODY" 2>/dev/null || true)
|
|
240
|
+
SID=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('sid',d.get('uploaded',[{}])[0].get('sid','')))" "$BODY" 2>/dev/null || true)
|
|
241
|
+
fi
|
|
82
242
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-F "files=@$FILE" \
|
|
88
|
-
"${SERVER}/api/projects/${PROJECT}/upload${QUERY}")
|
|
243
|
+
BADGE=""
|
|
244
|
+
if [ -n "$TYPE" ]; then
|
|
245
|
+
BADGE=" [$TYPE]"
|
|
246
|
+
fi
|
|
89
247
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
248
|
+
t_end=$(_now_ms)
|
|
249
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
250
|
+
ms_network=$((t_post_curl - t_pre_curl))
|
|
251
|
+
ms_local=$((t_pre_curl - t_start))
|
|
252
|
+
ms_total=$((t_end - t_start))
|
|
253
|
+
|
|
254
|
+
if [ -n "$URL" ]; then
|
|
255
|
+
echo "OK: $BASENAME${BADGE} → $URL"
|
|
256
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
257
|
+
"$ms_total" "$ms_network" "$ms_local" "$SID" "$URL" "${TYPE}" "ok"
|
|
258
|
+
_discord_notify "$BASENAME" "$ms_total" "$URL"
|
|
259
|
+
else
|
|
260
|
+
echo "OK: $BASENAME${BADGE} (HTTP $HTTP_CODE — no URL in response)"
|
|
261
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
262
|
+
"$ms_total" "$ms_network" "$ms_local" "$SID" "" "${TYPE}" "ok"
|
|
263
|
+
fi
|
|
264
|
+
SUCCESS=$((SUCCESS + 1))
|
|
265
|
+
else
|
|
266
|
+
t_end=$(_now_ms)
|
|
267
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
268
|
+
ms_network=$((t_post_curl - t_pre_curl))
|
|
269
|
+
ms_local=$((t_pre_curl - t_start))
|
|
270
|
+
ms_total=$((t_end - t_start))
|
|
271
|
+
|
|
272
|
+
SERVER_MSG=$(extract_server_message "$BODY")
|
|
273
|
+
METRIC_STATUS="error_other"
|
|
274
|
+
case "$HTTP_CODE" in
|
|
275
|
+
413)
|
|
276
|
+
echo "FAIL: $BASENAME — file too large (server limit is 15MB)"
|
|
277
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
278
|
+
METRIC_STATUS="error_413"
|
|
279
|
+
;;
|
|
280
|
+
415)
|
|
281
|
+
echo "FAIL: $BASENAME — unsupported media type (server rejected MIME)"
|
|
282
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
283
|
+
METRIC_STATUS="error_415"
|
|
284
|
+
;;
|
|
285
|
+
400)
|
|
286
|
+
echo "FAIL: $BASENAME — bad request (check --type value or file content)"
|
|
287
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
288
|
+
METRIC_STATUS="error_400"
|
|
289
|
+
;;
|
|
290
|
+
401|403)
|
|
291
|
+
echo "FAIL: $BASENAME — authentication error (check token in $CONFIG)"
|
|
292
|
+
;;
|
|
293
|
+
*)
|
|
294
|
+
echo "FAIL: $BASENAME (HTTP $HTTP_CODE)"
|
|
295
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
296
|
+
;;
|
|
297
|
+
esac
|
|
298
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
299
|
+
"$ms_total" "$ms_network" "$ms_local" "" "" "${TYPE}" "$METRIC_STATUS"
|
|
300
|
+
FAILED=$((FAILED + 1))
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# --- Markdown / text path: original behavior ---
|
|
93
304
|
else
|
|
94
|
-
|
|
95
|
-
(
|
|
305
|
+
t_pre_curl=$(_now_ms)
|
|
306
|
+
RESPONSE=$(curl -s -w "\n%{http_code}" \
|
|
307
|
+
--max-time 60 \
|
|
308
|
+
-X POST \
|
|
309
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
310
|
+
-H "X-Requested-With: XMLHttpRequest" \
|
|
311
|
+
-F "files=@$FILE" \
|
|
312
|
+
"${SERVER}/api/projects/${PROJECT}/upload${FOLDER_QUERY}") || {
|
|
313
|
+
t_post_curl=$(_now_ms)
|
|
314
|
+
t_end=$(_now_ms)
|
|
315
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
316
|
+
ms_network=$((t_post_curl - t_pre_curl))
|
|
317
|
+
ms_local=$((t_pre_curl - t_start))
|
|
318
|
+
ms_total=$((t_end - t_start))
|
|
319
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
320
|
+
"$ms_total" "$ms_network" "$ms_local" "" "" "" "error_network"
|
|
321
|
+
echo "FAIL: $BASENAME — network error (curl failed)"
|
|
322
|
+
FAILED=$((FAILED + 1))
|
|
323
|
+
continue
|
|
324
|
+
}
|
|
325
|
+
t_post_curl=$(_now_ms)
|
|
326
|
+
|
|
327
|
+
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
|
|
328
|
+
BODY=$(echo "$RESPONSE" | sed '$d')
|
|
329
|
+
|
|
330
|
+
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
331
|
+
# Try to read absolute url from response first; fall back to SID-based link
|
|
332
|
+
URL=""
|
|
333
|
+
SID=""
|
|
334
|
+
if command -v jq &>/dev/null; then
|
|
335
|
+
URL=$(echo "$BODY" | jq -r '.url // .uploaded[0].url // empty' 2>/dev/null || true)
|
|
336
|
+
SID=$(echo "$BODY" | jq -r '.uploaded[0].sid // empty' 2>/dev/null || true)
|
|
337
|
+
else
|
|
338
|
+
URL=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('url',d.get('uploaded',[{}])[0].get('url','')))" "$BODY" 2>/dev/null || true)
|
|
339
|
+
SID=$(python3 -c "import sys,json; d=json.loads(sys.argv[1]); print(d.get('uploaded',[])[0].get('sid',''))" "$BODY" 2>/dev/null || true)
|
|
340
|
+
fi
|
|
341
|
+
|
|
342
|
+
t_end=$(_now_ms)
|
|
343
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
344
|
+
ms_network=$((t_post_curl - t_pre_curl))
|
|
345
|
+
ms_local=$((t_pre_curl - t_start))
|
|
346
|
+
ms_total=$((t_end - t_start))
|
|
347
|
+
|
|
348
|
+
if [ -n "$URL" ]; then
|
|
349
|
+
echo "OK: $BASENAME → $URL"
|
|
350
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
351
|
+
"$ms_total" "$ms_network" "$ms_local" "$SID" "$URL" "" "ok"
|
|
352
|
+
_discord_notify "$BASENAME" "$ms_total" "$URL"
|
|
353
|
+
elif [ -n "$SID" ]; then
|
|
354
|
+
echo "OK: $BASENAME → ${SERVER}/p/${PROJECT}/f/${SID}"
|
|
355
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
356
|
+
"$ms_total" "$ms_network" "$ms_local" "$SID" "${SERVER}/p/${PROJECT}/f/${SID}" "" "ok"
|
|
357
|
+
_discord_notify "$BASENAME" "$ms_total" "${SERVER}/p/${PROJECT}/f/${SID}"
|
|
358
|
+
else
|
|
359
|
+
echo "OK: $BASENAME (HTTP $HTTP_CODE)"
|
|
360
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
361
|
+
"$ms_total" "$ms_network" "$ms_local" "" "" "" "ok"
|
|
362
|
+
fi
|
|
363
|
+
SUCCESS=$((SUCCESS + 1))
|
|
364
|
+
else
|
|
365
|
+
t_end=$(_now_ms)
|
|
366
|
+
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
367
|
+
ms_network=$((t_post_curl - t_pre_curl))
|
|
368
|
+
ms_local=$((t_pre_curl - t_start))
|
|
369
|
+
ms_total=$((t_end - t_start))
|
|
370
|
+
|
|
371
|
+
SERVER_MSG=$(extract_server_message "$BODY")
|
|
372
|
+
METRIC_STATUS="error_other"
|
|
373
|
+
case "$HTTP_CODE" in
|
|
374
|
+
413)
|
|
375
|
+
echo "FAIL: $BASENAME — file too large (server limit is 15MB)"
|
|
376
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
377
|
+
METRIC_STATUS="error_413"
|
|
378
|
+
;;
|
|
379
|
+
415)
|
|
380
|
+
echo "FAIL: $BASENAME — unsupported media type"
|
|
381
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
382
|
+
METRIC_STATUS="error_415"
|
|
383
|
+
;;
|
|
384
|
+
400)
|
|
385
|
+
echo "FAIL: $BASENAME — bad request"
|
|
386
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
387
|
+
METRIC_STATUS="error_400"
|
|
388
|
+
;;
|
|
389
|
+
401|403)
|
|
390
|
+
echo "FAIL: $BASENAME — authentication error (check token in $CONFIG)"
|
|
391
|
+
;;
|
|
392
|
+
*)
|
|
393
|
+
echo "FAIL: $BASENAME (HTTP $HTTP_CODE)"
|
|
394
|
+
[ -n "$SERVER_MSG" ] && echo " Server: $SERVER_MSG"
|
|
395
|
+
;;
|
|
396
|
+
esac
|
|
397
|
+
_append_metric "$TS" "$BASENAME" "$EXT" "$BYTES" \
|
|
398
|
+
"$ms_total" "$ms_network" "$ms_local" "" "" "" "$METRIC_STATUS"
|
|
399
|
+
FAILED=$((FAILED + 1))
|
|
400
|
+
fi
|
|
96
401
|
fi
|
|
97
402
|
done
|
|
98
403
|
|
package/package.json
CHANGED
|
@@ -1,38 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mdpush",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Push markdown files to Preview Reader from your terminal",
|
|
3
|
+
"version": "1.1.0",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
6
|
+
"node": ">=20.0.0"
|
|
8
7
|
},
|
|
9
8
|
"bin": {
|
|
10
9
|
"mdpush": "./bin/mdpush.js"
|
|
11
10
|
},
|
|
12
|
-
"files": [
|
|
13
|
-
"bin/",
|
|
14
|
-
"src/",
|
|
15
|
-
"assets/",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"keywords": [
|
|
19
|
-
"markdown",
|
|
20
|
-
"preview",
|
|
21
|
-
"reader",
|
|
22
|
-
"docs",
|
|
23
|
-
"push",
|
|
24
|
-
"cli",
|
|
25
|
-
"upload"
|
|
26
|
-
],
|
|
27
|
-
"repository": {
|
|
28
|
-
"type": "git",
|
|
29
|
-
"url": "https://github.com/sielay/preview-reader",
|
|
30
|
-
"directory": "packages/cli"
|
|
31
|
-
},
|
|
32
|
-
"homepage": "https://github.com/sielay/preview-reader/tree/master/packages/cli",
|
|
33
|
-
"bugs": "https://github.com/sielay/preview-reader/issues",
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"author": "sielay",
|
|
36
11
|
"dependencies": {
|
|
37
12
|
"commander": "^12.0.0",
|
|
38
13
|
"chalk": "^5.3.0",
|
|
@@ -5,7 +5,7 @@ import { readConfig } from '../lib/config-store.js'
|
|
|
5
5
|
export function configCommand() {
|
|
6
6
|
const config = readConfig()
|
|
7
7
|
if (!config.server) {
|
|
8
|
-
console.log(chalk.yellow('Not
|
|
8
|
+
console.log(chalk.yellow('Not logged in. Run: mdpush login --server <url>'))
|
|
9
9
|
return
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -13,7 +13,4 @@ export function configCommand() {
|
|
|
13
13
|
console.log(` Server: ${config.server}`)
|
|
14
14
|
console.log(` Username: ${config.username}`)
|
|
15
15
|
console.log(` Token: ${config.token ? '[stored]' : 'none'}`)
|
|
16
|
-
if (config.defaultProject) {
|
|
17
|
-
console.log(` Project: ${config.defaultProject}`)
|
|
18
|
-
}
|
|
19
16
|
}
|
|
@@ -5,18 +5,21 @@ import ora from 'ora'
|
|
|
5
5
|
import { apiRequest } from '../lib/api-request.js'
|
|
6
6
|
import { readConfig } from '../lib/config-store.js'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const HTML_TYPE_WHITELIST = new Set(['spec', 'review', 'design', 'report', 'playground'])
|
|
9
|
+
|
|
10
|
+
const isHtml = (f) => /\.html$/i.test(f)
|
|
11
|
+
const isMdTxt = (f) => /\.(md|txt)$/i.test(f)
|
|
12
|
+
|
|
13
|
+
/** Push markdown, text, and HTML artifact files to a project */
|
|
9
14
|
export async function pushCommand(files, options) {
|
|
10
|
-
const
|
|
11
|
-
const project = options.project || config.defaultProject
|
|
12
|
-
const { folder } = options
|
|
15
|
+
const { project, folder, type } = options
|
|
13
16
|
|
|
14
|
-
if (!
|
|
15
|
-
console.log(chalk.red(
|
|
16
|
-
process.exit(
|
|
17
|
+
if (type && !HTML_TYPE_WHITELIST.has(type)) {
|
|
18
|
+
console.log(chalk.red(`Invalid --type "${type}". Allowed: ${[...HTML_TYPE_WHITELIST].join('|')}`))
|
|
19
|
+
process.exit(2)
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
// Resolve file list — expand directories
|
|
22
|
+
// Resolve file list — expand directories (md/txt only inside dirs)
|
|
20
23
|
const resolvedFiles = []
|
|
21
24
|
for (const f of files) {
|
|
22
25
|
const stat = fs.statSync(f, { throwIfNoEntry: false })
|
|
@@ -26,43 +29,85 @@ export async function pushCommand(files, options) {
|
|
|
26
29
|
}
|
|
27
30
|
if (stat.isDirectory()) {
|
|
28
31
|
const dirFiles = fs.readdirSync(f, { recursive: true })
|
|
29
|
-
.filter(name =>
|
|
32
|
+
.filter(name => isMdTxt(name))
|
|
30
33
|
.map(name => path.join(f, name))
|
|
31
34
|
resolvedFiles.push(...dirFiles)
|
|
32
|
-
} else if (
|
|
35
|
+
} else if (isMdTxt(f) || isHtml(f)) {
|
|
33
36
|
resolvedFiles.push(f)
|
|
34
37
|
} else {
|
|
35
|
-
console.log(chalk.yellow(`Skipping: ${f} (not .md or .
|
|
38
|
+
console.log(chalk.yellow(`Skipping: ${f} (not .md, .txt, or .html)`))
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
if (resolvedFiles.length === 0) {
|
|
40
|
-
console.log(chalk.red('No .md or .
|
|
43
|
+
console.log(chalk.red('No .md, .txt, or .html files to upload.'))
|
|
41
44
|
process.exit(1)
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const htmlFiles = resolvedFiles.filter(isHtml)
|
|
48
|
+
const otherFiles = resolvedFiles.filter(f => !isHtml(f))
|
|
49
|
+
|
|
50
|
+
if (htmlFiles.length > 0 && folder) {
|
|
51
|
+
console.log(chalk.yellow(`Note: --folder is ignored for HTML uploads (artifacts are flat by design)`))
|
|
52
|
+
}
|
|
53
|
+
|
|
44
54
|
console.log(chalk.blue(`Uploading ${resolvedFiles.length} file(s) to ${project}${folder ? `/${folder}` : ''}...`))
|
|
45
55
|
|
|
56
|
+
const uploadedFiles = []
|
|
46
57
|
let success = 0
|
|
47
58
|
let failed = 0
|
|
48
59
|
|
|
60
|
+
// Per-file dispatch — mirrors push.sh behavior, simpler than batched FormData
|
|
49
61
|
for (const filePath of resolvedFiles) {
|
|
50
|
-
const
|
|
62
|
+
const basename = path.basename(filePath)
|
|
63
|
+
const spinner = ora(`Uploading ${basename}`).start()
|
|
51
64
|
try {
|
|
52
65
|
const formData = new FormData()
|
|
53
66
|
const content = fs.readFileSync(filePath)
|
|
54
|
-
formData.append('files', new Blob([content]),
|
|
67
|
+
formData.append('files', new Blob([content]), basename)
|
|
55
68
|
|
|
56
|
-
const
|
|
57
|
-
|
|
69
|
+
const html = isHtml(filePath)
|
|
70
|
+
const params = new URLSearchParams()
|
|
71
|
+
if (html) {
|
|
72
|
+
params.set('contentType', 'html')
|
|
73
|
+
if (type) params.set('type', type)
|
|
74
|
+
} else if (folder) {
|
|
75
|
+
params.set('folder', folder)
|
|
76
|
+
}
|
|
77
|
+
const query = params.toString() ? `?${params.toString()}` : ''
|
|
78
|
+
const url = `/api/projects/${project}/upload${query}`
|
|
58
79
|
|
|
59
|
-
|
|
80
|
+
const res = await apiRequest(url, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
|
83
|
+
body: formData
|
|
84
|
+
})
|
|
85
|
+
const data = await res.json()
|
|
86
|
+
|
|
87
|
+
spinner.succeed(chalk.green(basename))
|
|
60
88
|
success++
|
|
89
|
+
|
|
90
|
+
if (data.uploaded) uploadedFiles.push(...data.uploaded)
|
|
61
91
|
} catch (err) {
|
|
62
|
-
spinner.fail(chalk.red(`${
|
|
92
|
+
spinner.fail(chalk.red(`${basename}: ${err.message}`))
|
|
63
93
|
failed++
|
|
64
94
|
}
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
console.log(`\n${chalk.green(`${success} uploaded`)}${failed ? `, ${chalk.red(`${failed} failed`)}` : ''}`)
|
|
98
|
+
|
|
99
|
+
if (uploadedFiles.length > 0) {
|
|
100
|
+
const config = readConfig()
|
|
101
|
+
for (const f of uploadedFiles) {
|
|
102
|
+
if (!f.sid) continue
|
|
103
|
+
if (f.url) {
|
|
104
|
+
// HTML artifact: server emits viewer URL
|
|
105
|
+
const badge = f.type ? chalk.magenta(`[${f.type}] `) : ''
|
|
106
|
+
console.log(chalk.cyan(`🔗 ${badge}${f.url} ${f.name}`))
|
|
107
|
+
} else {
|
|
108
|
+
// md/txt: legacy /p/<slug>/f/<sid> path
|
|
109
|
+
console.log(chalk.cyan(`🔗 ${config.server}/p/${project}/f/${f.sid} ${f.name}`))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
68
113
|
}
|
package/src/index.js
CHANGED
|
@@ -3,16 +3,11 @@ import { loginCommand } from './commands/login-command.js'
|
|
|
3
3
|
import { pushCommand } from './commands/push-command.js'
|
|
4
4
|
import { configCommand } from './commands/config-command.js'
|
|
5
5
|
import { logoutCommand } from './commands/logout-command.js'
|
|
6
|
-
import { setupCommand } from './commands/setup-command.js'
|
|
7
6
|
|
|
8
7
|
program
|
|
9
8
|
.name('mdpush')
|
|
10
|
-
.description('Push markdown files to Preview Reader')
|
|
11
|
-
.version('1.
|
|
12
|
-
|
|
13
|
-
program.command('setup')
|
|
14
|
-
.description('Interactive setup — configure server, token, and default project')
|
|
15
|
-
.action(setupCommand)
|
|
9
|
+
.description('Push markdown, text, and HTML artifact files to Preview Reader')
|
|
10
|
+
.version('1.1.0')
|
|
16
11
|
|
|
17
12
|
program.command('login')
|
|
18
13
|
.description('Authenticate with server')
|
|
@@ -20,9 +15,10 @@ program.command('login')
|
|
|
20
15
|
.action(loginCommand)
|
|
21
16
|
|
|
22
17
|
program.command('push')
|
|
23
|
-
.description('Upload files to a project')
|
|
24
|
-
.
|
|
25
|
-
.option('-f, --folder <path>', 'Target folder within project')
|
|
18
|
+
.description('Upload .md/.txt/.html files to a project')
|
|
19
|
+
.requiredOption('-p, --project <slug>', 'Project slug')
|
|
20
|
+
.option('-f, --folder <path>', 'Target folder within project (md/txt only)')
|
|
21
|
+
.option('-t, --type <value>', 'HTML artifact type: spec|review|design|report|playground (HTML files only)')
|
|
26
22
|
.argument('<files...>', 'Files or directory to upload')
|
|
27
23
|
.action(pushCommand)
|
|
28
24
|
|