Skip to content

Commit d03a2c9

Browse files
just-be-devclaude
andcommitted
feat(micro): restore micro CLI with raw stdin/stdout approach
Replace the interactive @clack/prompts TUI with a plain CLI: - micro list - print all posts to stdout - micro post "text" - post directly from argument - micro post - read content from stdin - micro delete <id> - delete a post - --branch / -b flag still supported for targeting preview deploys No dependencies — just fetch + process.stdin/stdout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0a194b4 commit d03a2c9

File tree

6 files changed

+226
-0
lines changed

6 files changed

+226
-0
lines changed

bun.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mise.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,11 @@ description = "Deploy wildcard subdomain service"
7777
run = "uv run --with openai-whisper python scripts/transcribe.py"
7878
description = "Generate word-level transcript from audio/video"
7979

80+
[tasks.micro]
81+
run = "bun packages/micro/index.ts"
82+
description = "Micro blog CLI (list posts, create, or delete)"
83+
raw = true
84+
8085
[tasks."changelog:entry"]
8186
run = "bun scripts/changelog-entry.ts"
8287
description = "Generate changelog entry for a PR"

packages/micro/index.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env bun
2+
3+
import { deletePost, list } from "./src/browse.ts";
4+
import { post } from "./src/post.ts";
5+
6+
const PRODUCTION_URL = "https://just-be.dev";
7+
const WORKER_NAME = "just-be-dev";
8+
const WORKERS_DEV_SUBDOMAIN = "just-be";
9+
10+
function siteUrlFromBranch(branch: string | undefined): string {
11+
if (!branch || branch === "main") return PRODUCTION_URL;
12+
const sanitized = branch
13+
.toLowerCase()
14+
.replace(/[^a-z0-9-]+/g, "-")
15+
.replace(/^-+|-+$/g, "");
16+
return `https://${sanitized}-${WORKER_NAME}.${WORKERS_DEV_SUBDOMAIN}.workers.dev`;
17+
}
18+
19+
const rawArgs = process.argv.slice(2);
20+
let branch: string | undefined;
21+
const args: string[] = [];
22+
23+
for (let i = 0; i < rawArgs.length; i++) {
24+
if ((rawArgs[i] === "--branch" || rawArgs[i] === "-b") && i + 1 < rawArgs.length) {
25+
branch = rawArgs[++i];
26+
} else {
27+
args.push(rawArgs[i]);
28+
}
29+
}
30+
31+
const command = args[0];
32+
const siteUrl = siteUrlFromBranch(branch);
33+
34+
async function main() {
35+
if (!command || command === "list") {
36+
await list(siteUrl);
37+
} else if (command === "post") {
38+
await post(args[1], siteUrl);
39+
} else if (command === "delete") {
40+
const id = Number(args[1]);
41+
if (!args[1] || !Number.isFinite(id)) {
42+
console.error("Usage: micro delete <id>");
43+
process.exit(1);
44+
}
45+
await deletePost(id, siteUrl);
46+
} else {
47+
console.error(`Unknown command: ${command}`);
48+
console.log("Usage:");
49+
console.log(" micro [list] [--branch <name>] - List all posts");
50+
console.log(
51+
' micro post [--branch <name>] ["text"] - Create a post (reads stdin if no text)',
52+
);
53+
console.log(" micro delete [--branch <name>] <id> - Delete a post");
54+
process.exit(1);
55+
}
56+
}
57+
58+
main().catch((error) => {
59+
console.error("Error:", error.message);
60+
process.exit(1);
61+
});

packages/micro/package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@just-be/micro",
3+
"version": "0.1.0",
4+
"description": "CLI tool for creating and managing micro blog posts",
5+
"keywords": [
6+
"cli",
7+
"microblog"
8+
],
9+
"license": "MIT",
10+
"author": "Justin Bennett",
11+
"repository": {
12+
"type": "git",
13+
"url": "git+https://github.com/just-be-dev/just-be.dev.git",
14+
"directory": "packages/micro"
15+
},
16+
"bin": {
17+
"micro": "index.ts"
18+
},
19+
"files": [
20+
"index.ts",
21+
"src"
22+
],
23+
"type": "module",
24+
"exports": {
25+
".": "./index.ts"
26+
},
27+
"publishConfig": {
28+
"access": "public"
29+
},
30+
"engines": {
31+
"bun": ">=1.0.0"
32+
}
33+
}

