vibex-app 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 +44 -0
- package/dist/api.js +69 -0
- package/dist/build.js +132 -0
- package/dist/config.js +33 -0
- package/dist/index.js +95 -0
- package/dist/login.js +40 -0
- package/dist/types.js +3 -0
- package/dist/ui.js +48 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# vibex CLI
|
|
2
|
+
|
|
3
|
+
Vibex from your terminal — describe an app, a Coder + Reviewer AI pair builds it, and the
|
|
4
|
+
files land in a local folder. Same account, plans, and dashboard as the web app.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npm i -g vibex-app
|
|
8
|
+
|
|
9
|
+
vibex login # paste a token from Settings → CLI access
|
|
10
|
+
vibex build "a habit tracker with streaks"
|
|
11
|
+
vibex build "a trading journal" --dir ./journal
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> npm package is `vibex-app` (the bare `vibex` name is squatted and `vibex-cli` belongs to an
|
|
15
|
+
> unrelated project) — the installed command is still `vibex`.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | What it does |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `vibex login [--token vx_…] [--url …]` | Verify + store your access token in `~/.vibex` |
|
|
22
|
+
| `vibex build "<idea>"` | Create a project, stream the build, write files locally |
|
|
23
|
+
| `vibex build --project <id>` | Rebuild / resume an existing project |
|
|
24
|
+
| `vibex whoami` | Show the connected account + plan |
|
|
25
|
+
| `vibex logout` | Forget the stored token |
|
|
26
|
+
|
|
27
|
+
Build flags: `--dir <path>` · `--platform web|api|cli` · `--steer "<correction>"` · `--force`
|
|
28
|
+
|
|
29
|
+
## Notes
|
|
30
|
+
|
|
31
|
+
- Tokens are minted (and revoked) in the web app under **Settings → CLI access**; only a hash
|
|
32
|
+
is stored server-side.
|
|
33
|
+
- Builds stream over the same API the web workspace uses — usage counts against your plan and
|
|
34
|
+
every run appears on your dashboard.
|
|
35
|
+
- `VIBEX_API_URL` / `VIBEX_TOKEN` env vars override the stored config (useful in CI).
|
|
36
|
+
|
|
37
|
+
## Develop
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
cd cli
|
|
41
|
+
npm install
|
|
42
|
+
npm run build # tsc → dist/
|
|
43
|
+
node dist/index.js --help
|
|
44
|
+
```
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Thin client of the Vibex backend — the same endpoints the web app uses, authenticated with
|
|
2
|
+
// a bearer token instead of a session cookie.
|
|
3
|
+
export class ApiError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function headers(token) {
|
|
11
|
+
return { "content-type": "application/json", authorization: `Bearer ${token}` };
|
|
12
|
+
}
|
|
13
|
+
export async function apiMe(baseUrl, token) {
|
|
14
|
+
const r = await fetch(new URL("/api/me", baseUrl), { headers: headers(token) });
|
|
15
|
+
const j = (await r.json().catch(() => null));
|
|
16
|
+
if (!r.ok || !j?.ok) {
|
|
17
|
+
throw new ApiError(j?.error === "invalid_token" ? "Invalid or revoked token — mint a new one in Settings → CLI access." : `API error (${r.status})`, r.status);
|
|
18
|
+
}
|
|
19
|
+
return { user: j.user, plan: j.plan ?? "free" };
|
|
20
|
+
}
|
|
21
|
+
export async function createProject(baseUrl, token, spec) {
|
|
22
|
+
const r = await fetch(new URL("/api/projects", baseUrl), {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: headers(token),
|
|
25
|
+
body: JSON.stringify({ spec }),
|
|
26
|
+
});
|
|
27
|
+
const j = (await r.json().catch(() => null));
|
|
28
|
+
if (!r.ok || !j?.ok || !j.id) {
|
|
29
|
+
throw new ApiError(j?.message ?? (j?.error === "invalid_token" ? "Invalid token — run `vibex login`." : `Couldn't create the project (${r.status}).`), r.status);
|
|
30
|
+
}
|
|
31
|
+
return { id: j.id, title: j.title ?? "" };
|
|
32
|
+
}
|
|
33
|
+
// Stream a build for an owned project. Yields the server's SSE events as they arrive.
|
|
34
|
+
export async function* streamRun(baseUrl, token, projectId, opts) {
|
|
35
|
+
const r = await fetch(new URL("/api/run", baseUrl), {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: headers(token),
|
|
38
|
+
body: JSON.stringify({ spec: {}, projectId, steer: opts?.steer, startIndex: opts?.startIndex ?? 0 }),
|
|
39
|
+
});
|
|
40
|
+
if (r.status === 409 || r.status === 429) {
|
|
41
|
+
const j = (await r.json().catch(() => null));
|
|
42
|
+
throw new ApiError(j?.message ?? (r.status === 429 ? "Rate limit reached — wait a moment, then retry." : "Another build is already running."), r.status);
|
|
43
|
+
}
|
|
44
|
+
if (!r.ok || !r.body)
|
|
45
|
+
throw new ApiError(`API error (${r.status}).`, r.status);
|
|
46
|
+
const reader = r.body.getReader();
|
|
47
|
+
const dec = new TextDecoder();
|
|
48
|
+
let buf = "";
|
|
49
|
+
for (;;) {
|
|
50
|
+
const { value, done } = await reader.read();
|
|
51
|
+
if (done)
|
|
52
|
+
break;
|
|
53
|
+
buf += dec.decode(value, { stream: true });
|
|
54
|
+
let idx;
|
|
55
|
+
while ((idx = buf.indexOf("\n\n")) !== -1) {
|
|
56
|
+
const block = buf.slice(0, idx);
|
|
57
|
+
buf = buf.slice(idx + 2);
|
|
58
|
+
const line = block.split("\n").find((l) => l.startsWith("data:"));
|
|
59
|
+
if (!line)
|
|
60
|
+
continue;
|
|
61
|
+
try {
|
|
62
|
+
yield JSON.parse(line.slice(5).trim());
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* skip malformed frame */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// vibex build — create (or reuse) a project, stream the Coder+Reviewer build, and write the
|
|
2
|
+
// generated files into a local folder. The stream is the same SSE feed the web workspace uses.
|
|
3
|
+
import { mkdirSync, readdirSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import pc from "picocolors";
|
|
6
|
+
import { createProject, streamRun } from "./api.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { banner, info, stepLine, coderLine, reviewerLine, done, fileList, fail, warn, link } from "./ui.js";
|
|
9
|
+
function slugify(idea) {
|
|
10
|
+
return (idea
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
13
|
+
.replace(/(^-+|-+$)/g, "")
|
|
14
|
+
.slice(0, 40) || "vibex-app");
|
|
15
|
+
}
|
|
16
|
+
// Generated paths are model output — never let one escape the target folder.
|
|
17
|
+
function safeJoin(root, rel) {
|
|
18
|
+
if (path.isAbsolute(rel) || /^[a-zA-Z]:/.test(rel))
|
|
19
|
+
return null;
|
|
20
|
+
const norm = path.normalize(rel);
|
|
21
|
+
if (norm.startsWith("..") || norm.includes(`..${path.sep}`))
|
|
22
|
+
return null;
|
|
23
|
+
return path.join(root, norm);
|
|
24
|
+
}
|
|
25
|
+
function writeFiles(dir, files) {
|
|
26
|
+
const written = [];
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
const target = safeJoin(dir, f.path);
|
|
29
|
+
if (!target) {
|
|
30
|
+
warn(`skipped unsafe path: ${f.path}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
34
|
+
writeFileSync(target, f.content, "utf8");
|
|
35
|
+
written.push(f.path);
|
|
36
|
+
}
|
|
37
|
+
return written;
|
|
38
|
+
}
|
|
39
|
+
export async function runBuild(idea, opts) {
|
|
40
|
+
const cfg = loadConfig();
|
|
41
|
+
if (!cfg.token) {
|
|
42
|
+
fail("Not signed in — run `vibex login` first (token: Settings → CLI access on the web app).");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (!idea.trim() && !opts.project) {
|
|
46
|
+
fail('Tell it what to build: vibex build "a habit tracker with streaks"');
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const dir = path.resolve(opts.dir ?? `./${slugify(idea || opts.project || "vibex-app")}`);
|
|
50
|
+
if (existsSync(dir) && readdirSync(dir).length > 0 && !opts.force) {
|
|
51
|
+
fail(`${dir} isn't empty — pick another --dir or pass --force to write into it.`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// 1) Project row (owned, so the run persists and shows on the web dashboard).
|
|
55
|
+
let projectId = opts.project;
|
|
56
|
+
let title = idea.trim();
|
|
57
|
+
if (!projectId) {
|
|
58
|
+
const p = await createProject(cfg.baseUrl, cfg.token, { idea: idea.trim(), platform: opts.platform ?? "web" });
|
|
59
|
+
projectId = p.id;
|
|
60
|
+
title = p.title || title;
|
|
61
|
+
}
|
|
62
|
+
banner(`Building ${pc.yellow(`"${title || projectId}"`)}`);
|
|
63
|
+
info(`server: ${cfg.baseUrl} · project: ${projectId}`);
|
|
64
|
+
// 2) Stream the build.
|
|
65
|
+
let stepsTotal = 0;
|
|
66
|
+
let files = [];
|
|
67
|
+
let tokens = 0;
|
|
68
|
+
let cost = 0;
|
|
69
|
+
let live = true;
|
|
70
|
+
let paused = null;
|
|
71
|
+
let errored = null;
|
|
72
|
+
for await (const ev of streamRun(cfg.baseUrl, cfg.token, projectId, { steer: opts.steer })) {
|
|
73
|
+
switch (ev.type) {
|
|
74
|
+
case "planned":
|
|
75
|
+
stepsTotal = ev.steps.length;
|
|
76
|
+
live = ev.live;
|
|
77
|
+
info(`planned ${ev.steps.length} steps${ev.live ? "" : " (simulated — no model key on the server)"}`);
|
|
78
|
+
break;
|
|
79
|
+
case "step_start":
|
|
80
|
+
stepLine(ev.index + 1, stepsTotal || ev.index + 1, ev.title);
|
|
81
|
+
break;
|
|
82
|
+
case "coder":
|
|
83
|
+
if (ev.path)
|
|
84
|
+
coderLine(ev.path, ev.tokens, ev.stub);
|
|
85
|
+
break;
|
|
86
|
+
case "reviewer":
|
|
87
|
+
reviewerLine(ev.verdict, ev.note);
|
|
88
|
+
break;
|
|
89
|
+
case "usage":
|
|
90
|
+
tokens = ev.tokens;
|
|
91
|
+
cost = ev.cost;
|
|
92
|
+
break;
|
|
93
|
+
case "paused":
|
|
94
|
+
paused = `${ev.reason} — progress is saved; resume with: vibex build --project ${projectId}`;
|
|
95
|
+
break;
|
|
96
|
+
case "complete":
|
|
97
|
+
files = ev.files ?? [];
|
|
98
|
+
tokens = ev.tokens;
|
|
99
|
+
cost = ev.cost;
|
|
100
|
+
break;
|
|
101
|
+
case "error":
|
|
102
|
+
errored = ev.message;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (errored) {
|
|
109
|
+
fail(`The engine hit an error: ${errored}`);
|
|
110
|
+
info(`retry from the last completed step: vibex build --project ${projectId}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
if (paused) {
|
|
114
|
+
warn(paused);
|
|
115
|
+
process.exit(2);
|
|
116
|
+
}
|
|
117
|
+
if (!files.length) {
|
|
118
|
+
fail("The build finished without producing files — check the project on the web dashboard.");
|
|
119
|
+
link("dashboard:", `${cfg.baseUrl}/result?project=${projectId}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
// 3) Land the files.
|
|
123
|
+
mkdirSync(dir, { recursive: true });
|
|
124
|
+
const written = writeFiles(dir, files);
|
|
125
|
+
done(`Build complete ${pc.dim(`— ${(tokens / 1000).toFixed(1)}k tokens · ~$${cost.toFixed(2)}${live ? "" : " · simulated"}`)}`);
|
|
126
|
+
fileList(written, dir);
|
|
127
|
+
for (const p of written)
|
|
128
|
+
console.log(` ${pc.dim("+")} ${p}`);
|
|
129
|
+
link("web view:", `${cfg.baseUrl}/result?project=${projectId}`);
|
|
130
|
+
if (written.includes("index.html"))
|
|
131
|
+
info(`open ${path.join(dir, "index.html")} in a browser to run it`);
|
|
132
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ~/.vibex/config.json — base URL + access token. Env vars override for CI:
|
|
2
|
+
// VIBEX_API_URL, VIBEX_TOKEN.
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
export const DEFAULT_BASE_URL = "https://vibe-x-liart.vercel.app";
|
|
7
|
+
const DIR = join(homedir(), ".vibex");
|
|
8
|
+
const FILE = join(DIR, "config.json");
|
|
9
|
+
export function loadConfig() {
|
|
10
|
+
let file = {};
|
|
11
|
+
try {
|
|
12
|
+
file = JSON.parse(readFileSync(FILE, "utf8"));
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
/* first run */
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
baseUrl: process.env.VIBEX_API_URL || file.baseUrl || DEFAULT_BASE_URL,
|
|
19
|
+
token: process.env.VIBEX_TOKEN || file.token,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function saveConfig(cfg) {
|
|
23
|
+
mkdirSync(DIR, { recursive: true });
|
|
24
|
+
writeFileSync(FILE, JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
25
|
+
}
|
|
26
|
+
export function clearConfig() {
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(FILE, "{}\n", "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* nothing to clear */
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// vibex — Vibex from your terminal. `vibex login` with a token from Settings → CLI access,
|
|
3
|
+
// then `vibex build "an idea"` streams the Coder+Reviewer build and writes real files into a
|
|
4
|
+
// local folder. Thin client of the Vibex backend: plans, limits, and dashboard all apply.
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import { runLogin, runLogout, runWhoami } from "./login.js";
|
|
8
|
+
import { runBuild } from "./build.js";
|
|
9
|
+
import { fail } from "./ui.js";
|
|
10
|
+
import { ApiError } from "./api.js";
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const VERSION = require("../package.json").version;
|
|
13
|
+
const HELP = `
|
|
14
|
+
${pc.bold("vibex")} — from idea to code, in your terminal
|
|
15
|
+
|
|
16
|
+
${pc.bold("Usage")}
|
|
17
|
+
vibex login [--token vx_…] [--url https://…] connect your account
|
|
18
|
+
vibex build "<idea>" [options] build an app into a local folder
|
|
19
|
+
vibex whoami show the signed-in account
|
|
20
|
+
vibex logout forget the stored token
|
|
21
|
+
|
|
22
|
+
${pc.bold("Build options")}
|
|
23
|
+
--dir <path> target folder (default: ./<idea-slug>)
|
|
24
|
+
--platform <p> web | api | cli (default: web)
|
|
25
|
+
--project <id> rebuild an existing project instead of creating one
|
|
26
|
+
--steer "<note>" fold a correction into the build
|
|
27
|
+
--force write into a non-empty folder
|
|
28
|
+
|
|
29
|
+
${pc.bold("Examples")}
|
|
30
|
+
vibex build "a habit tracker with streaks"
|
|
31
|
+
vibex build "a trading journal" --dir ./journal --force
|
|
32
|
+
|
|
33
|
+
Tokens are minted in ${pc.underline("Settings → CLI access")} on the web app.
|
|
34
|
+
`;
|
|
35
|
+
function parseArgs(argv) {
|
|
36
|
+
const [cmd = "help", ...rest] = argv;
|
|
37
|
+
const positional = [];
|
|
38
|
+
const flags = {};
|
|
39
|
+
for (let i = 0; i < rest.length; i++) {
|
|
40
|
+
const a = rest[i];
|
|
41
|
+
if (a.startsWith("--")) {
|
|
42
|
+
const key = a.slice(2);
|
|
43
|
+
const next = rest[i + 1];
|
|
44
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
45
|
+
flags[key] = next;
|
|
46
|
+
i++;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
flags[key] = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
positional.push(a);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { cmd, positional, flags };
|
|
57
|
+
}
|
|
58
|
+
async function main() {
|
|
59
|
+
const { cmd, positional, flags } = parseArgs(process.argv.slice(2));
|
|
60
|
+
switch (cmd) {
|
|
61
|
+
case "login":
|
|
62
|
+
await runLogin({ token: typeof flags.token === "string" ? flags.token : undefined, url: typeof flags.url === "string" ? flags.url : undefined });
|
|
63
|
+
break;
|
|
64
|
+
case "logout":
|
|
65
|
+
runLogout();
|
|
66
|
+
break;
|
|
67
|
+
case "whoami":
|
|
68
|
+
await runWhoami();
|
|
69
|
+
break;
|
|
70
|
+
case "build":
|
|
71
|
+
await runBuild(positional.join(" "), {
|
|
72
|
+
dir: typeof flags.dir === "string" ? flags.dir : undefined,
|
|
73
|
+
platform: typeof flags.platform === "string" ? flags.platform : undefined,
|
|
74
|
+
project: typeof flags.project === "string" ? flags.project : undefined,
|
|
75
|
+
steer: typeof flags.steer === "string" ? flags.steer : undefined,
|
|
76
|
+
force: flags.force === true,
|
|
77
|
+
});
|
|
78
|
+
break;
|
|
79
|
+
case "--version":
|
|
80
|
+
case "-v":
|
|
81
|
+
case "version":
|
|
82
|
+
console.log(VERSION);
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
console.log(HELP);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
main().catch((e) => {
|
|
90
|
+
if (e instanceof ApiError)
|
|
91
|
+
fail(e.message);
|
|
92
|
+
else
|
|
93
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
package/dist/login.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// login / logout / whoami — token paste flow. The token is minted once in the web app
|
|
2
|
+
// (Settings → CLI access) and stored in ~/.vibex/config.json (0600).
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { apiMe } from "./api.js";
|
|
6
|
+
import { loadConfig, saveConfig, clearConfig, DEFAULT_BASE_URL } from "./config.js";
|
|
7
|
+
import { banner, info, done, fail } from "./ui.js";
|
|
8
|
+
export async function runLogin(opts) {
|
|
9
|
+
const baseUrl = opts.url || loadConfig().baseUrl || DEFAULT_BASE_URL;
|
|
10
|
+
banner("Connect your Vibex account");
|
|
11
|
+
info(`server: ${baseUrl}`);
|
|
12
|
+
info(`mint a token at ${baseUrl}/settings (CLI access), then paste it below`);
|
|
13
|
+
let token = opts.token?.trim();
|
|
14
|
+
if (!token) {
|
|
15
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
token = (await rl.question(`\n ${pc.bold("token")} ${pc.dim("(vx_…)")}: `)).trim();
|
|
17
|
+
rl.close();
|
|
18
|
+
}
|
|
19
|
+
if (!/^vx_[a-f0-9]{48}$/.test(token)) {
|
|
20
|
+
fail("That doesn't look like a Vibex token (expected vx_ + 48 hex characters).");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
const me = await apiMe(baseUrl, token);
|
|
24
|
+
saveConfig({ baseUrl, token });
|
|
25
|
+
done(`Signed in as ${me.user.email ?? me.user.name ?? "your account"} (${me.plan} plan)`);
|
|
26
|
+
info("try: vibex build \"a habit tracker with streaks\"");
|
|
27
|
+
}
|
|
28
|
+
export function runLogout() {
|
|
29
|
+
clearConfig();
|
|
30
|
+
done("Token forgotten. `vibex login` to reconnect.");
|
|
31
|
+
}
|
|
32
|
+
export async function runWhoami() {
|
|
33
|
+
const cfg = loadConfig();
|
|
34
|
+
if (!cfg.token) {
|
|
35
|
+
fail("Not signed in — run `vibex login` first.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const me = await apiMe(cfg.baseUrl, cfg.token);
|
|
39
|
+
console.log(`${me.user.email ?? me.user.name ?? "unknown"} ${pc.dim(`· ${me.plan} plan · ${cfg.baseUrl}`)}`);
|
|
40
|
+
}
|
package/dist/types.js
ADDED
package/dist/ui.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Terminal rendering — small, deliberate, no spinner theater. Events stream steadily, so each
|
|
2
|
+
// one prints a line; color carries the role coding (coder = orange-ish, reviewer = green),
|
|
3
|
+
// mirroring the web app's conversation feed.
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
export const sym = {
|
|
6
|
+
mark: pc.yellow("◆"),
|
|
7
|
+
ok: pc.green("✔"),
|
|
8
|
+
warn: pc.yellow("▲"),
|
|
9
|
+
fail: pc.red("✖"),
|
|
10
|
+
pen: pc.yellow("✎"),
|
|
11
|
+
dot: pc.dim("·"),
|
|
12
|
+
};
|
|
13
|
+
export function banner(text) {
|
|
14
|
+
console.log(`\n${sym.mark} ${pc.bold(text)}`);
|
|
15
|
+
}
|
|
16
|
+
export function info(text) {
|
|
17
|
+
console.log(` ${pc.dim(text)}`);
|
|
18
|
+
}
|
|
19
|
+
export function stepLine(n, of, title) {
|
|
20
|
+
console.log(`\n${pc.dim(`${String(n).padStart(2)}/${of}`)} ${pc.bold(title)}`);
|
|
21
|
+
}
|
|
22
|
+
export function coderLine(path, tokens, stub) {
|
|
23
|
+
if (stub) {
|
|
24
|
+
console.log(` ${sym.warn} ${pc.yellow(`placeholder for ${path}`)} ${pc.dim("(model unavailable)")}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(` ${sym.pen} wrote ${pc.bold(path)} ${pc.dim(`(${(tokens / 1000).toFixed(1)}k tok)`)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function reviewerLine(verdict, note) {
|
|
31
|
+
const chip = verdict === "pass" ? pc.green("PASS") : pc.yellow("REVISE");
|
|
32
|
+
console.log(` ${chip} ${note}`);
|
|
33
|
+
}
|
|
34
|
+
export function done(text) {
|
|
35
|
+
console.log(`\n${sym.ok} ${pc.bold(text)}`);
|
|
36
|
+
}
|
|
37
|
+
export function fileList(paths, dir) {
|
|
38
|
+
console.log(` ${pc.dim("→")} ${paths.length} file${paths.length === 1 ? "" : "s"} written to ${pc.bold(dir)}`);
|
|
39
|
+
}
|
|
40
|
+
export function fail(text) {
|
|
41
|
+
console.error(`\n${sym.fail} ${pc.red(text)}`);
|
|
42
|
+
}
|
|
43
|
+
export function warn(text) {
|
|
44
|
+
console.log(`${sym.warn} ${pc.yellow(text)}`);
|
|
45
|
+
}
|
|
46
|
+
export function link(label, url) {
|
|
47
|
+
console.log(` ${pc.dim(label)} ${pc.underline(pc.cyan(url))}`);
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibex-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vibex from your terminal — describe an app, a Coder + Reviewer AI pair builds it into a local folder.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"vibex": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.17"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"prepublishOnly": "tsc"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"picocolors": "^1.1.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^20.14.0",
|
|
27
|
+
"typescript": "^5.5.0"
|
|
28
|
+
}
|
|
29
|
+
}
|