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 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
@@ -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();
@@ -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
+ }
@@ -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."