readable-cli 0.1.2 → 0.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "readable-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "CLI for Readable — publish Markdown from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,17 +11,20 @@
11
11
  "dist"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsc",
15
- "dev": "tsc --watch",
14
+ "build": "tsup",
15
+ "dev": "tsup --watch",
16
16
  "prepublishOnly": "npm run build"
17
17
  },
18
- "dependencies": {
19
- "commander": "^12.1.0"
20
- },
18
+ "dependencies": {},
21
19
  "devDependencies": {
22
20
  "@types/node": "^20.19.0",
21
+ "commander": "^12.1.0",
22
+ "tsup": "^8.5.1",
23
23
  "typescript": "^5.7.0"
24
24
  },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
25
28
  "engines": {
26
29
  "node": ">=18.0.0"
27
30
  },
package/dist/api.d.ts DELETED
@@ -1,13 +0,0 @@
1
- export type ApiResponse<T> = {
2
- ok: true;
3
- status: number;
4
- data: T;
5
- } | {
6
- ok: false;
7
- status: number;
8
- error: string;
9
- };
10
- export declare function apiRequest<T>(path: string, options?: {
11
- method?: string;
12
- body?: unknown;
13
- }): Promise<ApiResponse<T>>;
package/dist/api.js DELETED
@@ -1,43 +0,0 @@
1
- import { getApiKey, getApiBase } from "./config.js";
2
- export async function apiRequest(path, options = {}) {
3
- const key = await getApiKey();
4
- if (!key) {
5
- return { ok: false, status: 401, error: "Not authenticated. Run `readable login` to set your API key." };
6
- }
7
- const base = await getApiBase();
8
- const url = `${base}${path}`;
9
- const headers = {
10
- Authorization: `Bearer ${key}`,
11
- "X-Readable-Source": "cli",
12
- };
13
- if (options.body !== undefined) {
14
- headers["Content-Type"] = "application/json";
15
- }
16
- let res;
17
- try {
18
- res = await fetch(url, {
19
- method: options.method ?? "GET",
20
- headers,
21
- body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
22
- });
23
- }
24
- catch (e) {
25
- const msg = e instanceof Error ? e.message : String(e);
26
- return { ok: false, status: 0, error: `Network error: ${msg}` };
27
- }
28
- let data;
29
- const ct = res.headers.get("content-type") ?? "";
30
- try {
31
- data = ct.includes("application/json") ? await res.json() : await res.text();
32
- }
33
- catch {
34
- data = null;
35
- }
36
- if (!res.ok) {
37
- const errMsg = data && typeof data === "object" && "error" in data
38
- ? String(data.error)
39
- : `HTTP ${res.status}`;
40
- return { ok: false, status: res.status, error: errMsg };
41
- }
42
- return { ok: true, status: res.status, data: data };
43
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerAuthCommands(program: Command): Command;
@@ -1,86 +0,0 @@
1
- import { readConfig, writeConfig, getApiKey, getApiBase } from "../config.js";
2
- import { apiRequest } from "../api.js";
3
- import { success, error, info, bold, dim, gray } from "../fmt.js";
4
- import { createInterface } from "readline";
5
- function prompt(question) {
6
- const rl = createInterface({ input: process.stdin, output: process.stdout });
7
- return new Promise((resolve) => {
8
- rl.question(question, (answer) => {
9
- rl.close();
10
- resolve(answer.trim());
11
- });
12
- });
13
- }
14
- export function registerAuthCommands(program) {
15
- const auth = program
16
- .command("login")
17
- .description("Save your Readable API key")
18
- .option("--key <key>", "API key (skip interactive prompt)")
19
- .option("--api-url <url>", "Override API base URL")
20
- .action(async (opts) => {
21
- let key = opts.key;
22
- if (!key) {
23
- const base = opts.apiUrl ?? (await (async () => {
24
- const cfg = await readConfig();
25
- return cfg.apiBase;
26
- })());
27
- console.log();
28
- console.log(`${bold("Readable CLI")} — authenticate`);
29
- console.log(dim(`API: ${base}`));
30
- console.log();
31
- console.log(`Get your key at: ${bold(`${base}/my-pages`)} → Settings → API Keys`);
32
- console.log();
33
- key = await prompt("Paste your API key: ");
34
- }
35
- if (!key) {
36
- error("No key provided.");
37
- process.exit(1);
38
- }
39
- // Validate the key by hitting the pages list endpoint
40
- const existing = await readConfig();
41
- const testBase = opts.apiUrl ?? existing.apiBase;
42
- const testConfig = { ...existing, apiKey: key, apiBase: testBase };
43
- // Temporarily write so apiRequest can pick it up
44
- await writeConfig(testConfig);
45
- const res = await apiRequest("/api/v1/pages");
46
- if (!res.ok) {
47
- error(`Invalid key or unreachable server: ${res.error}`);
48
- // Roll back
49
- await writeConfig(existing);
50
- process.exit(1);
51
- }
52
- success(`Authenticated. Key saved to ~/.readable/config.json`);
53
- info(`You have ${res.data.pages.length} page(s).`);
54
- });
55
- program
56
- .command("logout")
57
- .description("Remove saved API key")
58
- .action(async () => {
59
- const config = await readConfig();
60
- if (!config.apiKey) {
61
- info("No key stored.");
62
- return;
63
- }
64
- const { apiKey: _removed, ...rest } = config;
65
- await writeConfig(rest);
66
- success("Logged out. API key removed.");
67
- });
68
- program
69
- .command("whoami")
70
- .description("Show the active API key and base URL")
71
- .action(async () => {
72
- const key = await getApiKey();
73
- const base = await getApiBase();
74
- if (!key) {
75
- info("Not authenticated. Run `readable login` to set your API key.");
76
- }
77
- else {
78
- const masked = key.length > 8
79
- ? `${key.slice(0, 4)}${"•".repeat(key.length - 8)}${key.slice(-4)}`
80
- : "•".repeat(key.length);
81
- console.log(`${bold("Key:")} ${masked}`);
82
- console.log(`${bold("Base:")} ${gray(base)}`);
83
- }
84
- });
85
- return auth;
86
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerPagesCommand(program: Command): void;
@@ -1,99 +0,0 @@
1
- import { apiRequest } from "../api.js";
2
- import { success, error, info, bold, dim, gray } from "../fmt.js";
3
- import { table } from "../fmt.js";
4
- import { createInterface } from "readline";
5
- function formatDate(iso) {
6
- try {
7
- return new Date(iso).toLocaleDateString("en-GB", {
8
- year: "numeric",
9
- month: "short",
10
- day: "numeric",
11
- });
12
- }
13
- catch {
14
- return iso;
15
- }
16
- }
17
- async function confirm(question) {
18
- if (!process.stdin.isTTY)
19
- return false;
20
- const rl = createInterface({ input: process.stdin, output: process.stdout });
21
- return new Promise((resolve) => {
22
- rl.question(`${question} [y/N] `, (ans) => {
23
- rl.close();
24
- resolve(ans.trim().toLowerCase() === "y");
25
- });
26
- });
27
- }
28
- export function registerPagesCommand(program) {
29
- const pages = program
30
- .command("pages")
31
- .description("Manage your Readable pages");
32
- pages
33
- .command("list")
34
- .description("List all your pages")
35
- .option("--json", "Output raw JSON")
36
- .action(async (opts) => {
37
- const res = await apiRequest("/api/v1/pages");
38
- if (!res.ok) {
39
- error(res.error);
40
- process.exit(1);
41
- }
42
- const items = res.data.pages;
43
- if (opts.json) {
44
- console.log(JSON.stringify(items, null, 2));
45
- return;
46
- }
47
- if (items.length === 0) {
48
- info("No pages yet. Run `readable publish <file>` to create one.");
49
- return;
50
- }
51
- console.log();
52
- table(["Title", "ID / Slug", "Visibility", "Views", "Updated"], items.map((p) => [
53
- p.title ?? dim("Untitled"),
54
- p.slug ?? gray(p.id),
55
- p.visibility,
56
- p.view_count,
57
- formatDate(p.updated_at),
58
- ]));
59
- console.log();
60
- info(`${items.length} page${items.length === 1 ? "" : "s"}`);
61
- });
62
- pages
63
- .command("open <id>")
64
- .description("Print the URL of a page")
65
- .action(async (id) => {
66
- const res = await apiRequest("/api/v1/pages");
67
- if (!res.ok) {
68
- error(res.error);
69
- process.exit(1);
70
- }
71
- const page = res.data.pages.find((p) => p.id === id || p.slug === id);
72
- if (!page) {
73
- error(`Page not found: ${id}`);
74
- process.exit(1);
75
- }
76
- console.log(bold(page.url));
77
- });
78
- pages
79
- .command("delete <id>")
80
- .description("Delete a page by ID")
81
- .option("-y, --yes", "Skip confirmation prompt")
82
- .action(async (id, opts) => {
83
- if (!opts.yes) {
84
- const ok = await confirm(`Delete page ${bold(id)}?`);
85
- if (!ok) {
86
- info("Aborted.");
87
- return;
88
- }
89
- }
90
- const res = await apiRequest(`/api/v1/pages/${id}`, {
91
- method: "DELETE",
92
- });
93
- if (!res.ok) {
94
- error(`Delete failed: ${res.error}`);
95
- process.exit(1);
96
- }
97
- success(`Deleted page ${id}`);
98
- });
99
- }
@@ -1,2 +0,0 @@
1
- import { Command } from "commander";
2
- export declare function registerPublishCommand(program: Command): void;
@@ -1,128 +0,0 @@
1
- import { readFile, watch } from "fs/promises";
2
- import { apiRequest } from "../api.js";
3
- import { success, error, info, warn, bold, dim } from "../fmt.js";
4
- async function readInput(filePath) {
5
- if (filePath === "-") {
6
- // Read from stdin
7
- const chunks = [];
8
- for await (const chunk of process.stdin) {
9
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
10
- }
11
- return Buffer.concat(chunks).toString("utf8");
12
- }
13
- return readFile(filePath, "utf8");
14
- }
15
- async function doPublish(raw, opts, isWatch = false) {
16
- const body = { raw };
17
- if (opts.visibility)
18
- body.settings = { visibility: opts.visibility };
19
- if (opts.update) {
20
- // PATCH existing page
21
- const patchBody = { raw };
22
- if (opts.slug)
23
- patchBody.slug = opts.slug;
24
- if (opts.visibility)
25
- patchBody.visibility = opts.visibility;
26
- const res = await apiRequest(`/api/v1/pages/${opts.update}`, {
27
- method: "PATCH",
28
- body: patchBody,
29
- });
30
- if (!res.ok) {
31
- error(`Update failed: ${res.error}`);
32
- return null;
33
- }
34
- if (isWatch) {
35
- info(`Updated → ${bold(res.data.url)}`);
36
- }
37
- else {
38
- success(`Updated: ${bold(res.data.url)}`);
39
- }
40
- return res.data;
41
- }
42
- // POST new page
43
- const res = await apiRequest("/api/v1/publish", {
44
- method: "POST",
45
- body: { raw },
46
- });
47
- if (!res.ok) {
48
- error(`Publish failed: ${res.error}`);
49
- return null;
50
- }
51
- const { id, url } = res.data;
52
- // Apply post-publish metadata patches
53
- if (opts.slug || opts.visibility) {
54
- const metaPatch = {};
55
- if (opts.slug)
56
- metaPatch.slug = opts.slug;
57
- if (opts.visibility)
58
- metaPatch.visibility = opts.visibility;
59
- const patchRes = await apiRequest(`/api/v1/pages/${id}`, {
60
- method: "PATCH",
61
- body: metaPatch,
62
- });
63
- if (!patchRes.ok) {
64
- warn(`Published but metadata patch failed: ${patchRes.error}`);
65
- success(`Published: ${bold(url)}`);
66
- return { id, url };
67
- }
68
- success(`Published: ${bold(patchRes.data.url)}`);
69
- return { id, url: patchRes.data.url };
70
- }
71
- success(`Published: ${bold(url)}`);
72
- console.log(dim(` ID: ${id}`));
73
- return { id, url };
74
- }
75
- export function registerPublishCommand(program) {
76
- program
77
- .command("publish [file]")
78
- .description("Publish a Markdown file (use - for stdin)")
79
- .option("--slug <slug>", "Set a custom URL slug")
80
- .option("--visibility <v>", "public or unlisted (default: public)", "public")
81
- .option("--update <id>", "Update an existing page by ID instead of creating new")
82
- .option("--watch", "Watch file for changes and re-publish automatically")
83
- .action(async (file, opts) => {
84
- const filePath = file ?? "-";
85
- if (opts.watch && filePath === "-") {
86
- error("--watch cannot be used with stdin.");
87
- process.exit(1);
88
- }
89
- // Initial publish
90
- let raw;
91
- try {
92
- raw = await readInput(filePath);
93
- }
94
- catch (e) {
95
- error(`Cannot read "${filePath}": ${e instanceof Error ? e.message : String(e)}`);
96
- process.exit(1);
97
- }
98
- const result = await doPublish(raw, opts);
99
- if (!result)
100
- process.exit(1);
101
- if (!opts.watch)
102
- return;
103
- // --watch mode: watch file for changes
104
- const pageId = opts.update ?? result.id;
105
- info(`Watching ${bold(filePath)} for changes… (Ctrl+C to stop)`);
106
- console.log();
107
- try {
108
- const watcher = watch(filePath);
109
- for await (const event of watcher) {
110
- if (event.eventType !== "change")
111
- continue;
112
- // Debounce: small delay to let the write finish
113
- await new Promise((r) => setTimeout(r, 80));
114
- try {
115
- const updated = await readFile(filePath, "utf8");
116
- await doPublish(updated, { ...opts, update: pageId }, true);
117
- }
118
- catch (readErr) {
119
- warn(`Read error: ${readErr instanceof Error ? readErr.message : String(readErr)}`);
120
- }
121
- }
122
- }
123
- catch (e) {
124
- error(`Watch failed: ${e instanceof Error ? e.message : String(e)}`);
125
- process.exit(1);
126
- }
127
- });
128
- }
package/dist/config.d.ts DELETED
@@ -1,9 +0,0 @@
1
- export declare const DEFAULT_API_BASE = "https://readable.ashwinsathian.com";
2
- export type Config = {
3
- apiKey?: string;
4
- apiBase: string;
5
- };
6
- export declare function readConfig(): Promise<Config>;
7
- export declare function writeConfig(config: Config): Promise<void>;
8
- export declare function getApiKey(): Promise<string | null>;
9
- export declare function getApiBase(): Promise<string>;
package/dist/config.js DELETED
@@ -1,34 +0,0 @@
1
- import { homedir } from "os";
2
- import { join } from "path";
3
- import { readFile, writeFile, mkdir } from "fs/promises";
4
- const CONFIG_DIR = join(homedir(), ".readable");
5
- const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
- export const DEFAULT_API_BASE = "https://readable.ashwinsathian.com";
7
- export async function readConfig() {
8
- try {
9
- const raw = await readFile(CONFIG_PATH, "utf8");
10
- const parsed = JSON.parse(raw);
11
- return { apiBase: DEFAULT_API_BASE, ...parsed };
12
- }
13
- catch {
14
- return { apiBase: DEFAULT_API_BASE };
15
- }
16
- }
17
- export async function writeConfig(config) {
18
- await mkdir(CONFIG_DIR, { recursive: true });
19
- await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
20
- }
21
- export async function getApiKey() {
22
- const env = process.env.READABLE_API_KEY;
23
- if (env)
24
- return env;
25
- const config = await readConfig();
26
- return config.apiKey ?? null;
27
- }
28
- export async function getApiBase() {
29
- const env = process.env.READABLE_API_URL;
30
- if (env)
31
- return env.replace(/\/$/, "");
32
- const config = await readConfig();
33
- return config.apiBase.replace(/\/$/, "");
34
- }
package/dist/fmt.d.ts DELETED
@@ -1,14 +0,0 @@
1
- export declare const dim: (s: string) => string;
2
- export declare const bold: (s: string) => string;
3
- export declare const green: (s: string) => string;
4
- export declare const red: (s: string) => string;
5
- export declare const yellow: (s: string) => string;
6
- export declare const cyan: (s: string) => string;
7
- export declare const gray: (s: string) => string;
8
- export declare function success(msg: string): void;
9
- export declare function info(msg: string): void;
10
- export declare function warn(msg: string): void;
11
- export declare function error(msg: string): void;
12
- type Row = (string | number | null | undefined)[];
13
- export declare function table(headers: string[], rows: Row[]): void;
14
- export {};
package/dist/fmt.js DELETED
@@ -1,41 +0,0 @@
1
- // ANSI colour helpers — fall back gracefully when NO_COLOR is set
2
- const NO_COLOR = Boolean(process.env.NO_COLOR) || !process.stdout.isTTY;
3
- const c = (code, s) => (NO_COLOR ? s : `\x1b[${code}m${s}\x1b[0m`);
4
- export const dim = (s) => c(2, s);
5
- export const bold = (s) => c(1, s);
6
- export const green = (s) => c(32, s);
7
- export const red = (s) => c(31, s);
8
- export const yellow = (s) => c(33, s);
9
- export const cyan = (s) => c(36, s);
10
- export const gray = (s) => c(90, s);
11
- export function success(msg) {
12
- console.log(`${green("✓")} ${msg}`);
13
- }
14
- export function info(msg) {
15
- console.log(`${cyan("→")} ${msg}`);
16
- }
17
- export function warn(msg) {
18
- console.warn(`${yellow("!")} ${msg}`);
19
- }
20
- export function error(msg) {
21
- console.error(`${red("✗")} ${msg}`);
22
- }
23
- export function table(headers, rows) {
24
- if (rows.length === 0) {
25
- console.log(dim("(no results)"));
26
- return;
27
- }
28
- // Compute column widths
29
- const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] ?? "").length)));
30
- const divider = widths.map((w) => "─".repeat(w + 2)).join("┼");
31
- const pad = (s, w) => s + " ".repeat(Math.max(0, w - s.length));
32
- const headerRow = widths
33
- .map((w, i) => ` ${bold(pad(headers[i], w))} `)
34
- .join("│");
35
- const dataRows = rows.map((row) => widths.map((w, i) => ` ${pad(String(row[i] ?? ""), w)} `).join("│"));
36
- console.log(headerRow);
37
- console.log(dim(divider));
38
- for (const row of dataRows) {
39
- console.log(row);
40
- }
41
- }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};