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.
- package/README.md +26 -0
- package/package.json +34 -0
- 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
|
+
}
|