vibebin 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 (3) hide show
  1. package/README.md +26 -0
  2. package/package.json +34 -0
  3. package/vibebin.js +115 -0
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # Vibebin CLI
2
+
3
+ Publish a single HTML or Markdown artifact to an authenticated Vibebin URL.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install --global vibebin
9
+ ```
10
+
11
+ ## Use
12
+
13
+ ```sh
14
+ vibebin login --api https://vibebin.yeshc.workers.dev
15
+ vibebin publish ./artifact.html --title "Planning artifact"
16
+ vibebin publish ./notes.md --title "Decision notes" --format markdown
17
+ ```
18
+
19
+ Set `VIBEBIN_API` to avoid passing `--api` on each command:
20
+
21
+ ```sh
22
+ export VIBEBIN_API=https://vibebin.yeshc.workers.dev
23
+ ```
24
+
25
+ Publishing prints only the artifact URL by default. Pass `--json` for structured
26
+ output.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "vibebin",
3
+ "version": "0.1.0",
4
+ "description": "Publish HTML and Markdown artifacts to Vibebin",
5
+ "type": "module",
6
+ "bin": {
7
+ "vibebin": "vibebin.js"
8
+ },
9
+ "files": [
10
+ "vibebin.js",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "keywords": [
17
+ "vibebin",
18
+ "markdown",
19
+ "html",
20
+ "publishing",
21
+ "cli"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/yesh0907/vibebin.git",
26
+ "directory": "packages/cli"
27
+ },
28
+ "homepage": "https://github.com/yesh0907/vibebin#readme",
29
+ "bugs": "https://github.com/yesh0907/vibebin/issues",
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }
package/vibebin.js ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { realpathSync } from "node:fs";
4
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
5
+ import { homedir } from "node:os";
6
+ import path from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
10
+ const configDir = path.join(homedir(), ".config", "vibebin");
11
+ const tokenPath = path.join(configDir, "token");
12
+
13
+ if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
14
+ main().catch((error) => {
15
+ console.error(error instanceof Error ? error.message : String(error));
16
+ process.exit(1);
17
+ });
18
+ }
19
+
20
+ async function main() {
21
+ const [command, ...args] = process.argv.slice(2);
22
+ if (command === "publish") return publish(args);
23
+ if (command === "login") return login(args);
24
+ usage();
25
+ }
26
+
27
+ /**
28
+ * @param {string[]} args
29
+ */
30
+ async function login(args) {
31
+ const apiBase = option(args, "--api") ?? process.env.VIBEBIN_API ?? "https://vibebin.dev";
32
+ const response = await fetch(`${apiBase}/api/cli/login`, { method: "POST" });
33
+ if (!response.ok) throw new Error(`Login failed: ${response.status} ${await response.text()}`);
34
+ const payload = await response.json();
35
+ console.error(`Open this URL to approve CLI access:\n${payload.activationUrl}`);
36
+ const deadline = Date.now() + 10 * 60 * 1000;
37
+ while (Date.now() < deadline) {
38
+ await new Promise((resolve) => setTimeout(resolve, 2000));
39
+ const poll = await fetch(`${apiBase}/api/cli/login/${encodeURIComponent(payload.code)}`);
40
+ if (poll.status === 410) throw new Error("Login code expired.");
41
+ const status = await poll.json();
42
+ if (status.status === "approved") {
43
+ await mkdir(configDir, { recursive: true });
44
+ await writeFile(tokenPath, `${status.token}\n`, { mode: 0o600 });
45
+ console.error("Vibebin CLI authenticated.");
46
+ return;
47
+ }
48
+ }
49
+ throw new Error("Timed out waiting for CLI approval.");
50
+ }
51
+
52
+ /**
53
+ * @param {string[]} args
54
+ */
55
+ async function publish(args) {
56
+ const filePath = args.find((arg) => !arg.startsWith("--"));
57
+ if (!filePath) throw new Error("Usage: vibebin publish <file> --title <title>");
58
+ const title = option(args, "--title");
59
+ if (!title) throw new Error("--title is required");
60
+ const description = option(args, "--description") ?? "";
61
+ const kind = inferKind(filePath, option(args, "--format"));
62
+ const apiBase = option(args, "--api") ?? process.env.VIBEBIN_API ?? "https://vibebin.dev";
63
+ const token = process.env.VIBEBIN_TOKEN ?? (await readFile(tokenPath, "utf8").catch(() => "")).trim();
64
+ if (!token) throw new Error("No Vibebin token found. Run `vibebin login` first.");
65
+ const info = await stat(filePath);
66
+ if (!info.isFile()) throw new Error("Refusing to upload anything other than a single file.");
67
+ if (info.size === 0) throw new Error("Refusing to upload an empty file.");
68
+ if (info.size > MAX_UPLOAD_BYTES) throw new Error("Refusing to upload files larger than 10 MB.");
69
+ const bytes = await readFile(filePath);
70
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
71
+ const form = new FormData();
72
+ form.set("file", new Blob([bytes]), path.basename(filePath));
73
+ form.set("title", title);
74
+ form.set("description", description);
75
+ form.set("kind", kind);
76
+ form.set("sha256", sha256);
77
+ const response = await fetch(`${apiBase}/api/artifacts`, { method: "POST", headers: { authorization: `Bearer ${token}` }, body: form });
78
+ if (!response.ok) throw new Error(`Publish failed: ${response.status} ${await response.text()}`);
79
+ const payload = await response.json();
80
+ if (args.includes("--json")) {
81
+ console.log(JSON.stringify({ ...payload, sha256 }));
82
+ } else {
83
+ console.log(payload.url);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * @param {string} filePath
89
+ * @param {string} [override]
90
+ */
91
+ export function inferKind(filePath, override) {
92
+ if (override) {
93
+ if (override === "html" || override === "markdown") return override;
94
+ throw new Error("--format must be html or markdown");
95
+ }
96
+ const ext = path.extname(filePath).toLowerCase();
97
+ if (ext === ".html" || ext === ".htm") return "html";
98
+ if (ext === ".md" || ext === ".markdown") return "markdown";
99
+ throw new Error("Unsupported file extension. Use --format html|markdown to override.");
100
+ }
101
+
102
+ /**
103
+ * @param {string[]} args
104
+ * @param {string} name
105
+ */
106
+ function option(args, name) {
107
+ const index = args.indexOf(name);
108
+ if (index === -1) return undefined;
109
+ return args[index + 1];
110
+ }
111
+
112
+ function usage() {
113
+ console.error("Usage:\n vibebin login [--api URL]\n vibebin publish <file> --title <title> [--description text] [--format html|markdown] [--json] [--api URL]");
114
+ process.exit(1);
115
+ }