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.
- package/package.json +24 -0
- 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();
|