pubblue 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.
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import * as fs2 from "fs";
5
+ import * as path2 from "path";
6
+ import { createInterface } from "readline/promises";
7
+ import { Command } from "commander";
8
+
9
+ // src/lib/api.ts
10
+ var PublishApiClient = class {
11
+ constructor(baseUrl, apiKey) {
12
+ this.baseUrl = baseUrl;
13
+ this.apiKey = apiKey;
14
+ }
15
+ async request(path3, options = {}) {
16
+ const url = new URL(path3, this.baseUrl);
17
+ const res = await fetch(url, {
18
+ ...options,
19
+ headers: {
20
+ "Content-Type": "application/json",
21
+ Authorization: `Bearer ${this.apiKey}`,
22
+ ...options.headers
23
+ }
24
+ });
25
+ const data = await res.json();
26
+ if (!res.ok) {
27
+ throw new Error(data.error || `Request failed with status ${res.status}`);
28
+ }
29
+ return data;
30
+ }
31
+ async publish(opts) {
32
+ return this.request("/api/v1/publish", {
33
+ method: "POST",
34
+ body: JSON.stringify(opts)
35
+ });
36
+ }
37
+ async get(slug) {
38
+ const data = await this.request(`/api/v1/publications?slug=${encodeURIComponent(slug)}`);
39
+ return data.publication;
40
+ }
41
+ async list() {
42
+ const data = await this.request("/api/v1/publications");
43
+ return data.publications;
44
+ }
45
+ async update(opts) {
46
+ return this.request("/api/v1/publications", {
47
+ method: "PATCH",
48
+ body: JSON.stringify(opts)
49
+ });
50
+ }
51
+ async remove(slug) {
52
+ await this.request(`/api/v1/publications?slug=${encodeURIComponent(slug)}`, {
53
+ method: "DELETE"
54
+ });
55
+ }
56
+ };
57
+
58
+ // src/lib/config.ts
59
+ import * as fs from "fs";
60
+ import * as os from "os";
61
+ import * as path from "path";
62
+ var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
63
+ function getConfigDir(homeDir) {
64
+ const home = homeDir || os.homedir();
65
+ return path.join(home, ".config", "pubblue");
66
+ }
67
+ function getConfigPath(homeDir) {
68
+ const dir = getConfigDir(homeDir);
69
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
70
+ try {
71
+ fs.chmodSync(dir, 448);
72
+ } catch {
73
+ }
74
+ return path.join(dir, "config.json");
75
+ }
76
+ function loadConfig(homeDir) {
77
+ const configPath = getConfigPath(homeDir);
78
+ if (!fs.existsSync(configPath)) return null;
79
+ const raw = fs.readFileSync(configPath, "utf-8");
80
+ return JSON.parse(raw);
81
+ }
82
+ function saveConfig(config, homeDir) {
83
+ const configPath = getConfigPath(homeDir);
84
+ fs.writeFileSync(configPath, `${JSON.stringify({ apiKey: config.apiKey }, null, 2)}
85
+ `, {
86
+ mode: 384
87
+ });
88
+ try {
89
+ fs.chmodSync(configPath, 384);
90
+ } catch {
91
+ }
92
+ }
93
+ function getConfig(homeDir) {
94
+ const envKey = process.env.PUBBLUE_API_KEY;
95
+ const envUrl = process.env.PUBBLUE_URL;
96
+ const baseUrl = envUrl || DEFAULT_BASE_URL;
97
+ if (envKey) {
98
+ return { apiKey: envKey, baseUrl };
99
+ }
100
+ const saved = loadConfig(homeDir);
101
+ if (!saved) {
102
+ throw new Error(
103
+ "Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
104
+ );
105
+ }
106
+ return {
107
+ apiKey: saved.apiKey,
108
+ baseUrl
109
+ };
110
+ }
111
+
112
+ // src/index.ts
113
+ var program = new Command();
114
+ function createClient() {
115
+ const config = getConfig();
116
+ return new PublishApiClient(config.baseUrl, config.apiKey);
117
+ }
118
+ async function readFromStdin() {
119
+ const chunks = [];
120
+ for await (const chunk of process.stdin) {
121
+ chunks.push(chunk);
122
+ }
123
+ return Buffer.concat(chunks).toString("utf-8").trim();
124
+ }
125
+ function printPublishResult(result) {
126
+ const verb = result.updated ? "Updated" : "Published";
127
+ console.log(`${verb}: ${result.url}`);
128
+ }
129
+ function formatVisibility(isPublic) {
130
+ return isPublic ? "public" : "private";
131
+ }
132
+ async function readApiKeyFromPrompt() {
133
+ const rl = createInterface({
134
+ input: process.stdin,
135
+ output: process.stdout
136
+ });
137
+ try {
138
+ const answer = await rl.question("Enter API key: ");
139
+ return answer.trim();
140
+ } finally {
141
+ rl.close();
142
+ }
143
+ }
144
+ async function resolveConfigureApiKey(opts) {
145
+ if (opts.apiKey && opts.apiKeyStdin) {
146
+ throw new Error("Use only one of --api-key or --api-key-stdin.");
147
+ }
148
+ if (opts.apiKey) {
149
+ return opts.apiKey.trim();
150
+ }
151
+ if (opts.apiKeyStdin) {
152
+ return readFromStdin();
153
+ }
154
+ const envKey = process.env.PUBBLUE_API_KEY?.trim();
155
+ if (envKey) return envKey;
156
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
157
+ throw new Error(
158
+ "No TTY available. Provide --api-key, --api-key-stdin, or PUBBLUE_API_KEY for configure."
159
+ );
160
+ }
161
+ return readApiKeyFromPrompt();
162
+ }
163
+ program.name("pubblue").description("Publish static content and get shareable URLs").version("0.1.0");
164
+ program.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").action(async (opts) => {
165
+ try {
166
+ const apiKey = await resolveConfigureApiKey(opts);
167
+ saveConfig({ apiKey });
168
+ console.log("Configuration saved.");
169
+ } catch (error) {
170
+ const message = error instanceof Error ? error.message : "Failed to configure CLI.";
171
+ console.error(message);
172
+ process.exit(1);
173
+ }
174
+ });
175
+ program.command("publish").description("Publish a file").argument("<file>", "Path to the file to publish").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--private", "Make the publication private").action(async (file, opts) => {
176
+ const client = createClient();
177
+ const filePath = path2.resolve(file);
178
+ if (!fs2.existsSync(filePath)) {
179
+ console.error(`File not found: ${filePath}`);
180
+ process.exit(1);
181
+ }
182
+ const content = fs2.readFileSync(filePath, "utf-8");
183
+ const filename = path2.basename(filePath);
184
+ const result = await client.publish({
185
+ filename,
186
+ content,
187
+ title: opts.title,
188
+ slug: opts.slug,
189
+ isPublic: !opts.private
190
+ });
191
+ printPublishResult(result);
192
+ });
193
+ program.command("publish-content").description("Publish content directly from stdin or argument").requiredOption("--filename <name>", "Filename (determines content type, e.g. page.html)").option("--content <content>", "Content string (if not provided, reads from stdin)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--private", "Make the publication private").action(
194
+ async (opts) => {
195
+ const client = createClient();
196
+ const content = opts.content ?? await readFromStdin();
197
+ const result = await client.publish({
198
+ filename: opts.filename,
199
+ content,
200
+ title: opts.title,
201
+ slug: opts.slug,
202
+ isPublic: !opts.private
203
+ });
204
+ printPublishResult(result);
205
+ }
206
+ );
207
+ program.command("get").description("Get details of a publication").argument("<slug>", "Slug of the publication").action(async (slug) => {
208
+ const client = createClient();
209
+ const pub = await client.get(slug);
210
+ console.log(` Slug: ${pub.slug}`);
211
+ console.log(` File: ${pub.filename}`);
212
+ console.log(` Type: ${pub.contentType}`);
213
+ if (pub.title) console.log(` Title: ${pub.title}`);
214
+ console.log(` Status: ${formatVisibility(pub.isPublic)}`);
215
+ console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
216
+ console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
217
+ console.log(` Size: ${pub.content.length} bytes`);
218
+ });
219
+ program.command("update").description("Update publication metadata").argument("<slug>", "Slug of the publication to update").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").action(async (slug, opts) => {
220
+ const client = createClient();
221
+ let isPublic;
222
+ if (opts.public) isPublic = true;
223
+ else if (opts.private) isPublic = false;
224
+ const result = await client.update({ slug, title: opts.title, isPublic });
225
+ console.log(`Updated: ${result.slug}`);
226
+ if (result.title) console.log(` Title: ${result.title}`);
227
+ console.log(` Status: ${formatVisibility(result.isPublic)}`);
228
+ });
229
+ program.command("list").description("List your publications").action(async () => {
230
+ const client = createClient();
231
+ const pubs = await client.list();
232
+ if (pubs.length === 0) {
233
+ console.log("No publications.");
234
+ return;
235
+ }
236
+ for (const pub of pubs) {
237
+ const date = new Date(pub.createdAt).toLocaleDateString();
238
+ console.log(
239
+ ` ${pub.slug} ${pub.filename} [${pub.contentType}] ${formatVisibility(pub.isPublic)} ${date}`
240
+ );
241
+ }
242
+ });
243
+ program.command("delete").description("Delete a publication").argument("<slug>", "Slug of the publication to delete").action(async (slug) => {
244
+ const client = createClient();
245
+ await client.remove(slug);
246
+ console.log(`Deleted: ${slug}`);
247
+ });
248
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "pubblue",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool for publishing static content via pub.blue",
5
+ "type": "module",
6
+ "bin": {
7
+ "pubblue": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup src/index.ts --format esm --dts --clean",
11
+ "dev": "tsup src/index.ts --format esm --watch",
12
+ "test": "vitest run",
13
+ "test:watch": "vitest",
14
+ "lint": "tsc --noEmit"
15
+ },
16
+ "dependencies": {
17
+ "commander": "^13.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@types/node": "22.10.2",
21
+ "tsup": "^8.3.6",
22
+ "typescript": "^5.7.2",
23
+ "vitest": "^3.0.0"
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "pnpm": {
32
+ "onlyBuiltDependencies": [
33
+ "esbuild"
34
+ ]
35
+ }
36
+ }