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 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
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js'
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,8 @@
1
+ import chalk from 'chalk'
2
+ import { clearConfig } from '../lib/config-store.js'
3
+
4
+ /** Clear stored credentials */
5
+ export function logoutCommand() {
6
+ clearConfig()
7
+ console.log(chalk.green('Logged out. Config file removed.'))
8
+ }
@@ -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
+ }