mdpush 1.0.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/LICENSE +21 -0
- package/README.md +114 -0
- package/assets/SKILL.md +44 -0
- package/assets/scripts/push.sh +102 -0
- package/bin/mdpush.js +2 -0
- package/package.json +42 -0
- package/src/commands/config-command.js +19 -0
- package/src/commands/login-command.js +58 -0
- package/src/commands/logout-command.js +8 -0
- package/src/commands/push-command.js +68 -0
- package/src/commands/setup-command.js +102 -0
- package/src/index.js +37 -0
- package/src/lib/api-request.js +25 -0
- package/src/lib/config-store.js +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sielay
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# mdpush
|
|
2
|
+
|
|
3
|
+
Push markdown files to [Preview Reader](https://github.com/sielay/preview-reader) from your terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g mdpush
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use directly with npx (no install needed):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx mdpush setup
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Run interactive setup (server URL, API token, default project)
|
|
21
|
+
mdpush setup
|
|
22
|
+
|
|
23
|
+
# 2. Push a file
|
|
24
|
+
mdpush push README.md
|
|
25
|
+
|
|
26
|
+
# 3. Push to a specific project
|
|
27
|
+
mdpush push -p my-project docs/*.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### `mdpush setup`
|
|
33
|
+
|
|
34
|
+
Interactive wizard to configure server, token, and default project. Validates your token against the server before saving. Optionally installs the Claude Code skill.
|
|
35
|
+
|
|
36
|
+
### `mdpush push [files...]`
|
|
37
|
+
|
|
38
|
+
Upload `.md` and `.txt` files to a project.
|
|
39
|
+
|
|
40
|
+
| Flag | Description |
|
|
41
|
+
|------|-------------|
|
|
42
|
+
| `-p, --project <slug>` | Target project (falls back to default from setup) |
|
|
43
|
+
| `-f, --folder <path>` | Subfolder within the project |
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
mdpush push README.md # Uses default project
|
|
47
|
+
mdpush push -p docs-site *.md # Specific project
|
|
48
|
+
mdpush push -p docs-site -f guides plan.md # Into subfolder
|
|
49
|
+
mdpush push docs/ # Upload entire directory
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `mdpush login`
|
|
53
|
+
|
|
54
|
+
Authenticate with username/password to generate an API token.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
mdpush login -s https://docs.example.com
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `mdpush config`
|
|
61
|
+
|
|
62
|
+
Show stored configuration (server, username, default project).
|
|
63
|
+
|
|
64
|
+
### `mdpush logout`
|
|
65
|
+
|
|
66
|
+
Clear stored credentials from `~/.mdpush.json`.
|
|
67
|
+
|
|
68
|
+
## Claude Code Integration
|
|
69
|
+
|
|
70
|
+
During `mdpush setup`, if Claude Code is detected (`~/.claude/skills/` exists), you'll be offered to install the mdpush skill automatically.
|
|
71
|
+
|
|
72
|
+
Once installed, use `/mdpush` in Claude Code to push files directly from your AI assistant.
|
|
73
|
+
|
|
74
|
+
Manual install:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
mkdir -p ~/.claude/skills/mdpush/scripts
|
|
78
|
+
cp node_modules/mdpush/assets/SKILL.md ~/.claude/skills/mdpush/SKILL.md
|
|
79
|
+
cp node_modules/mdpush/assets/scripts/push.sh ~/.claude/skills/mdpush/scripts/push.sh
|
|
80
|
+
chmod +x ~/.claude/skills/mdpush/scripts/push.sh
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
Config is stored at `~/.mdpush.json` with restricted permissions (0600):
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"server": "https://docs.example.com",
|
|
90
|
+
"token": "<api-token>",
|
|
91
|
+
"username": "you",
|
|
92
|
+
"defaultProject": "my-project"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Generate tokens from the web UI: **Sidebar > API Tokens > Generate Token**.
|
|
97
|
+
|
|
98
|
+
## Troubleshooting
|
|
99
|
+
|
|
100
|
+
| Error | Fix |
|
|
101
|
+
|-------|-----|
|
|
102
|
+
| `401 Unauthorized` | Token expired or revoked. Generate a new one from the web UI. |
|
|
103
|
+
| `403 Forbidden` | No write access to this project. Ask the owner to invite you. |
|
|
104
|
+
| `404 Not Found` | Wrong project slug. Check it in the web UI URL: `/p/<slug>` |
|
|
105
|
+
| `413 Too Large` | File exceeds 5MB limit. |
|
|
106
|
+
|
|
107
|
+
## Requirements
|
|
108
|
+
|
|
109
|
+
- Node.js >= 18
|
|
110
|
+
- A running [Preview Reader](https://github.com/sielay/preview-reader) server
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
package/assets/SKILL.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# mdpush — Push Markdown to Preview Reader
|
|
7
|
+
|
|
8
|
+
Push `.md` and `.txt` files from the current project to a Preview Reader server.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
```
|
|
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
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Requires `~/.mdpush.json` config file:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"server": "https://docs.sielay.cloud",
|
|
25
|
+
"token": "<api-token>",
|
|
26
|
+
"defaultProject": "<your-project-slug>"
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Generate a token from the Preview Reader web UI: **Sidebar > API Tokens > Generate Token**.
|
|
31
|
+
|
|
32
|
+
**Never commit `~/.mdpush.json` to version control.**
|
|
33
|
+
|
|
34
|
+
## Execution
|
|
35
|
+
|
|
36
|
+
Run the push script:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bash ~/.claude/skills/mdpush/scripts/push.sh [flags] <files...>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Flags: `-p <project>` (override project), `-f <folder>` (subfolder target).
|
|
43
|
+
|
|
44
|
+
Report the script output to the user. If config is missing, show the setup instructions above.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# mdpush — push .md/.txt files to Preview Reader via API
|
|
3
|
+
# Reads config from ~/.mdpush.json (server, token, defaultProject)
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
CONFIG="$HOME/.mdpush.json"
|
|
7
|
+
|
|
8
|
+
# Check config exists
|
|
9
|
+
if [ ! -f "$CONFIG" ]; then
|
|
10
|
+
echo "ERROR: Config not found at $CONFIG"
|
|
11
|
+
echo "Create it with:"
|
|
12
|
+
echo ' { "server": "https://your-server.com", "token": "<api-token>", "defaultProject": "slug" }'
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Parse config — try jq first, fallback to python3
|
|
17
|
+
if command -v jq &>/dev/null; then
|
|
18
|
+
SERVER=$(jq -r '.server' "$CONFIG")
|
|
19
|
+
TOKEN=$(jq -r '.token' "$CONFIG")
|
|
20
|
+
DEFAULT_PROJECT=$(jq -r '.defaultProject' "$CONFIG")
|
|
21
|
+
else
|
|
22
|
+
SERVER=$(python3 -c "import sys,json; print(json.load(open(sys.argv[1]))['server'])" "$CONFIG")
|
|
23
|
+
TOKEN=$(python3 -c "import sys,json; print(json.load(open(sys.argv[1]))['token'])" "$CONFIG")
|
|
24
|
+
DEFAULT_PROJECT=$(python3 -c "import sys,json; print(json.load(open(sys.argv[1]))['defaultProject'])" "$CONFIG")
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Validate config
|
|
28
|
+
if [ -z "$SERVER" ] || [ -z "$TOKEN" ]; then
|
|
29
|
+
echo "ERROR: server and token are required in $CONFIG"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Parse CLI args
|
|
34
|
+
PROJECT="${DEFAULT_PROJECT:-}"
|
|
35
|
+
FOLDER=""
|
|
36
|
+
FILES=()
|
|
37
|
+
|
|
38
|
+
while [[ $# -gt 0 ]]; do
|
|
39
|
+
case "$1" in
|
|
40
|
+
-p) PROJECT="$2"; shift 2 ;;
|
|
41
|
+
-f) FOLDER="$2"; shift 2 ;;
|
|
42
|
+
*) FILES+=("$1"); shift ;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [ -z "$PROJECT" ]; then
|
|
47
|
+
echo "ERROR: No project specified. Use -p <slug> or set defaultProject in $CONFIG"
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [ ${#FILES[@]} -eq 0 ]; then
|
|
52
|
+
echo "ERROR: No files specified"
|
|
53
|
+
echo "Usage: push.sh [-p project] [-f folder] <files...>"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Build query string for folder
|
|
58
|
+
QUERY=""
|
|
59
|
+
if [ -n "$FOLDER" ]; then
|
|
60
|
+
ENCODED_FOLDER=$(python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1]))" "$FOLDER")
|
|
61
|
+
QUERY="?folder=$ENCODED_FOLDER"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Upload each file
|
|
65
|
+
SUCCESS=0
|
|
66
|
+
FAILED=0
|
|
67
|
+
|
|
68
|
+
for FILE in "${FILES[@]}"; do
|
|
69
|
+
# Check file exists
|
|
70
|
+
if [ ! -f "$FILE" ]; then
|
|
71
|
+
echo "SKIP: $FILE (not found)"
|
|
72
|
+
continue
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
BASENAME=$(basename "$FILE")
|
|
76
|
+
|
|
77
|
+
# Filter to .md/.txt only
|
|
78
|
+
if [[ ! "$BASENAME" =~ \.(md|txt)$ ]]; then
|
|
79
|
+
echo "SKIP: $BASENAME (not .md or .txt)"
|
|
80
|
+
continue
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
84
|
+
--max-time 60 \
|
|
85
|
+
-X POST \
|
|
86
|
+
-H "Authorization: Bearer $TOKEN" \
|
|
87
|
+
-F "files=@$FILE" \
|
|
88
|
+
"${SERVER}/api/projects/${PROJECT}/upload${QUERY}")
|
|
89
|
+
|
|
90
|
+
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
|
91
|
+
echo "OK: $BASENAME ($HTTP_CODE)"
|
|
92
|
+
((SUCCESS++))
|
|
93
|
+
else
|
|
94
|
+
echo "FAIL: $BASENAME (HTTP $HTTP_CODE)"
|
|
95
|
+
((FAILED++))
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
echo ""
|
|
100
|
+
echo "Done: $SUCCESS uploaded, $FAILED failed"
|
|
101
|
+
[ "$FAILED" -gt 0 ] && exit 1
|
|
102
|
+
exit 0
|
package/bin/mdpush.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mdpush",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Push markdown files to Preview Reader from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"mdpush": "./bin/mdpush.js"
|
|
11
|
+
},
|
|
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
|
+
"dependencies": {
|
|
37
|
+
"commander": "^12.0.0",
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"ora": "^8.0.0",
|
|
40
|
+
"prompts": "^2.4.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { readConfig } from '../lib/config-store.js'
|
|
3
|
+
|
|
4
|
+
/** Show stored CLI configuration */
|
|
5
|
+
export function configCommand() {
|
|
6
|
+
const config = readConfig()
|
|
7
|
+
if (!config.server) {
|
|
8
|
+
console.log(chalk.yellow('Not configured. Run: mdpush setup'))
|
|
9
|
+
return
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
console.log(chalk.bold('mdpush configuration:'))
|
|
13
|
+
console.log(` Server: ${config.server}`)
|
|
14
|
+
console.log(` Username: ${config.username}`)
|
|
15
|
+
console.log(` Token: ${config.token ? '[stored]' : 'none'}`)
|
|
16
|
+
if (config.defaultProject) {
|
|
17
|
+
console.log(` Project: ${config.defaultProject}`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import prompts from 'prompts'
|
|
3
|
+
import { writeConfig } from '../lib/config-store.js'
|
|
4
|
+
|
|
5
|
+
/** Login to Preview Reader server — prompts credentials, stores API token */
|
|
6
|
+
export async function loginCommand(options) {
|
|
7
|
+
const server = options.server.replace(/\/$/, '')
|
|
8
|
+
|
|
9
|
+
const { username, password } = await prompts([
|
|
10
|
+
{ type: 'text', name: 'username', message: 'Username' },
|
|
11
|
+
{ type: 'password', name: 'password', message: 'Password' }
|
|
12
|
+
])
|
|
13
|
+
|
|
14
|
+
if (!username || !password) {
|
|
15
|
+
console.log(chalk.red('Login cancelled.'))
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Step 1: Login to get JWT
|
|
21
|
+
const loginRes = await fetch(`${server}/api/auth/login`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
body: JSON.stringify({ username, password })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (!loginRes.ok) {
|
|
28
|
+
const err = await loginRes.json().catch(() => ({}))
|
|
29
|
+
throw new Error(err.error || `Login failed (${loginRes.status})`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { token: jwt } = await loginRes.json()
|
|
33
|
+
|
|
34
|
+
// Step 2: Generate API token for CLI
|
|
35
|
+
const tokenRes = await fetch(`${server}/api/auth/tokens`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
'Authorization': `Bearer ${jwt}`
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({ name: 'mdpush-cli' })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (!tokenRes.ok) {
|
|
45
|
+
throw new Error('Failed to generate API token')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { token: apiToken } = await tokenRes.json()
|
|
49
|
+
|
|
50
|
+
// Step 3: Store config
|
|
51
|
+
writeConfig({ server, token: apiToken, username })
|
|
52
|
+
|
|
53
|
+
console.log(chalk.green(`Logged in as ${chalk.bold(username)} on ${server}`))
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.log(chalk.red(`Error: ${err.message}`))
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import ora from 'ora'
|
|
5
|
+
import { apiRequest } from '../lib/api-request.js'
|
|
6
|
+
import { readConfig } from '../lib/config-store.js'
|
|
7
|
+
|
|
8
|
+
/** Push markdown files to a project */
|
|
9
|
+
export async function pushCommand(files, options) {
|
|
10
|
+
const config = readConfig()
|
|
11
|
+
const project = options.project || config.defaultProject
|
|
12
|
+
const { folder } = options
|
|
13
|
+
|
|
14
|
+
if (!project) {
|
|
15
|
+
console.log(chalk.red('No project specified. Use -p <slug> or run: mdpush setup'))
|
|
16
|
+
process.exit(1)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Resolve file list — expand directories
|
|
20
|
+
const resolvedFiles = []
|
|
21
|
+
for (const f of files) {
|
|
22
|
+
const stat = fs.statSync(f, { throwIfNoEntry: false })
|
|
23
|
+
if (!stat) {
|
|
24
|
+
console.log(chalk.yellow(`Skipping: ${f} (not found)`))
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
if (stat.isDirectory()) {
|
|
28
|
+
const dirFiles = fs.readdirSync(f, { recursive: true })
|
|
29
|
+
.filter(name => /\.(md|txt)$/i.test(name))
|
|
30
|
+
.map(name => path.join(f, name))
|
|
31
|
+
resolvedFiles.push(...dirFiles)
|
|
32
|
+
} else if (/\.(md|txt)$/i.test(f)) {
|
|
33
|
+
resolvedFiles.push(f)
|
|
34
|
+
} else {
|
|
35
|
+
console.log(chalk.yellow(`Skipping: ${f} (not .md or .txt)`))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (resolvedFiles.length === 0) {
|
|
40
|
+
console.log(chalk.red('No .md or .txt files to upload.'))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.blue(`Uploading ${resolvedFiles.length} file(s) to ${project}${folder ? `/${folder}` : ''}...`))
|
|
45
|
+
|
|
46
|
+
let success = 0
|
|
47
|
+
let failed = 0
|
|
48
|
+
|
|
49
|
+
for (const filePath of resolvedFiles) {
|
|
50
|
+
const spinner = ora(`Uploading ${path.basename(filePath)}`).start()
|
|
51
|
+
try {
|
|
52
|
+
const formData = new FormData()
|
|
53
|
+
const content = fs.readFileSync(filePath)
|
|
54
|
+
formData.append('files', new Blob([content]), path.basename(filePath))
|
|
55
|
+
|
|
56
|
+
const url = `/api/projects/${project}/upload${folder ? `?folder=${encodeURIComponent(folder)}` : ''}`
|
|
57
|
+
await apiRequest(url, { method: 'POST', headers: {}, body: formData })
|
|
58
|
+
|
|
59
|
+
spinner.succeed(chalk.green(path.basename(filePath)))
|
|
60
|
+
success++
|
|
61
|
+
} catch (err) {
|
|
62
|
+
spinner.fail(chalk.red(`${path.basename(filePath)}: ${err.message}`))
|
|
63
|
+
failed++
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`\n${chalk.green(`${success} uploaded`)}${failed ? `, ${chalk.red(`${failed} failed`)}` : ''}`)
|
|
68
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import chalk from 'chalk'
|
|
6
|
+
import prompts from 'prompts'
|
|
7
|
+
import { writeConfig } from '../lib/config-store.js'
|
|
8
|
+
|
|
9
|
+
/** Interactive setup wizard — configure server, token, default project */
|
|
10
|
+
export async function setupCommand() {
|
|
11
|
+
console.log(chalk.bold('\nmdpush setup\n'))
|
|
12
|
+
|
|
13
|
+
// 1. Server URL
|
|
14
|
+
const { server } = await prompts({
|
|
15
|
+
type: 'text',
|
|
16
|
+
name: 'server',
|
|
17
|
+
message: 'Server URL',
|
|
18
|
+
initial: 'https://docs.sielay.cloud'
|
|
19
|
+
})
|
|
20
|
+
if (!server) { console.log(chalk.red('Setup cancelled.')); return }
|
|
21
|
+
|
|
22
|
+
const cleanServer = server.replace(/\/$/, '')
|
|
23
|
+
|
|
24
|
+
// 2. API token (masked input)
|
|
25
|
+
const { token } = await prompts({
|
|
26
|
+
type: 'password',
|
|
27
|
+
name: 'token',
|
|
28
|
+
message: 'API token (from web UI > API Tokens)'
|
|
29
|
+
})
|
|
30
|
+
if (!token) { console.log(chalk.red('Setup cancelled.')); return }
|
|
31
|
+
|
|
32
|
+
// 3. Validate token against server
|
|
33
|
+
let username
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetch(`${cleanServer}/api/auth/me`, {
|
|
36
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
37
|
+
})
|
|
38
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
39
|
+
const data = await res.json()
|
|
40
|
+
username = data.username
|
|
41
|
+
console.log(chalk.green(` Authenticated as ${chalk.bold(username)}`))
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.log(chalk.red(` Token validation failed: ${err.message}`))
|
|
44
|
+
console.log(chalk.yellow(' Generate a token at: <server> > Sidebar > API Tokens'))
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 4. Default project (optional)
|
|
49
|
+
const { defaultProject } = await prompts({
|
|
50
|
+
type: 'text',
|
|
51
|
+
name: 'defaultProject',
|
|
52
|
+
message: 'Default project slug (optional, press Enter to skip)'
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// 5. Save config
|
|
56
|
+
writeConfig({
|
|
57
|
+
server: cleanServer,
|
|
58
|
+
token,
|
|
59
|
+
username,
|
|
60
|
+
...(defaultProject ? { defaultProject } : {})
|
|
61
|
+
})
|
|
62
|
+
console.log(chalk.green(' Config saved to ~/.mdpush.json'))
|
|
63
|
+
|
|
64
|
+
// 6. Claude Code skill install (optional)
|
|
65
|
+
await maybeInstallClaudeSkill()
|
|
66
|
+
|
|
67
|
+
console.log(chalk.green(chalk.bold('\nSetup complete!') + ' Try: mdpush push README.md\n'))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Detect Claude Code and offer to install mdpush skill */
|
|
71
|
+
async function maybeInstallClaudeSkill() {
|
|
72
|
+
const skillDir = path.join(os.homedir(), '.claude', 'skills')
|
|
73
|
+
if (!fs.existsSync(skillDir)) return
|
|
74
|
+
|
|
75
|
+
const { install } = await prompts({
|
|
76
|
+
type: 'confirm',
|
|
77
|
+
name: 'install',
|
|
78
|
+
message: 'Claude Code detected. Install mdpush skill?',
|
|
79
|
+
initial: true
|
|
80
|
+
})
|
|
81
|
+
if (!install) return
|
|
82
|
+
|
|
83
|
+
const targetDir = path.join(skillDir, 'mdpush')
|
|
84
|
+
const targetScriptsDir = path.join(targetDir, 'scripts')
|
|
85
|
+
fs.mkdirSync(targetScriptsDir, { recursive: true })
|
|
86
|
+
|
|
87
|
+
// Assets are bundled at ../../assets/ relative to this file
|
|
88
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url))
|
|
89
|
+
const assetsDir = path.join(currentDir, '..', '..', 'assets')
|
|
90
|
+
|
|
91
|
+
fs.copyFileSync(
|
|
92
|
+
path.join(assetsDir, 'SKILL.md'),
|
|
93
|
+
path.join(targetDir, 'SKILL.md')
|
|
94
|
+
)
|
|
95
|
+
fs.copyFileSync(
|
|
96
|
+
path.join(assetsDir, 'scripts', 'push.sh'),
|
|
97
|
+
path.join(targetScriptsDir, 'push.sh')
|
|
98
|
+
)
|
|
99
|
+
fs.chmodSync(path.join(targetScriptsDir, 'push.sh'), 0o755)
|
|
100
|
+
|
|
101
|
+
console.log(chalk.green(' Claude Code skill installed to ~/.claude/skills/mdpush/'))
|
|
102
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { program } from 'commander'
|
|
2
|
+
import { loginCommand } from './commands/login-command.js'
|
|
3
|
+
import { pushCommand } from './commands/push-command.js'
|
|
4
|
+
import { configCommand } from './commands/config-command.js'
|
|
5
|
+
import { logoutCommand } from './commands/logout-command.js'
|
|
6
|
+
import { setupCommand } from './commands/setup-command.js'
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.name('mdpush')
|
|
10
|
+
.description('Push markdown files to Preview Reader')
|
|
11
|
+
.version('1.0.0')
|
|
12
|
+
|
|
13
|
+
program.command('setup')
|
|
14
|
+
.description('Interactive setup — configure server, token, and default project')
|
|
15
|
+
.action(setupCommand)
|
|
16
|
+
|
|
17
|
+
program.command('login')
|
|
18
|
+
.description('Authenticate with server')
|
|
19
|
+
.requiredOption('-s, --server <url>', 'Server URL (e.g., https://docs.example.com)')
|
|
20
|
+
.action(loginCommand)
|
|
21
|
+
|
|
22
|
+
program.command('push')
|
|
23
|
+
.description('Upload files to a project')
|
|
24
|
+
.option('-p, --project <slug>', 'Project slug')
|
|
25
|
+
.option('-f, --folder <path>', 'Target folder within project')
|
|
26
|
+
.argument('<files...>', 'Files or directory to upload')
|
|
27
|
+
.action(pushCommand)
|
|
28
|
+
|
|
29
|
+
program.command('config')
|
|
30
|
+
.description('Show stored configuration')
|
|
31
|
+
.action(configCommand)
|
|
32
|
+
|
|
33
|
+
program.command('logout')
|
|
34
|
+
.description('Clear stored credentials')
|
|
35
|
+
.action(logoutCommand)
|
|
36
|
+
|
|
37
|
+
program.parse()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readConfig } from './config-store.js'
|
|
2
|
+
|
|
3
|
+
/** Make authenticated HTTP request to Preview Reader server */
|
|
4
|
+
export async function apiRequest(path, options = {}) {
|
|
5
|
+
const config = readConfig()
|
|
6
|
+
if (!config.server || !config.token) {
|
|
7
|
+
throw new Error('Not logged in. Run: mdpush login --server <url>')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const url = `${config.server.replace(/\/$/, '')}${path}`
|
|
11
|
+
const { headers: optHeaders, ...rest } = options
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
...rest,
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `Bearer ${config.token}`,
|
|
16
|
+
...optHeaders
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
if (!res.ok) {
|
|
21
|
+
const err = await res.json().catch(() => ({ error: res.statusText }))
|
|
22
|
+
throw new Error(err.error || `HTTP ${res.status}`)
|
|
23
|
+
}
|
|
24
|
+
return res
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATH = path.join(os.homedir(), '.mdpush.json')
|
|
6
|
+
|
|
7
|
+
/** Read stored config from ~/.mdpush.json */
|
|
8
|
+
export function readConfig() {
|
|
9
|
+
if (!fs.existsSync(CONFIG_PATH)) return {}
|
|
10
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Write config to ~/.mdpush.json with restricted permissions */
|
|
14
|
+
export function writeConfig(data) {
|
|
15
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(data, null, 2))
|
|
16
|
+
// Restrict file permissions on Unix (0600) — skip silently on Windows
|
|
17
|
+
try { fs.chmodSync(CONFIG_PATH, 0o600) } catch (e) {
|
|
18
|
+
if (e.code !== 'ENOTSUP' && e.code !== 'EPERM') throw e
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Delete config file */
|
|
23
|
+
export function clearConfig() {
|
|
24
|
+
if (fs.existsSync(CONFIG_PATH)) fs.unlinkSync(CONFIG_PATH)
|
|
25
|
+
}
|