typelit-app 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/README.md +46 -0
- package/bin/typelit.mjs +158 -0
- package/lib/browser.mjs +25 -0
- package/lib/output.mjs +26 -0
- package/lib/session.mjs +72 -0
- package/lib/skill.mjs +26 -0
- package/package.json +31 -0
- package/skill/SKILL.md +28 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typelit-app
|
|
2
|
+
|
|
3
|
+
View markdown files beautifully in your browser from the terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g typelit-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use directly with npx:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx typelit-app README.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
typelit README.md
|
|
21
|
+
typelit docs/guide.md
|
|
22
|
+
typelit --help
|
|
23
|
+
typelit --version
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## How it works
|
|
27
|
+
|
|
28
|
+
1. Reads your markdown file locally
|
|
29
|
+
2. Creates a secure, single-use session on typelit.app
|
|
30
|
+
3. Opens your browser to the session URL
|
|
31
|
+
4. Pushes the content (session expires in 60 seconds)
|
|
32
|
+
|
|
33
|
+
Your file never leaves your machine until you run the command. Sessions are single-use and expire automatically.
|
|
34
|
+
|
|
35
|
+
## Claude Code integration
|
|
36
|
+
|
|
37
|
+
The CLI automatically installs a Claude Code skill on first run. After that, use `/typelit README.md` inside Claude Code to view any markdown file.
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
- Node.js 18+
|
|
42
|
+
- A web browser
|
|
43
|
+
|
|
44
|
+
## License
|
|
45
|
+
|
|
46
|
+
MIT
|
package/bin/typelit.mjs
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve, basename } from 'node:path';
|
|
5
|
+
import { createSession, pushContent, createShare } from '../lib/session.mjs';
|
|
6
|
+
import { openBrowser } from '../lib/browser.mjs';
|
|
7
|
+
import { ensureSkillInstalled } from '../lib/skill.mjs';
|
|
8
|
+
import { log, success, error, warn, bold } from '../lib/output.mjs';
|
|
9
|
+
|
|
10
|
+
const VERSION = '1.0.0';
|
|
11
|
+
const MAX_SIZE = 1_000_000; // 1MB
|
|
12
|
+
const VIEWER_BASE = 'https://typelit.app/p';
|
|
13
|
+
|
|
14
|
+
const W = '\x1b[97m';
|
|
15
|
+
const T = '\x1b[38;2;198;95;70m';
|
|
16
|
+
const DIM = '\x1b[2m';
|
|
17
|
+
const BOLD = '\x1b[1m';
|
|
18
|
+
const R = '\x1b[0m';
|
|
19
|
+
|
|
20
|
+
const LOGO =
|
|
21
|
+
`\n` +
|
|
22
|
+
`${W} ╔╦╗ ╦ ╦ ╔═╗ ╔═╗ ${T}╦ ╦ ╔╦╗${R}\n` +
|
|
23
|
+
`${W} ║ ╚╦╝ ╠═╝ ║╣ ${T}║ ║ ║ ${R}\n` +
|
|
24
|
+
`${W} ╩ ╩ ╩ ╚═╝ ${T}╩═╝ ╩ ╩ ${R}\n` +
|
|
25
|
+
`${DIM} beautiful markdown, right here${R}\n`;
|
|
26
|
+
|
|
27
|
+
const HELP = `${LOGO}
|
|
28
|
+
${BOLD}Usage:${R}
|
|
29
|
+
typelit <file.md> Open a markdown file
|
|
30
|
+
typelit <file.md> --share Open + create a 7-day share link
|
|
31
|
+
typelit <file.md> --no-skill Skip Claude Code skill auto-install
|
|
32
|
+
typelit view <file.md> Same as above
|
|
33
|
+
typelit --help Show this help
|
|
34
|
+
typelit --version Show version
|
|
35
|
+
|
|
36
|
+
${BOLD}Examples:${R}
|
|
37
|
+
typelit README.md
|
|
38
|
+
typelit README.md --share
|
|
39
|
+
npx typelit-app CHANGELOG.md
|
|
40
|
+
|
|
41
|
+
${DIM}https://typelit.app${R}
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
|
|
47
|
+
if (args.includes('--help') || args.includes('-h') || args.length === 0) {
|
|
48
|
+
console.log(HELP);
|
|
49
|
+
process.exit(args.length === 0 ? 1 : 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
53
|
+
console.log(VERSION);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse flags
|
|
58
|
+
const wantShare = args.includes('--share');
|
|
59
|
+
const skipSkill = args.includes('--no-skill');
|
|
60
|
+
const positional = args.filter(a => !a.startsWith('--'));
|
|
61
|
+
|
|
62
|
+
// Support `typelit view <file>` or `typelit <file>`
|
|
63
|
+
let filePath = positional[0];
|
|
64
|
+
if (filePath === 'view') {
|
|
65
|
+
filePath = positional[1];
|
|
66
|
+
if (!filePath) {
|
|
67
|
+
error('Missing file path. Usage: typelit view <file.md>');
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Auto-install Claude Code skill (best-effort, opt-out with --no-skill)
|
|
73
|
+
if (!skipSkill) {
|
|
74
|
+
const skillInstalled = ensureSkillInstalled();
|
|
75
|
+
if (skillInstalled) {
|
|
76
|
+
log('Installing Claude Code /typelit skill...');
|
|
77
|
+
success('Claude Code skill installed — use /typelit in Claude Code');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Read file
|
|
82
|
+
const fullPath = resolve(filePath);
|
|
83
|
+
let content;
|
|
84
|
+
try {
|
|
85
|
+
content = readFileSync(fullPath, 'utf-8');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.code === 'ENOENT') {
|
|
88
|
+
error(`File not found: ${fullPath}`);
|
|
89
|
+
} else if (err.code === 'EACCES') {
|
|
90
|
+
error(`Permission denied: ${fullPath}`);
|
|
91
|
+
} else {
|
|
92
|
+
error(`Could not read file: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Size check
|
|
98
|
+
const size = Buffer.byteLength(content, 'utf-8');
|
|
99
|
+
if (size > MAX_SIZE) {
|
|
100
|
+
error(`File too large (${(size / 1_000_000).toFixed(1)}MB) — 1MB maximum.`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const filename = basename(fullPath);
|
|
105
|
+
log(`Opening ${filename} (${formatSize(size)})...`);
|
|
106
|
+
|
|
107
|
+
// Create session
|
|
108
|
+
let sessionId;
|
|
109
|
+
try {
|
|
110
|
+
sessionId = await createSession();
|
|
111
|
+
} catch (err) {
|
|
112
|
+
error(err.message);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Open browser
|
|
117
|
+
const url = `${VIEWER_BASE}/${sessionId}`;
|
|
118
|
+
try {
|
|
119
|
+
await openBrowser(url);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
warn(`Could not open browser automatically.`);
|
|
122
|
+
log(`Open this URL: ${url}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Push content (small delay to let browser start loading)
|
|
126
|
+
await sleep(1000);
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await pushContent(sessionId, content, filename);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
error(err.message);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
success(`${filename} is now live in your browser`);
|
|
136
|
+
|
|
137
|
+
// Share if requested
|
|
138
|
+
if (wantShare) {
|
|
139
|
+
log('Creating share link...');
|
|
140
|
+
try {
|
|
141
|
+
const shareUrl = await createShare(content, filename);
|
|
142
|
+
success(`Share link (7 days): ${bold(shareUrl)}`);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
warn(`Could not create share link: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function sleep(ms) {
|
|
150
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatSize(bytes) {
|
|
154
|
+
if (bytes < 1000) return `${bytes}B`;
|
|
155
|
+
return `${(bytes / 1000).toFixed(1)}KB`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
main();
|
package/lib/browser.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export function openBrowser(url) {
|
|
4
|
+
const platform = process.platform;
|
|
5
|
+
let cmd;
|
|
6
|
+
let args;
|
|
7
|
+
|
|
8
|
+
if (platform === 'darwin') {
|
|
9
|
+
cmd = 'open';
|
|
10
|
+
args = [url];
|
|
11
|
+
} else if (platform === 'win32') {
|
|
12
|
+
cmd = 'cmd';
|
|
13
|
+
args = ['/c', 'start', '', url];
|
|
14
|
+
} else {
|
|
15
|
+
cmd = 'xdg-open';
|
|
16
|
+
args = [url];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
execFile(cmd, args, (err) => {
|
|
21
|
+
if (err) reject(new Error(`Could not open browser: ${err.message}`));
|
|
22
|
+
else resolve();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
package/lib/output.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const BOLD = '\x1b[1m';
|
|
2
|
+
const DIM = '\x1b[2m';
|
|
3
|
+
const RED = '\x1b[31m';
|
|
4
|
+
const GREEN = '\x1b[32m';
|
|
5
|
+
const YELLOW = '\x1b[33m';
|
|
6
|
+
const RESET = '\x1b[0m';
|
|
7
|
+
|
|
8
|
+
export function log(msg) {
|
|
9
|
+
console.log(`${DIM}typelit${RESET} ${msg}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function success(msg) {
|
|
13
|
+
console.log(`${GREEN}typelit${RESET} ${msg}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function warn(msg) {
|
|
17
|
+
console.error(`${YELLOW}typelit${RESET} ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function error(msg) {
|
|
21
|
+
console.error(`${RED}typelit${RESET} ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function bold(str) {
|
|
25
|
+
return `${BOLD}${str}${RESET}`;
|
|
26
|
+
}
|
package/lib/session.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const API_BASE = 'https://typelit.app/api';
|
|
2
|
+
|
|
3
|
+
export async function createSession() {
|
|
4
|
+
const res = await fetch(`${API_BASE}/session`, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
if (res.status === 429) {
|
|
10
|
+
throw new Error('Rate limited — too many sessions. Try again in a minute.');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!res.ok) {
|
|
14
|
+
throw new Error(`Failed to create session (HTTP ${res.status})`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const data = await res.json();
|
|
18
|
+
return data.sessionId;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function pushContent(sessionId, content, filename) {
|
|
22
|
+
const res = await fetch(`${API_BASE}/session/${sessionId}`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: { 'Content-Type': 'application/json' },
|
|
25
|
+
body: JSON.stringify({ content, filename }),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (res.status === 413) {
|
|
29
|
+
throw new Error('File too large — 1MB maximum.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (res.status === 429) {
|
|
33
|
+
throw new Error('Rate limited — too many pushes. Try again in a minute.');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const body = await res.json().catch(() => ({}));
|
|
38
|
+
throw new Error(body.error || `Failed to push content (HTTP ${res.status})`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function createShare(markdown, title) {
|
|
43
|
+
// Generate a stable-ish user ID for CLI shares
|
|
44
|
+
const userId = 'cli-' + Math.random().toString(36).slice(2, 10);
|
|
45
|
+
|
|
46
|
+
const res = await fetch(`${API_BASE}/share`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
'Origin': 'https://typelit.app',
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
markdown,
|
|
54
|
+
title: title || 'Untitled.md',
|
|
55
|
+
user_id: userId,
|
|
56
|
+
font_theme: 'classic',
|
|
57
|
+
dark_mode: false,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (res.status === 429) {
|
|
62
|
+
throw new Error('Rate limited — too many shares. Try again later.');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const body = await res.json().catch(() => ({}));
|
|
67
|
+
throw new Error(body.error || `Failed to create share (HTTP ${res.status})`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = await res.json();
|
|
71
|
+
return data.url;
|
|
72
|
+
}
|
package/lib/skill.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync, mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const SKILL_DIR = join(homedir(), '.claude', 'skills', 'typelit');
|
|
8
|
+
const SKILL_PATH = join(SKILL_DIR, 'SKILL.md');
|
|
9
|
+
const BUNDLED_SKILL = join(__dirname, '..', 'skill', 'SKILL.md');
|
|
10
|
+
|
|
11
|
+
export function ensureSkillInstalled() {
|
|
12
|
+
try {
|
|
13
|
+
if (existsSync(SKILL_PATH)) return false;
|
|
14
|
+
|
|
15
|
+
// Log before writing so the user knows what's happening
|
|
16
|
+
console.log(`\x1b[2mtypelit\x1b[0m Writing Claude Code skill to ${SKILL_PATH} (use --no-skill to skip)`);
|
|
17
|
+
|
|
18
|
+
const template = readFileSync(BUNDLED_SKILL, 'utf-8');
|
|
19
|
+
mkdirSync(SKILL_DIR, { recursive: true });
|
|
20
|
+
writeFileSync(SKILL_PATH, template, 'utf-8');
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
// Best-effort — don't fail the main flow
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "typelit-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "View markdown files beautifully in your browser from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"typelit": "./bin/typelit.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"skill/"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"markdown",
|
|
19
|
+
"viewer",
|
|
20
|
+
"cli",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"typelit"
|
|
23
|
+
],
|
|
24
|
+
"author": "typelit",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://typelit.app",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/jonthebeef/typelit"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typelit
|
|
3
|
+
description: Open a markdown file in typelit.app for beautiful rendering.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Typelit
|
|
7
|
+
|
|
8
|
+
Open any markdown file in typelit.app — beautifully rendered in your browser.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
/typelit [file-path]
|
|
13
|
+
|
|
14
|
+
## Instructions
|
|
15
|
+
|
|
16
|
+
Run this command, replacing FILE_PATH with the actual file path:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx typelit-app "FILE_PATH"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
If typelit-app is installed globally, you can also run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
typelit "FILE_PATH"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
After the command completes, respond: "Opened FILE_PATH in typelit."
|