packages/micro/src/browse.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
interface Post {
2+
id: number;
3+
content: string;
4+
createdAt: string;
5+
syndicatedTo: Array<{ platform: string; id: string; url: string }>;
6+
}
7+
8+
function getSecret(): string {
9+
const secret = process.env.MICRO_SECRET;
10+
if (!secret) {
11+
console.error("Error: MICRO_SECRET environment variable not set");
12+
process.exit(1);
13+
}
14+
return secret;
15+
}
16+
17+
export async function list(siteUrl: string) {
18+
const secret = getSecret();
19+
20+
const res = await fetch(`${siteUrl}/micro`, {
21+
headers: {
22+
Authorization: `Bearer ${secret}`,
23+
Accept: "application/json",
24+
},
25+
});
26+
27+
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
28+
29+
const posts = (await res.json()) as Post[];
30+
31+
if (posts.length === 0) {
32+
console.log("no posts yet");
33+
return;
34+
}
35+
36+
for (const post of posts) {
37+
const date = new Date(post.createdAt).toLocaleDateString();
38+
console.log(`[${post.id}] ${date} ${post.content}`);
39+
if (post.syndicatedTo.length > 0) {
40+
console.log(` syndicated: ${post.syndicatedTo.map((s) => s.platform).join(", ")}`);
41+
}
42+
}
43+
}
44+
45+
export async function deletePost(id: number, siteUrl: string) {
46+
const secret = getSecret();
47+
48+
const res = await fetch(`${siteUrl}/micro/${id}`, {
49+
method: "DELETE",
50+
headers: { Authorization: `Bearer ${secret}` },
51+
});
52+
53+
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
54+
55+
console.log(`deleted #${id}`);
56+
}

packages/micro/src/post.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
const MAX_LENGTH = 280;
2+
3+
function getSecret(): string {
4+
const secret = process.env.MICRO_SECRET;
5+
if (!secret) {
6+
console.error("Error: MICRO_SECRET environment variable not set");
7+
process.exit(1);
8+
}
9+
return secret;
10+
}
11+
12+
export async function post(content: string | undefined, siteUrl: string) {
13+
let postContent = content;
14+
15+
if (!postContent) {
16+
const chunks: Buffer[] = [];
17+
for await (const chunk of process.stdin) {
18+
chunks.push(chunk as Buffer);
19+
}
20+
postContent = Buffer.concat(chunks).toString("utf8").trim();
21+
}
22+
23+
if (!postContent) {
24+
console.error("Error: no content provided (pass as argument or pipe via stdin)");
25+
process.exit(1);
26+
}
27+
28+
if (postContent.length > MAX_LENGTH) {
29+
console.error(`Error: too long (${postContent.length}/${MAX_LENGTH} characters)`);
30+
process.exit(1);
31+
}
32+
33+
const secret = getSecret();
34+
35+
const res = await fetch(`${siteUrl}/micro`, {
36+
method: "POST",
37+
headers: {
38+
"Content-Type": "application/json",
39+
Authorization: `Bearer ${secret}`,
40+
},
41+
body: JSON.stringify({ content: postContent }),
42+
});
43+
44+
if (!res.ok) {
45+
throw new Error(`${res.status}: ${await res.text()}`);
46+
}
47+
48+
const { post: newPost, syndication } = (await res.json()) as {
49+
post: { id: number; content: string; createdAt: string };
50+
syndication: Array<{ platform: string; success: boolean; url?: string; error?: string }>;
51+
};
52+
53+
console.log(`posted #${newPost.id}`);
54+
55+
for (const s of syndication) {
56+
if (s.success) {
57+
console.log(` ${s.platform}: ${s.url}`);
58+
} else {
59+
console.error(` ${s.platform}: ${s.error}`);
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)