readflow-md 0.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.
Files changed (2) hide show
  1. package/package.json +24 -0
  2. package/readflow.mjs +570 -0
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "readflow-md",
3
+ "version": "0.1.0",
4
+ "description": "Share markdown files instantly from the terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "readflow": "./readflow.mjs"
8
+ },
9
+ "keywords": [
10
+ "markdown",
11
+ "share",
12
+ "cli",
13
+ "readme",
14
+ "documentation"
15
+ ],
16
+ "license": "MIT",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Abhinav-ranish/Readflow.git"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ }
24
+ }
package/readflow.mjs ADDED
@@ -0,0 +1,570 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Readflow CLI — Share markdown files instantly
5
+ *
6
+ * npx readflow share README.md
7
+ * npx readflow install Install project skill files
8
+ * npx readflow config --show Show config
9
+ * npx readflow login Authenticate via browser
10
+ */
11
+
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'fs';
13
+ import { basename, join, resolve } from 'path';
14
+ import { homedir } from 'os';
15
+ import { createInterface } from 'readline';
16
+ import { execSync } from 'child_process';
17
+
18
+ const API_BASE = process.env.READFLOW_API_URL || 'https://readflow.aranish.uk';
19
+ const CONFIG_DIR = join(homedir(), '.readflow');
20
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
21
+
22
+ // ─── Config ───────────────────────────────────────────���──────
23
+
24
+ function loadConfig() {
25
+ try {
26
+ if (existsSync(CONFIG_FILE)) return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
27
+ } catch {}
28
+ return {};
29
+ }
30
+
31
+ function saveConfig(cfg) {
32
+ mkdirSync(CONFIG_DIR, { recursive: true });
33
+ writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
34
+ }
35
+
36
+ function getToken(opts) {
37
+ return opts?.token || process.env.READFLOW_TOKEN || loadConfig().token || null;
38
+ }
39
+
40
+ // ─── Project config (per-project .readflow/) ─────────────────
41
+
42
+ function projectConfigPath() {
43
+ return join(process.cwd(), '.readflow', 'config.json');
44
+ }
45
+
46
+ function loadProjectConfig() {
47
+ try {
48
+ const p = projectConfigPath();
49
+ if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8'));
50
+ } catch {}
51
+ return {};
52
+ }
53
+
54
+ function saveProjectConfig(cfg) {
55
+ const dir = join(process.cwd(), '.readflow');
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(projectConfigPath(), JSON.stringify(cfg, null, 2));
58
+ }
59
+
60
+ // ─── Prompt helper ───────────────────────────────────────────
61
+
62
+ function ask(question) {
63
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
64
+ return new Promise(resolve => {
65
+ rl.question(question, answer => { rl.close(); resolve(answer.trim()); });
66
+ });
67
+ }
68
+
69
+ // ─── Browser auth flow ──────────────────────────────────────
70
+
71
+ function openBrowser(url) {
72
+ try {
73
+ const platform = process.platform;
74
+ if (platform === 'darwin') execSync(`open "${url}"`);
75
+ else if (platform === 'win32') execSync(`start "" "${url}"`);
76
+ else execSync(`xdg-open "${url}"`);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ function sleep(ms) {
84
+ return new Promise(r => setTimeout(r, ms));
85
+ }
86
+
87
+ async function browserLogin() {
88
+ console.log('\n Starting browser login...\n');
89
+
90
+ // Request a code from the server
91
+ let code;
92
+ try {
93
+ const res = await fetch(`${API_BASE}/api/cli/auth`, {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/json' },
96
+ body: JSON.stringify({ action: 'create' }),
97
+ });
98
+ const data = await res.json();
99
+ code = data.code;
100
+ } catch {
101
+ console.error(' Error: Could not connect to server.');
102
+ return false;
103
+ }
104
+
105
+ const authUrl = `${API_BASE}/cli/auth?code=${code}`;
106
+
107
+ const opened = openBrowser(authUrl);
108
+ if (opened) {
109
+ console.log(` Browser opened! Approve the login there.`);
110
+ } else {
111
+ console.log(` Could not open browser. Visit this URL:`);
112
+ }
113
+ console.log(` ${authUrl}\n`);
114
+ console.log(` Code: ${code}\n`);
115
+ console.log(' Waiting for approval...');
116
+
117
+ // Poll for approval (max 10 min, every 3s)
118
+ for (let i = 0; i < 200; i++) {
119
+ await sleep(3000);
120
+ try {
121
+ const res = await fetch(`${API_BASE}/api/cli/auth?code=${code}`);
122
+ const data = await res.json();
123
+
124
+ if (data.status === 'approved') {
125
+ const cfg = loadConfig();
126
+ saveConfig({ ...cfg, token: data.token, setupDone: true });
127
+ console.log(`\n ✓ Logged in as ${data.email || 'your account'}`);
128
+ console.log(` All shares will be linked to your account.\n`);
129
+ return true;
130
+ }
131
+
132
+ if (data.status === 'expired') {
133
+ console.log('\n Code expired. Try again.');
134
+ return false;
135
+ }
136
+ } catch {
137
+ // Network hiccup, keep polling
138
+ }
139
+ }
140
+
141
+ console.log('\n Timed out waiting for approval.');
142
+ return false;
143
+ }
144
+
145
+ // ─── First-run check ────────────────────────────────────────
146
+
147
+ async function firstRunCheck() {
148
+ const cfg = loadConfig();
149
+ if (cfg.setupDone) return;
150
+
151
+ console.log(`\n Welcome to Readflow CLI!\n`);
152
+ console.log(` You can share anonymously or log in to post to your account.`);
153
+ console.log(` Logged-in shares appear in your dashboard and can be edited.`);
154
+ console.log(` You can change this anytime with: npx readflow login\n`);
155
+
156
+ const choice = await ask(' Log in via browser? (y/n): ');
157
+
158
+ if (choice.toLowerCase() === 'y') {
159
+ const ok = await browserLogin();
160
+ if (!ok) {
161
+ saveConfig({ setupDone: true });
162
+ console.log(` Continuing anonymously. Run "npx readflow login" anytime.\n`);
163
+ }
164
+ } else {
165
+ saveConfig({ setupDone: true });
166
+ console.log(`\n Sharing anonymously. Run "npx readflow login" to log in later.\n`);
167
+ }
168
+ }
169
+
170
+ // ─── Arg parsing ────────────────────────────────────────────
171
+
172
+ function parseArgs(args) {
173
+ const result = { file: null, title: null, password: null, expires: null, token: null, update: false, help: false };
174
+ let i = 0;
175
+ while (i < args.length) {
176
+ const arg = args[i];
177
+ if (arg === '--help' || arg === '-h') { result.help = true; }
178
+ else if (arg === '--title' || arg === '-t') { result.title = args[++i]; }
179
+ else if (arg === '--password' || arg === '-p') { result.password = args[++i]; }
180
+ else if (arg === '--expires' || arg === '-e') { result.expires = args[++i]; }
181
+ else if (arg === '--token') { result.token = args[++i]; }
182
+ else if (arg === '--update' || arg === '-u') { result.update = true; }
183
+ else if (!result.file) { result.file = arg; }
184
+ i++;
185
+ }
186
+ return result;
187
+ }
188
+
189
+ function parseExpiry(str) {
190
+ if (!str) return undefined;
191
+ const match = str.match(/^(\d+)(h|d|m)$/);
192
+ if (!match) return undefined;
193
+ const num = parseInt(match[1]);
194
+ switch (match[2]) {
195
+ case 'h': return num * 3600;
196
+ case 'd': return num * 86400;
197
+ case 'm': return num * 60;
198
+ }
199
+ return undefined;
200
+ }
201
+
202
+ // ─── Install command ────────────────────────────────────────
203
+
204
+ function detectProjectName() {
205
+ try {
206
+ const pkg = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf8'));
207
+ return pkg.name || basename(process.cwd());
208
+ } catch {
209
+ return basename(process.cwd());
210
+ }
211
+ }
212
+
213
+ function findMarkdownFiles() {
214
+ const files = [];
215
+ const ignore = ['node_modules', '.git', '.readflow', 'dist', 'build', '.next'];
216
+
217
+ function walk(dir, depth) {
218
+ if (depth > 3) return;
219
+ try {
220
+ for (const entry of readdirSync(dir)) {
221
+ if (ignore.includes(entry)) continue;
222
+ const full = join(dir, entry);
223
+ const stat = statSync(full);
224
+ if (stat.isDirectory()) walk(full, depth + 1);
225
+ else if (entry.endsWith('.md') || entry.endsWith('.mdx')) {
226
+ const rel = full.replace(process.cwd() + '/', '');
227
+ files.push(rel);
228
+ }
229
+ }
230
+ } catch {}
231
+ }
232
+
233
+ walk(process.cwd(), 0);
234
+ return files;
235
+ }
236
+
237
+ async function handleInstall() {
238
+ const projectName = detectProjectName();
239
+ console.log(`\n Installing Readflow for: ${projectName}\n`);
240
+
241
+ const projCfg = loadProjectConfig();
242
+
243
+ // Create .readflow dir
244
+ const rfDir = join(process.cwd(), '.readflow');
245
+ mkdirSync(rfDir, { recursive: true });
246
+
247
+ // Write CLAUDE.md skill file
248
+ const claudeMd = `# Readflow Commit & Document Guidelines
249
+
250
+ ## Commit Message Format
251
+
252
+ - First letter MUST be capitalized
253
+ - Include project name in scope: \`[${projectName}] Your message\`
254
+ - Use present tense: "Add feature" not "Added feature"
255
+ - Keep first line under 72 characters
256
+
257
+ ### Examples
258
+ - \`[${projectName}] Add user authentication\`
259
+ - \`[${projectName}] Fix markdown parsing edge case\`
260
+ - \`[${projectName}] Update API documentation\`
261
+
262
+ ## Document Sharing
263
+
264
+ When README.md is created or updated, upload it to Readflow:
265
+ \`\`\`bash
266
+ npx readflow share README.md --title "${projectName} README" --update
267
+ \`\`\`
268
+
269
+ ## Tracked Files
270
+
271
+ The following files are automatically synced to Readflow when changed:
272
+ ${(projCfg.trackedFiles || ['README.md']).map(f => `- ${f}`).join('\n')}
273
+
274
+ To update tracked files: \`npx readflow install --reconfigure\`
275
+ `;
276
+
277
+ writeFileSync(join(rfDir, 'CLAUDE.md'), claudeMd);
278
+ console.log(' ✓ Created .readflow/CLAUDE.md (commit guidelines)');
279
+
280
+ // Write git hook helper
281
+ const hookScript = `#!/bin/sh
282
+ # Readflow post-commit hook — auto-upload tracked files
283
+ # Install: cp .readflow/post-commit .git/hooks/post-commit && chmod +x .git/hooks/post-commit
284
+
285
+ CHANGED=$(git diff-tree --no-commit-id --name-only -r HEAD)
286
+ CONFIG=".readflow/config.json"
287
+
288
+ if [ ! -f "$CONFIG" ]; then exit 0; fi
289
+
290
+ # Read tracked files from config
291
+ TRACKED=$(node -e "try{const c=JSON.parse(require('fs').readFileSync('$CONFIG','utf8'));(c.trackedFiles||[]).forEach(f=>console.log(f))}catch{}")
292
+
293
+ for FILE in $TRACKED; do
294
+ if echo "$CHANGED" | grep -q "^$FILE$"; then
295
+ echo "[readflow] Syncing $FILE..."
296
+ npx readflow share "$FILE" --update 2>/dev/null &
297
+ fi
298
+ done
299
+ `;
300
+
301
+ writeFileSync(join(rfDir, 'post-commit'), hookScript);
302
+ console.log(' ✓ Created .readflow/post-commit (git hook)');
303
+
304
+ // Ask about tracked files (skip if --reconfigure or already configured with --no-ask)
305
+ if (!projCfg.askedAboutFiles || process.argv.includes('--reconfigure')) {
306
+ const mdFiles = findMarkdownFiles();
307
+ const tracked = ['README.md'];
308
+
309
+ if (mdFiles.length > 1) {
310
+ console.log(`\n Found ${mdFiles.length} markdown files:\n`);
311
+ mdFiles.forEach((f, i) => {
312
+ const isReadme = f.toLowerCase() === 'readme.md';
313
+ console.log(` ${isReadme ? ' ✓' : ` ${i + 1}.`} ${f}${isReadme ? ' (always tracked)' : ''}`);
314
+ });
315
+
316
+ if (process.stdin.isTTY) {
317
+ const answer = await ask('\n Track additional files for auto-upload? (y/n): ');
318
+
319
+ if (answer.toLowerCase() === 'y') {
320
+ const otherFiles = mdFiles.filter(f => f.toLowerCase() !== 'readme.md');
321
+ console.log('');
322
+ for (const f of otherFiles) {
323
+ const include = await ask(` Track ${f}? (y/n): `);
324
+ if (include.toLowerCase() === 'y') tracked.push(f);
325
+ }
326
+ }
327
+ }
328
+ }
329
+
330
+ projCfg.trackedFiles = tracked;
331
+ projCfg.askedAboutFiles = true;
332
+ }
333
+
334
+ // Save project config
335
+ projCfg.projectName = projectName;
336
+ projCfg.installedAt = Date.now();
337
+ saveProjectConfig(projCfg);
338
+ console.log(' ✓ Created .readflow/config.json (project settings)');
339
+
340
+ // Re-write CLAUDE.md with final tracked files
341
+ const finalClaudeMd = `# Readflow Commit & Document Guidelines
342
+
343
+ ## Commit Message Format
344
+
345
+ - First letter MUST be capitalized
346
+ - Include project name in scope: \`[${projectName}] Your message\`
347
+ - Use present tense: "Add feature" not "Added feature"
348
+ - Keep first line under 72 characters
349
+
350
+ ### Examples
351
+ - \`[${projectName}] Add user authentication\`
352
+ - \`[${projectName}] Fix markdown parsing edge case\`
353
+ - \`[${projectName}] Update API documentation\`
354
+
355
+ ## Document Sharing
356
+
357
+ When README.md is created or updated, upload it to Readflow:
358
+ \`\`\`bash
359
+ npx readflow share README.md --title "${projectName} README" --update
360
+ \`\`\`
361
+
362
+ ## Tracked Files
363
+
364
+ The following files are automatically synced to Readflow when changed:
365
+ ${(projCfg.trackedFiles || ['README.md']).map(f => `- ${f}`).join('\n')}
366
+
367
+ To update tracked files: \`npx readflow install --reconfigure\`
368
+ `;
369
+ writeFileSync(join(rfDir, 'CLAUDE.md'), finalClaudeMd);
370
+
371
+ console.log(`\n Setup complete! To enable auto-upload on commit:\n`);
372
+ console.log(` cp .readflow/post-commit .git/hooks/post-commit`);
373
+ console.log(` chmod +x .git/hooks/post-commit\n`);
374
+ console.log(` Add ".readflow/" to your .gitignore if you don't want to share config.\n`);
375
+ }
376
+
377
+ // ─── Config command ──────────────────────────────────���──────
378
+
379
+ async function handleConfig(args) {
380
+ const cfg = loadConfig();
381
+
382
+ if (args.includes('--clear')) {
383
+ saveConfig({ ...cfg, token: undefined, setupDone: true });
384
+ console.log(' ✓ Token cleared. Shares will be anonymous.');
385
+ return;
386
+ }
387
+
388
+ if (args.includes('--show')) {
389
+ if (cfg.token) {
390
+ console.log(` Token: ${cfg.token.slice(0, 7)}...${cfg.token.slice(-4)}`);
391
+ console.log(` Mode: Authenticated`);
392
+ } else {
393
+ console.log(` Token: not set`);
394
+ console.log(` Mode: Anonymous`);
395
+ }
396
+ console.log(` API: ${API_BASE}`);
397
+
398
+ const projCfg = loadProjectConfig();
399
+ if (projCfg.projectName) {
400
+ console.log(` Project: ${projCfg.projectName}`);
401
+ console.log(` Tracked: ${(projCfg.trackedFiles || []).join(', ')}`);
402
+ }
403
+ return;
404
+ }
405
+
406
+ const tokenIdx = args.indexOf('--token');
407
+ if (tokenIdx !== -1 && args[tokenIdx + 1]) {
408
+ saveConfig({ ...cfg, token: args[tokenIdx + 1], setupDone: true });
409
+ console.log(` ✓ Token saved. Shares will be linked to your account.`);
410
+ return;
411
+ }
412
+
413
+ console.log(`
414
+ readflow config — Manage CLI settings
415
+
416
+ USAGE:
417
+ npx readflow config --token <key> Save agent token
418
+ npx readflow config --show Show current config
419
+ npx readflow config --clear Remove token (go anonymous)
420
+
421
+ Or log in via browser: npx readflow login
422
+ `);
423
+ }
424
+
425
+ // ─── Share command ──────────────────────────────────────────
426
+
427
+ async function handleShare(args) {
428
+ const opts = parseArgs(args);
429
+ if (opts.help) { printHelp(); process.exit(0); }
430
+
431
+ const hasToken = getToken(opts);
432
+ if (!hasToken && !loadConfig().setupDone && process.stdin.isTTY) {
433
+ await firstRunCheck();
434
+ }
435
+
436
+ let content;
437
+
438
+ if (!opts.file || opts.file === '-') {
439
+ const chunks = [];
440
+ process.stdin.setEncoding('utf8');
441
+ for await (const chunk of process.stdin) {
442
+ chunks.push(chunk);
443
+ }
444
+ content = chunks.join('');
445
+ if (!content.trim()) {
446
+ console.error('Error: No content provided via stdin');
447
+ process.exit(1);
448
+ }
449
+ } else {
450
+ try {
451
+ content = readFileSync(opts.file, 'utf8');
452
+ } catch {
453
+ console.error(`Error: Cannot read file "${opts.file}"`);
454
+ process.exit(1);
455
+ }
456
+ if (!opts.title) {
457
+ opts.title = basename(opts.file, '.md');
458
+ }
459
+ }
460
+
461
+ // Auto-detect update mode for tracked files
462
+ const projCfg = loadProjectConfig();
463
+ const isTracked = opts.file && projCfg.trackedFiles?.includes(opts.file);
464
+ const shouldUpdate = opts.update || isTracked;
465
+
466
+ const body = {
467
+ content,
468
+ title: opts.title || undefined,
469
+ password: opts.password || undefined,
470
+ expiresIn: parseExpiry(opts.expires),
471
+ };
472
+
473
+ // If updating, generate a deterministic slug from project name + file path
474
+ // This ensures each file maps to exactly one doc — no title collisions
475
+ if (shouldUpdate && opts.file) {
476
+ const project = projCfg.projectName || basename(process.cwd());
477
+ const fileSlug = opts.file.replace(/[\/\\]/g, '-').replace(/\.md$/i, '').toLowerCase().replace(/[^a-z0-9-]/g, '');
478
+ body.upsertSlug = `${project}-${fileSlug}`.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').slice(0, 60);
479
+ }
480
+
481
+ const token = getToken(opts);
482
+
483
+ try {
484
+ const headers = { 'Content-Type': 'application/json' };
485
+ if (token) headers['Authorization'] = `Bearer ${token}`;
486
+
487
+ const res = await fetch(`${API_BASE}/api/share`, {
488
+ method: 'POST',
489
+ headers,
490
+ body: JSON.stringify(body),
491
+ });
492
+
493
+ if (!res.ok) {
494
+ const data = await res.json().catch(() => ({}));
495
+ console.error(`Error: ${data.error || `HTTP ${res.status}`}`);
496
+ process.exit(1);
497
+ }
498
+
499
+ const data = await res.json();
500
+ const wasUpdated = data.updated;
501
+ console.log(`\n ✓ ${wasUpdated ? 'Updated' : 'Shared'} successfully!\n`);
502
+ console.log(` URL: ${data.url}`);
503
+ if (wasUpdated) console.log(` 📝 Existing document updated`);
504
+ if (token) console.log(` 👤 Posted to your account`);
505
+ else console.log(` 👻 Posted anonymously`);
506
+ if (opts.password) console.log(` 🔒 Password-protected`);
507
+ if (opts.expires) console.log(` ⏱ Expires in ${opts.expires}`);
508
+ console.log('');
509
+ } catch {
510
+ console.error(`Error: Could not connect to ${API_BASE}`);
511
+ process.exit(1);
512
+ }
513
+ }
514
+
515
+ // ─── Help ───────────────────────────────────────────────────
516
+
517
+ function printHelp() {
518
+ console.log(`
519
+ readflow — Share markdown files instantly
520
+
521
+ COMMANDS:
522
+ share <file> Share a markdown file
523
+ login Authenticate via browser
524
+ install Install project skill files & hooks
525
+ config Manage settings
526
+
527
+ SHARE OPTIONS:
528
+ --title, -t <title> Set document title
529
+ --password, -p <pass> Password-protect the share
530
+ --expires, -e <time> Set expiry (e.g., 1h, 24h, 7d)
531
+ --update, -u Update existing doc with same title (upsert)
532
+ --token <key> One-time token override
533
+ --help, -h Show this help
534
+
535
+ INSTALL OPTIONS:
536
+ --reconfigure Re-ask about tracked files
537
+
538
+ EXAMPLES:
539
+ npx readflow share README.md
540
+ npx readflow login
541
+ npx readflow install
542
+ npx readflow share docs/api.md --title "API Reference" --expires 7d
543
+
544
+ ENVIRONMENT:
545
+ READFLOW_API_URL Override API base URL
546
+ READFLOW_TOKEN Agent token override
547
+ `);
548
+ }
549
+
550
+ // ─── Main ───────────────────────────────────────────────────
551
+
552
+ async function main() {
553
+ const args = process.argv.slice(2);
554
+ const cmd = args[0];
555
+
556
+ if (!cmd || cmd === '--help' || cmd === '-h') {
557
+ printHelp();
558
+ process.exit(0);
559
+ }
560
+
561
+ switch (cmd) {
562
+ case 'share': return handleShare(args.slice(1));
563
+ case 'login': return browserLogin();
564
+ case 'install': return handleInstall();
565
+ case 'config': return handleConfig(args.slice(1));
566
+ default: return handleShare(args); // treat as file
567
+ }
568
+ }
569
+
570
+ main();