@vibiz/cli 0.0.0-stage → 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 +36 -2
- package/dist/chunk-NWL4LVVI.js +1015 -0
- package/dist/src-BOTTM5XC.js +2230 -0
- package/dist/vibiz.js +3302 -0
- package/package.json +37 -4
- package/templates/nextjs-saas/.env.example +13 -0
- package/templates/nextjs-saas/README.md +106 -0
- package/templates/nextjs-saas/_gitignore +8 -0
- package/templates/nextjs-saas/_npmrc +1 -0
- package/templates/nextjs-saas/app/api/auth/[...path]/route.ts +30 -0
- package/templates/nextjs-saas/app/api/chat/route.ts +76 -0
- package/templates/nextjs-saas/app/dashboard/actions.ts +32 -0
- package/templates/nextjs-saas/app/dashboard/page.tsx +130 -0
- package/templates/nextjs-saas/app/globals.css +17 -0
- package/templates/nextjs-saas/app/layout.tsx +28 -0
- package/templates/nextjs-saas/app/not-found.tsx +13 -0
- package/templates/nextjs-saas/app/page.tsx +63 -0
- package/templates/nextjs-saas/app/sign-in/page.tsx +8 -0
- package/templates/nextjs-saas/app/sign-up/page.tsx +8 -0
- package/templates/nextjs-saas/app.config.ts +15 -0
- package/templates/nextjs-saas/components/analytics.tsx +20 -0
- package/templates/nextjs-saas/components/auth-form.tsx +144 -0
- package/templates/nextjs-saas/components/auth-page.tsx +30 -0
- package/templates/nextjs-saas/components/chat-box.tsx +95 -0
- package/templates/nextjs-saas/components/header.tsx +24 -0
- package/templates/nextjs-saas/components/not-configured.tsx +17 -0
- package/templates/nextjs-saas/components/replay-consent.tsx +75 -0
- package/templates/nextjs-saas/components/replay-identify.tsx +21 -0
- package/templates/nextjs-saas/components/sign-out-button.tsx +25 -0
- package/templates/nextjs-saas/components/ui.tsx +47 -0
- package/templates/nextjs-saas/db/migrate.ts +21 -0
- package/templates/nextjs-saas/db/schema.ts +18 -0
- package/templates/nextjs-saas/lib/auth/client.ts +6 -0
- package/templates/nextjs-saas/lib/auth/server.ts +44 -0
- package/templates/nextjs-saas/lib/db.ts +32 -0
- package/templates/nextjs-saas/lib/env.ts +36 -0
- package/templates/nextjs-saas/lib/replay.ts +55 -0
- package/templates/nextjs-saas/next.config.ts +9 -0
- package/templates/nextjs-saas/package-lock.json +5359 -0
- package/templates/nextjs-saas/package.json +32 -0
- package/templates/nextjs-saas/postcss.config.mjs +3 -0
- package/templates/nextjs-saas/proxy.ts +18 -0
- package/templates/nextjs-saas/tsconfig.json +21 -0
package/dist/vibiz.js
ADDED
|
@@ -0,0 +1,3302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
Vibiz,
|
|
4
|
+
VibizApiError,
|
|
5
|
+
deployDirectory,
|
|
6
|
+
formatUnits,
|
|
7
|
+
formatUsd,
|
|
8
|
+
mergeEnvFile
|
|
9
|
+
} from "./chunk-NWL4LVVI.js";
|
|
10
|
+
|
|
11
|
+
// packages/cli/src/main.ts
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { chmod, mkdir, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
14
|
+
import { format } from "node:util";
|
|
15
|
+
|
|
16
|
+
// packages/cli/src/commands.ts
|
|
17
|
+
import { homedir as homedir2 } from "node:os";
|
|
18
|
+
import { join as join4, resolve } from "node:path";
|
|
19
|
+
import { parseArgs } from "node:util";
|
|
20
|
+
|
|
21
|
+
// packages/app/src/index.ts
|
|
22
|
+
var runtimeEnv = () => globalThis.process?.env ?? {};
|
|
23
|
+
var DEFAULT_URL = "https://app.vibiz.app";
|
|
24
|
+
var VibizAppError = class extends Error {
|
|
25
|
+
status;
|
|
26
|
+
code;
|
|
27
|
+
details;
|
|
28
|
+
constructor(status, code, message, details) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "VibizAppError";
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.details = details;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var WEBHOOK_TOLERANCE_S = 300;
|
|
37
|
+
var K = new Uint32Array([
|
|
38
|
+
1116352408,
|
|
39
|
+
1899447441,
|
|
40
|
+
3049323471,
|
|
41
|
+
3921009573,
|
|
42
|
+
961987163,
|
|
43
|
+
1508970993,
|
|
44
|
+
2453635748,
|
|
45
|
+
2870763221,
|
|
46
|
+
3624381080,
|
|
47
|
+
310598401,
|
|
48
|
+
607225278,
|
|
49
|
+
1426881987,
|
|
50
|
+
1925078388,
|
|
51
|
+
2162078206,
|
|
52
|
+
2614888103,
|
|
53
|
+
3248222580,
|
|
54
|
+
3835390401,
|
|
55
|
+
4022224774,
|
|
56
|
+
264347078,
|
|
57
|
+
604807628,
|
|
58
|
+
770255983,
|
|
59
|
+
1249150122,
|
|
60
|
+
1555081692,
|
|
61
|
+
1996064986,
|
|
62
|
+
2554220882,
|
|
63
|
+
2821834349,
|
|
64
|
+
2952996808,
|
|
65
|
+
3210313671,
|
|
66
|
+
3336571891,
|
|
67
|
+
3584528711,
|
|
68
|
+
113926993,
|
|
69
|
+
338241895,
|
|
70
|
+
666307205,
|
|
71
|
+
773529912,
|
|
72
|
+
1294757372,
|
|
73
|
+
1396182291,
|
|
74
|
+
1695183700,
|
|
75
|
+
1986661051,
|
|
76
|
+
2177026350,
|
|
77
|
+
2456956037,
|
|
78
|
+
2730485921,
|
|
79
|
+
2820302411,
|
|
80
|
+
3259730800,
|
|
81
|
+
3345764771,
|
|
82
|
+
3516065817,
|
|
83
|
+
3600352804,
|
|
84
|
+
4094571909,
|
|
85
|
+
275423344,
|
|
86
|
+
430227734,
|
|
87
|
+
506948616,
|
|
88
|
+
659060556,
|
|
89
|
+
883997877,
|
|
90
|
+
958139571,
|
|
91
|
+
1322822218,
|
|
92
|
+
1537002063,
|
|
93
|
+
1747873779,
|
|
94
|
+
1955562222,
|
|
95
|
+
2024104815,
|
|
96
|
+
2227730452,
|
|
97
|
+
2361852424,
|
|
98
|
+
2428436474,
|
|
99
|
+
2756734187,
|
|
100
|
+
3204031479,
|
|
101
|
+
3329325298
|
|
102
|
+
]);
|
|
103
|
+
function sha256(data) {
|
|
104
|
+
const h = [
|
|
105
|
+
1779033703,
|
|
106
|
+
3144134277,
|
|
107
|
+
1013904242,
|
|
108
|
+
2773480762,
|
|
109
|
+
1359893119,
|
|
110
|
+
2600822924,
|
|
111
|
+
528734635,
|
|
112
|
+
1541459225
|
|
113
|
+
];
|
|
114
|
+
const blocks = Math.ceil((data.length + 9) / 64);
|
|
115
|
+
const buf = new Uint8Array(blocks * 64);
|
|
116
|
+
buf.set(data);
|
|
117
|
+
buf[data.length] = 128;
|
|
118
|
+
const view = new DataView(buf.buffer);
|
|
119
|
+
view.setUint32(buf.length - 8, Math.floor(data.length / 536870912));
|
|
120
|
+
view.setUint32(buf.length - 4, data.length << 3 >>> 0);
|
|
121
|
+
const w = new Array(64).fill(0);
|
|
122
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
123
|
+
for (let b = 0; b < blocks; b++) {
|
|
124
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(b * 64 + i * 4);
|
|
125
|
+
for (let i = 16; i < 64; i++) {
|
|
126
|
+
const a15 = w[i - 15] ?? 0;
|
|
127
|
+
const a2 = w[i - 2] ?? 0;
|
|
128
|
+
const s0 = rotr(a15, 7) ^ rotr(a15, 18) ^ a15 >>> 3;
|
|
129
|
+
const s1 = rotr(a2, 17) ^ rotr(a2, 19) ^ a2 >>> 10;
|
|
130
|
+
w[i] = (w[i - 16] ?? 0) + s0 + (w[i - 7] ?? 0) + s1 >>> 0;
|
|
131
|
+
}
|
|
132
|
+
const v = [...h];
|
|
133
|
+
for (let i = 0; i < 64; i++) {
|
|
134
|
+
const [a = 0, bb = 0, c = 0, d = 0, e = 0, f = 0, g = 0, hh = 0] = v;
|
|
135
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + (K[i] ?? 0) + (w[i] ?? 0) >>> 0;
|
|
136
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & bb ^ a & c ^ bb & c) >>> 0;
|
|
137
|
+
v.splice(0, 8, t1 + t2 >>> 0, a, bb, c, d + t1 >>> 0, e, f, g);
|
|
138
|
+
}
|
|
139
|
+
for (let i = 0; i < 8; i++) h[i] = (h[i] ?? 0) + (v[i] ?? 0) >>> 0;
|
|
140
|
+
}
|
|
141
|
+
const digest = new Uint8Array(32);
|
|
142
|
+
const dv = new DataView(digest.buffer);
|
|
143
|
+
for (let i = 0; i < 8; i++) dv.setUint32(i * 4, h[i] ?? 0);
|
|
144
|
+
return digest;
|
|
145
|
+
}
|
|
146
|
+
function hmacSha256Hex(key, message) {
|
|
147
|
+
const enc = new TextEncoder();
|
|
148
|
+
let k = enc.encode(key);
|
|
149
|
+
if (k.length > 64) k = sha256(k);
|
|
150
|
+
const pad = (byte, rest) => {
|
|
151
|
+
const out = new Uint8Array(64 + rest.length);
|
|
152
|
+
for (let i = 0; i < 64; i++) out[i] = (k[i] ?? 0) ^ byte;
|
|
153
|
+
out.set(rest, 64);
|
|
154
|
+
return out;
|
|
155
|
+
};
|
|
156
|
+
const inner = sha256(pad(54, enc.encode(message)));
|
|
157
|
+
return [...sha256(pad(92, inner))].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
158
|
+
}
|
|
159
|
+
function sameString(a, b) {
|
|
160
|
+
if (a.length !== b.length) return false;
|
|
161
|
+
let diff = 0;
|
|
162
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
163
|
+
return diff === 0;
|
|
164
|
+
}
|
|
165
|
+
function verifyWebhook(rawBody, header, secret = runtimeEnv().VIBIZ_WEBHOOK_SECRET, nowSeconds = Math.floor(Date.now() / 1e3)) {
|
|
166
|
+
const bad = (why) => new VibizAppError(400, "invalid_signature", `Webhook refused: ${why}.`);
|
|
167
|
+
if (!secret) {
|
|
168
|
+
throw new VibizAppError(500, "not_configured", "No VIBIZ_WEBHOOK_SECRET in the env.");
|
|
169
|
+
}
|
|
170
|
+
if (!header) throw bad("no Vibiz-Signature header");
|
|
171
|
+
let t;
|
|
172
|
+
const sigs = [];
|
|
173
|
+
for (const part of header.split(",")) {
|
|
174
|
+
const i = part.indexOf("=");
|
|
175
|
+
const k = part.slice(0, i).trim();
|
|
176
|
+
const v = part.slice(i + 1).trim();
|
|
177
|
+
if (k === "t" && /^\d{1,12}$/.test(v)) t = Number(v);
|
|
178
|
+
else if (k === "v1" && v) sigs.push(v.toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
if (t === void 0 || !sigs.length) throw bad("malformed signature");
|
|
181
|
+
if (Math.abs(nowSeconds - t) > WEBHOOK_TOLERANCE_S) throw bad("too old");
|
|
182
|
+
const expected = hmacSha256Hex(secret, `${t}.${rawBody}`);
|
|
183
|
+
if (!sigs.some((sig) => sameString(sig, expected))) throw bad("signature does not match");
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(rawBody);
|
|
186
|
+
} catch {
|
|
187
|
+
throw bad("not JSON");
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function userHeader(user) {
|
|
191
|
+
if (/^[\x20-\x7e]*$/.test(user) && !user.startsWith("b64u:")) return user;
|
|
192
|
+
let bin = "";
|
|
193
|
+
for (const b of new TextEncoder().encode(user)) bin += String.fromCharCode(b);
|
|
194
|
+
return `b64u:${btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
195
|
+
}
|
|
196
|
+
var VibizApp = class _VibizApp {
|
|
197
|
+
options;
|
|
198
|
+
loaded = null;
|
|
199
|
+
constructor(options = {}) {
|
|
200
|
+
this.options = options;
|
|
201
|
+
}
|
|
202
|
+
/** The key, read when first needed, so `loadEnv()` or a late env still works. */
|
|
203
|
+
get key() {
|
|
204
|
+
const key = this.options.key ?? runtimeEnv().VIBIZ_APP_KEY;
|
|
205
|
+
if (!key) {
|
|
206
|
+
throw new VibizAppError(
|
|
207
|
+
401,
|
|
208
|
+
"unauthorized",
|
|
209
|
+
"No app key. Set VIBIZ_APP_KEY (Vibiz puts it in your app\u2019s env), or pass { key }."
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return key;
|
|
213
|
+
}
|
|
214
|
+
get url() {
|
|
215
|
+
return (this.options.url ?? runtimeEnv().VIBIZ_API_URL ?? DEFAULT_URL).replace(/\/$/, "");
|
|
216
|
+
}
|
|
217
|
+
/** A client that attributes spend and events to one of your users. */
|
|
218
|
+
user(id) {
|
|
219
|
+
return new _VibizApp({ ...this.options, user: id });
|
|
220
|
+
}
|
|
221
|
+
async request(path, init = {}) {
|
|
222
|
+
if (typeof globalThis.window !== "undefined" && !this.options.dangerouslyAllowBrowser) {
|
|
223
|
+
throw new VibizAppError(
|
|
224
|
+
400,
|
|
225
|
+
"invalid_request",
|
|
226
|
+
"The app key is a server secret: call Vibiz from your server, not the browser."
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const res = await (this.options.fetch ?? fetch)(`${this.url}/v1${path}`, {
|
|
230
|
+
method: init.method ?? (init.body === void 0 ? "GET" : "POST"),
|
|
231
|
+
headers: {
|
|
232
|
+
authorization: `Bearer ${this.key}`,
|
|
233
|
+
accept: "application/json",
|
|
234
|
+
...init.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
235
|
+
...this.options.user ? { "x-vibiz-user": userHeader(this.options.user) } : {}
|
|
236
|
+
},
|
|
237
|
+
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {}
|
|
238
|
+
});
|
|
239
|
+
const text = await res.text();
|
|
240
|
+
let data = null;
|
|
241
|
+
try {
|
|
242
|
+
data = text ? JSON.parse(text) : null;
|
|
243
|
+
} catch {
|
|
244
|
+
data = null;
|
|
245
|
+
}
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
const e = data?.error;
|
|
248
|
+
throw new VibizAppError(
|
|
249
|
+
res.status,
|
|
250
|
+
e?.code ?? "error",
|
|
251
|
+
e?.message ?? `Vibiz answered ${res.status}.`,
|
|
252
|
+
e?.details
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return data;
|
|
256
|
+
}
|
|
257
|
+
/** Which project and environment this key is. */
|
|
258
|
+
whoami() {
|
|
259
|
+
return this.request("/app");
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Fetch the app's env from Vibiz and put it into `process.env` (existing
|
|
263
|
+
* variables win unless `override`). For hosts that hold only VIBIZ_APP_KEY:
|
|
264
|
+
* call it once at boot, before anything reads the env. Fetched once per
|
|
265
|
+
* process; `refresh: true` fetches again (after a rotation).
|
|
266
|
+
*/
|
|
267
|
+
async loadEnv(opts = {}) {
|
|
268
|
+
if (!this.loaded || opts.refresh) {
|
|
269
|
+
this.loaded = this.request("/app/env").then((r) => r.env);
|
|
270
|
+
this.loaded.catch(() => {
|
|
271
|
+
this.loaded = null;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const env = await this.loaded;
|
|
275
|
+
const target = runtimeEnv();
|
|
276
|
+
for (const [k, v] of Object.entries(env)) {
|
|
277
|
+
if (opts.override || target[k] === void 0) target[k] = v;
|
|
278
|
+
}
|
|
279
|
+
return env;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The AI gateway, OpenAI-compatible: `new OpenAI(vibiz.ai)` or the Vercel AI
|
|
283
|
+
* SDK's `createOpenAI(vibiz.ai)`. Models: `vibiz/fast`, `vibiz/smart`…
|
|
284
|
+
*/
|
|
285
|
+
get ai() {
|
|
286
|
+
const self = this;
|
|
287
|
+
return {
|
|
288
|
+
get baseURL() {
|
|
289
|
+
return `${self.url}/v1/ai`;
|
|
290
|
+
},
|
|
291
|
+
get apiKey() {
|
|
292
|
+
return self.key;
|
|
293
|
+
},
|
|
294
|
+
/** One chat completion, without an SDK. */
|
|
295
|
+
chat: (input) => self.request("/ai/chat/completions", {
|
|
296
|
+
body: {
|
|
297
|
+
model: "vibiz/fast",
|
|
298
|
+
...input,
|
|
299
|
+
...self.options.user ? { user: self.options.user } : {}
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/** Email from the project's own address (needs the `email` resource). */
|
|
305
|
+
email = {
|
|
306
|
+
send: (input) => this.actions.run("email.send", input)
|
|
307
|
+
};
|
|
308
|
+
/** Any action (`web.scrape`, `web.search`, `places.search`, `email.send`…), billed to the project. */
|
|
309
|
+
actions = {
|
|
310
|
+
run: (name, input) => this.request(`/app/actions/${encodeURIComponent(name)}`, {
|
|
311
|
+
body: { input }
|
|
312
|
+
})
|
|
313
|
+
};
|
|
314
|
+
/**
|
|
315
|
+
* Payments (docs/PAYMENTS.md): Vibiz is the merchant, so this works from the
|
|
316
|
+
* first call, before the owner has verified anything. Refunds and
|
|
317
|
+
* withdrawals are the owner's, in the console.
|
|
318
|
+
*/
|
|
319
|
+
payments = {
|
|
320
|
+
/** A hosted checkout page for a price (one-time or a subscription) or an ad-hoc item. */
|
|
321
|
+
checkout: (input) => this.request(
|
|
322
|
+
"/app/payments/checkout",
|
|
323
|
+
{ body: input }
|
|
324
|
+
),
|
|
325
|
+
/** The user's live subscription (active, trialing or past due), or null. */
|
|
326
|
+
subscription: async (user) => {
|
|
327
|
+
const r = await this.request(
|
|
328
|
+
`/app/payments/subscriptions?user=${encodeURIComponent(user)}&status=active,trialing,past_due`
|
|
329
|
+
);
|
|
330
|
+
return r.data[0] ?? null;
|
|
331
|
+
},
|
|
332
|
+
/** Whether the user has an active (or trialing) subscription, optionally to one price id or lookup key. */
|
|
333
|
+
hasActive: async (user, price) => {
|
|
334
|
+
const r = await this.request(
|
|
335
|
+
`/app/payments/subscriptions?user=${encodeURIComponent(user)}&status=active,trialing`
|
|
336
|
+
);
|
|
337
|
+
if (!price) return r.data.length > 0;
|
|
338
|
+
if (r.data.some((x) => x.price_id === price)) return true;
|
|
339
|
+
const products = await this.payments.products();
|
|
340
|
+
const ids = new Set(
|
|
341
|
+
products.flatMap((p) => p.prices).filter((p) => p.lookup_key === price).map((p) => p.id)
|
|
342
|
+
);
|
|
343
|
+
return r.data.some((x) => x.price_id !== null && ids.has(x.price_id));
|
|
344
|
+
},
|
|
345
|
+
/** A link where the user manages their subscription and card. */
|
|
346
|
+
portal: (user, opts) => this.request("/app/payments/portal", {
|
|
347
|
+
body: { user, return_url: opts.return_url }
|
|
348
|
+
}),
|
|
349
|
+
/** The catalog: products with their prices. */
|
|
350
|
+
products: () => this.request("/app/payments/products").then((r) => r.data),
|
|
351
|
+
/** Payments, newest first (`user` keeps one of yours). */
|
|
352
|
+
payments: (opts = {}) => {
|
|
353
|
+
const q = new URLSearchParams();
|
|
354
|
+
for (const [k, v] of Object.entries(opts)) if (v !== void 0) q.set(k, String(v));
|
|
355
|
+
const qs = q.toString();
|
|
356
|
+
return this.request(
|
|
357
|
+
`/app/payments/payments${qs ? `?${qs}` : ""}`
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
/** Cancel a user's subscription (at the end of the period by default). */
|
|
361
|
+
cancel: (subscriptionId, opts = {}) => this.request(
|
|
362
|
+
`/app/payments/subscriptions/${encodeURIComponent(subscriptionId)}/cancel`,
|
|
363
|
+
{ body: opts }
|
|
364
|
+
)
|
|
365
|
+
};
|
|
366
|
+
/**
|
|
367
|
+
* Your connectors (docs/CONNECTORS.md): call an API through Vibiz, which
|
|
368
|
+
* holds the key for this app's environment. The answer's `data` is the
|
|
369
|
+
* API's own content. A dangerous operation needs the owner's permission.
|
|
370
|
+
*/
|
|
371
|
+
connectors = {
|
|
372
|
+
/** The connectors with their enabled operations and parameters. */
|
|
373
|
+
list: () => this.request("/app/connectors").then((r) => r.data),
|
|
374
|
+
call: (name, operation, input = {}) => this.request(`/app/connectors/${encodeURIComponent(name)}/call`, {
|
|
375
|
+
body: { operation, ...input }
|
|
376
|
+
})
|
|
377
|
+
};
|
|
378
|
+
/** Webhooks from Vibiz: `verify(rawBody, header)` checks the signature with VIBIZ_WEBHOOK_SECRET. */
|
|
379
|
+
webhooks = {
|
|
380
|
+
verify: (rawBody, header, secret) => verifyWebhook(rawBody, header, secret)
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* A server-side event in the project's analytics, next to the browser's.
|
|
384
|
+
* Never throws: analytics must not break the request that tracks it.
|
|
385
|
+
* Returns whether it was recorded (development keys don't record).
|
|
386
|
+
*/
|
|
387
|
+
async track(name, opts = {}) {
|
|
388
|
+
try {
|
|
389
|
+
const user = opts.user ?? this.options.user;
|
|
390
|
+
const r = await this.request("/app/events", {
|
|
391
|
+
body: {
|
|
392
|
+
name,
|
|
393
|
+
...user ? { user } : {},
|
|
394
|
+
...opts.path ? { path: opts.path } : {},
|
|
395
|
+
...opts.properties ? { properties: opts.properties } : {}
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return r.recorded;
|
|
399
|
+
} catch (e) {
|
|
400
|
+
globalThis.console?.warn?.(
|
|
401
|
+
`vibiz.track(${name}) failed: ${e instanceof Error ? e.message : String(e)}`
|
|
402
|
+
);
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
var createVibiz = (options) => new VibizApp(options);
|
|
408
|
+
var vibiz = new VibizApp();
|
|
409
|
+
|
|
410
|
+
// packages/cli/src/billing.ts
|
|
411
|
+
var dollars = (cents2) => `${cents2 < 0 ? "-" : ""}$${(Math.abs(cents2) / 100).toFixed(2)}`;
|
|
412
|
+
function parseDollars(raw, what = "amount") {
|
|
413
|
+
const v = (raw ?? "").trim().replace(/^\$/, "");
|
|
414
|
+
if (!/^\d+(\.\d{1,2})?$/.test(v)) throw new Error(`${what} is in dollars, like 25 or 25.50`);
|
|
415
|
+
return Math.round(Number(v) * 100);
|
|
416
|
+
}
|
|
417
|
+
var STATUS = {
|
|
418
|
+
active: "active",
|
|
419
|
+
low: "low balance",
|
|
420
|
+
payment_required: "out of credits: paid calls are refused",
|
|
421
|
+
past_due: "out of credits for a week: deploys and new resources are paused",
|
|
422
|
+
exempt: "covered by Vibiz"
|
|
423
|
+
};
|
|
424
|
+
var KIND = {
|
|
425
|
+
trial: "Trial credit",
|
|
426
|
+
topup: "Credits",
|
|
427
|
+
company_payment: "Company formation (card)",
|
|
428
|
+
usage: "Usage",
|
|
429
|
+
refund: "Refund",
|
|
430
|
+
dispute: "Card dispute",
|
|
431
|
+
dispute_reversal: "Dispute won",
|
|
432
|
+
adjustment: "Adjustment"
|
|
433
|
+
};
|
|
434
|
+
function formatEntry(e) {
|
|
435
|
+
const when = e.created_at.slice(0, 10);
|
|
436
|
+
const what = e.kind === "usage" && e.day ? `Usage ${e.day}` : KIND[e.kind];
|
|
437
|
+
return ` ${when} ${what.padEnd(26)} ${dollars(e.amount_cents).padStart(10)}${e.note && e.kind === "adjustment" ? ` ${e.note}` : ""}`;
|
|
438
|
+
}
|
|
439
|
+
function formatBilling(b, history) {
|
|
440
|
+
const lines = [
|
|
441
|
+
b.mode === "exempt" ? "Billing Vibiz covers this account\u2019s usage" : `Balance ${dollars(b.balance_cents)} (${STATUS[b.status]})`
|
|
442
|
+
];
|
|
443
|
+
if (b.pauses_at && b.status === "payment_required") {
|
|
444
|
+
lines.push(` deploys pause on ${b.pauses_at.slice(0, 10)} unless credits are added`);
|
|
445
|
+
}
|
|
446
|
+
lines.push(
|
|
447
|
+
`Card ${b.card ? `${b.card.brand} \u2022\u2022\u2022\u2022 ${b.card.last4}` : "none saved"}`,
|
|
448
|
+
`Auto ${b.auto_recharge.enabled ? `on: below ${dollars(b.auto_recharge.threshold_cents)}, add ${dollars(b.auto_recharge.amount_cents)}` : `off${b.auto_recharge.disabled_reason === "card_declined" ? " (turned off after two declines)" : ""}`}`,
|
|
449
|
+
`Daily cap $${b.daily_spend_limit_usd} a day on AI and actions`
|
|
450
|
+
);
|
|
451
|
+
if (history?.data.length) {
|
|
452
|
+
lines.push("", "Recent");
|
|
453
|
+
for (const e of history.data) lines.push(formatEntry(e));
|
|
454
|
+
}
|
|
455
|
+
if (b.next_step) lines.push("", b.next_step);
|
|
456
|
+
lines.push("", `Add credits: vibiz billing top-up 25 Console: ${b.console_url}`);
|
|
457
|
+
return lines.join("\n");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// packages/cli/src/detect.ts
|
|
461
|
+
import { readdir } from "node:fs/promises";
|
|
462
|
+
var SIGNS = [
|
|
463
|
+
{ kind: "vercel", label: "Vercel", files: /^(vercel\.json|\.vercel)$/, push: true },
|
|
464
|
+
{ kind: "netlify", label: "Netlify", files: /^(netlify\.toml|\.netlify)$/, push: true },
|
|
465
|
+
{ kind: "render", label: "Render", files: /^render\.ya?ml$/, push: true },
|
|
466
|
+
{ kind: "railway", label: "Railway", files: /^railway\.(json|toml)$/, push: true },
|
|
467
|
+
{ kind: "fly", label: "Fly.io", files: /^fly\.toml$/, push: true },
|
|
468
|
+
{ kind: "heroku", label: "Heroku", files: /^(Procfile|app\.json)$/, push: true },
|
|
469
|
+
{
|
|
470
|
+
kind: "cloudflare-workers",
|
|
471
|
+
label: "Cloudflare (Workers; for Pages use cloudflare-pages)",
|
|
472
|
+
files: /^wrangler\.(toml|json|jsonc)$/,
|
|
473
|
+
push: true
|
|
474
|
+
},
|
|
475
|
+
{ kind: "github-actions", label: "GitHub Actions", files: /^\.github$/, push: true },
|
|
476
|
+
{
|
|
477
|
+
kind: "docker",
|
|
478
|
+
label: "a container host (Docker)",
|
|
479
|
+
files: /^(Dockerfile|docker-compose\.ya?ml|compose\.ya?ml)$/,
|
|
480
|
+
push: false
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
kind: "aws",
|
|
484
|
+
label: "AWS (Serverless, SAM, Amplify, CDK)",
|
|
485
|
+
files: /^(serverless\.ya?ml|template\.ya?ml|samconfig\.toml|amplify\.yml|cdk\.json)$/,
|
|
486
|
+
push: false
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
kind: "gcp",
|
|
490
|
+
label: "Google Cloud (App Engine, Cloud Build)",
|
|
491
|
+
files: /^(app\.yaml|cloudbuild\.ya?ml)$/,
|
|
492
|
+
push: false
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
kind: "azure",
|
|
496
|
+
label: "Azure (Static Web Apps, azd)",
|
|
497
|
+
files: /^(staticwebapp\.config\.json|azure\.yaml)$/,
|
|
498
|
+
push: false
|
|
499
|
+
},
|
|
500
|
+
{ kind: "digitalocean", label: "DigitalOcean App Platform", files: /^\.do$/, push: false }
|
|
501
|
+
];
|
|
502
|
+
async function detectHosts(dir) {
|
|
503
|
+
const names = await readdir(dir).catch(() => []);
|
|
504
|
+
const out = [];
|
|
505
|
+
for (const sign of SIGNS) {
|
|
506
|
+
const file = names.find((n) => sign.files.test(n));
|
|
507
|
+
if (file && !out.some((h) => h.kind === sign.kind)) {
|
|
508
|
+
out.push({ kind: sign.kind, label: sign.label, file, push: sign.push });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
var ONE_KEY_PATHS = ` Put ONE secret in the host: VIBIZ_APP_KEY (an owner copies it from the console:
|
|
514
|
+
project \u25B8 Settings \u25B8 Production environment \u25B8 Show values). Then, at start:
|
|
515
|
+
|
|
516
|
+
Node, Bun, Deno import { vibiz } from '@vibiz/app'; await vibiz.loadEnv();
|
|
517
|
+
any language npx @vibiz/cli run -- <your start command>
|
|
518
|
+
Docker / shell eval "$(curl -fsS -H "Authorization: Bearer $VIBIZ_APP_KEY" \\
|
|
519
|
+
https://app.vibiz.app/v1/app/env?format=shell)" && exec <your server>
|
|
520
|
+
CI / build step npx @vibiz/cli env pull --format dotenv -o .env.production`;
|
|
521
|
+
|
|
522
|
+
// packages/cli/src/login.ts
|
|
523
|
+
import { hostname, platform, release } from "node:os";
|
|
524
|
+
import { join } from "node:path";
|
|
525
|
+
|
|
526
|
+
// packages/cli/src/version.ts
|
|
527
|
+
import { readFileSync } from "node:fs";
|
|
528
|
+
function cliVersion() {
|
|
529
|
+
try {
|
|
530
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
531
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
532
|
+
} catch {
|
|
533
|
+
return "0.0.0";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// packages/cli/src/login.ts
|
|
538
|
+
var DEFAULT_API_URL = "https://app.vibiz.app";
|
|
539
|
+
var pendingFile = (dir) => join(dir, "pending-login.json");
|
|
540
|
+
var configFile = (dir) => join(dir, "config.json");
|
|
541
|
+
async function readJson(io, path) {
|
|
542
|
+
try {
|
|
543
|
+
return JSON.parse(String(await io.local().readFile(path, "utf8")));
|
|
544
|
+
} catch {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function writePrivate(io, dir, path, value) {
|
|
549
|
+
const local = io.local();
|
|
550
|
+
await local.mkdir(dir, { recursive: true });
|
|
551
|
+
await local.writeFile(path, `${JSON.stringify(value, null, 2)}
|
|
552
|
+
`, { mode: 384 });
|
|
553
|
+
await local.chmod(path, 384);
|
|
554
|
+
}
|
|
555
|
+
async function forget(io, path) {
|
|
556
|
+
await io.local().writeFile(path, "{}\n", { mode: 384 }).catch(() => {
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
async function saveLogin(io, dir, saved) {
|
|
560
|
+
await writePrivate(io, dir, configFile(dir), saved);
|
|
561
|
+
}
|
|
562
|
+
async function readLogin(io, dir) {
|
|
563
|
+
return await readJson(io, configFile(dir)) ?? {};
|
|
564
|
+
}
|
|
565
|
+
var LoginError = class extends Error {
|
|
566
|
+
};
|
|
567
|
+
async function startLogin(api, f) {
|
|
568
|
+
let res;
|
|
569
|
+
try {
|
|
570
|
+
res = await f(`${api}/v1/cli/login`, {
|
|
571
|
+
method: "POST",
|
|
572
|
+
headers: { "content-type": "application/json" },
|
|
573
|
+
body: JSON.stringify({
|
|
574
|
+
hostname: hostname(),
|
|
575
|
+
os: `${platform()} ${release()}`,
|
|
576
|
+
cli_version: cliVersion()
|
|
577
|
+
})
|
|
578
|
+
});
|
|
579
|
+
} catch {
|
|
580
|
+
throw new LoginError(`Could not reach ${api}. Check the address (--api) and your connection.`);
|
|
581
|
+
}
|
|
582
|
+
const body = await res.json().catch(() => null);
|
|
583
|
+
if (!res.ok || !body || !("device_code" in body)) {
|
|
584
|
+
const msg = body && "error" in body ? body.error?.message : void 0;
|
|
585
|
+
if (res.status === 404 || res.status === 503) {
|
|
586
|
+
throw new LoginError(
|
|
587
|
+
`${api} does not offer browser login${msg ? ` (${msg})` : ""}. Use: vibiz login --key <vz_sk_\u2026> --api ${api}`
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
throw new LoginError(msg ?? `Login could not start (HTTP ${res.status}).`);
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
api,
|
|
594
|
+
device_code: body.device_code,
|
|
595
|
+
user_code: body.user_code,
|
|
596
|
+
verification_uri: body.verification_uri,
|
|
597
|
+
verification_uri_complete: body.verification_uri_complete,
|
|
598
|
+
interval: body.interval,
|
|
599
|
+
expires_at: Date.now() + body.expires_in * 1e3
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
async function browserLogin(io, api, deps) {
|
|
603
|
+
const f = deps.fetch ?? globalThis.fetch;
|
|
604
|
+
const sleep = deps.sleep ?? ((ms2) => new Promise((r) => setTimeout(r, ms2)));
|
|
605
|
+
const dir = deps.configDir;
|
|
606
|
+
const pendingPath = pendingFile(dir);
|
|
607
|
+
const previous = await readJson(io, pendingPath);
|
|
608
|
+
let pending;
|
|
609
|
+
const resumed = previous?.device_code && previous.api === api && typeof previous.expires_at === "number" && previous.expires_at > Date.now() + 15e3;
|
|
610
|
+
if (resumed) {
|
|
611
|
+
pending = previous;
|
|
612
|
+
io.log("Resuming the login started earlier.");
|
|
613
|
+
} else {
|
|
614
|
+
pending = await startLogin(api, f);
|
|
615
|
+
await writePrivate(io, dir, pendingPath, pending);
|
|
616
|
+
}
|
|
617
|
+
const minutes = Math.max(1, Math.round((pending.expires_at - Date.now()) / 6e4));
|
|
618
|
+
io.log(
|
|
619
|
+
[
|
|
620
|
+
"",
|
|
621
|
+
"To log in, open this link and click Authorize:",
|
|
622
|
+
"",
|
|
623
|
+
` ${pending.verification_uri_complete}`,
|
|
624
|
+
"",
|
|
625
|
+
` Code: ${pending.user_code} (the page shows the same code)`,
|
|
626
|
+
"",
|
|
627
|
+
`Waiting for approval\u2026 (the link works for ${minutes} min; run \`vibiz login\` again to keep waiting)`
|
|
628
|
+
].join("\n")
|
|
629
|
+
);
|
|
630
|
+
if (deps.open !== false) io.openUrl(pending.verification_uri_complete);
|
|
631
|
+
let interval = Math.max(1, pending.interval) * 1e3;
|
|
632
|
+
for (; ; ) {
|
|
633
|
+
if (Date.now() >= pending.expires_at) {
|
|
634
|
+
await forget(io, pendingPath);
|
|
635
|
+
throw new LoginError("The login link expired. Run `vibiz login` again.");
|
|
636
|
+
}
|
|
637
|
+
await sleep(interval);
|
|
638
|
+
let res;
|
|
639
|
+
try {
|
|
640
|
+
res = await f(`${api}/v1/cli/login/token`, {
|
|
641
|
+
method: "POST",
|
|
642
|
+
headers: { "content-type": "application/json" },
|
|
643
|
+
body: JSON.stringify({ device_code: pending.device_code })
|
|
644
|
+
});
|
|
645
|
+
} catch {
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const body = await res.json().catch(() => null);
|
|
649
|
+
if (res.ok && body?.api_key) {
|
|
650
|
+
await saveLogin(io, dir, {
|
|
651
|
+
apiKey: body.api_key,
|
|
652
|
+
apiUrl: api,
|
|
653
|
+
...body.key?.id ? { keyId: body.key.id } : {},
|
|
654
|
+
source: "browser"
|
|
655
|
+
});
|
|
656
|
+
await forget(io, pendingPath);
|
|
657
|
+
io.log(
|
|
658
|
+
`
|
|
659
|
+
\u2713 Logged in to ${body.account?.name ?? "your account"} as "${body.key?.name ?? "cli"}" (${body.key?.access ?? "agent"} key). Saved to ${configFile(dir)}`
|
|
660
|
+
);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
switch (body?.error) {
|
|
664
|
+
case "authorization_pending":
|
|
665
|
+
continue;
|
|
666
|
+
case "slow_down":
|
|
667
|
+
interval = (body.interval ?? interval / 1e3 + 5) * 1e3;
|
|
668
|
+
continue;
|
|
669
|
+
case "access_denied":
|
|
670
|
+
await forget(io, pendingPath);
|
|
671
|
+
throw new LoginError("The login was declined in the browser.");
|
|
672
|
+
case "expired_token":
|
|
673
|
+
await forget(io, pendingPath);
|
|
674
|
+
throw new LoginError("The login link expired. Run `vibiz login` again.");
|
|
675
|
+
default:
|
|
676
|
+
await forget(io, pendingPath);
|
|
677
|
+
throw new LoginError(
|
|
678
|
+
`Login failed (${body?.error ?? `HTTP ${res.status}`}). Run \`vibiz login\` again.`
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// packages/cli/src/scaffold.ts
|
|
685
|
+
import { existsSync } from "node:fs";
|
|
686
|
+
import { cp, readdir as readdir2, readFile, rename, writeFile } from "node:fs/promises";
|
|
687
|
+
import { dirname, join as join2 } from "node:path";
|
|
688
|
+
import { fileURLToPath } from "node:url";
|
|
689
|
+
var TEMPLATES = ["nextjs-saas"];
|
|
690
|
+
var SKIP = /* @__PURE__ */ new Set([
|
|
691
|
+
"node_modules",
|
|
692
|
+
".next",
|
|
693
|
+
".vercel",
|
|
694
|
+
".turbo",
|
|
695
|
+
".git",
|
|
696
|
+
".DS_Store",
|
|
697
|
+
"vibiz.json",
|
|
698
|
+
"next-env.d.ts"
|
|
699
|
+
]);
|
|
700
|
+
var isLocalEnv = (name) => (name === ".env" || name.startsWith(".env.")) && name !== ".env.example";
|
|
701
|
+
var skipped = (name) => SKIP.has(name) || isLocalEnv(name) || name.endsWith(".tsbuildinfo");
|
|
702
|
+
var HARMLESS = /* @__PURE__ */ new Set([".git", ".DS_Store"]);
|
|
703
|
+
var PACKED_DOTFILES = [".gitignore", ".npmrc"];
|
|
704
|
+
var packedName = (dotfile) => `_${dotfile.slice(1)}`;
|
|
705
|
+
function templatesDir() {
|
|
706
|
+
const fromEnv = process.env.VIBIZ_TEMPLATES_DIR?.trim();
|
|
707
|
+
if (fromEnv) return fromEnv;
|
|
708
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
709
|
+
const packaged = join2(here, "..", "templates");
|
|
710
|
+
if (existsSync(packaged)) return packaged;
|
|
711
|
+
return join2(here, "..", "..", "..", "templates");
|
|
712
|
+
}
|
|
713
|
+
function isTemplate(name) {
|
|
714
|
+
return TEMPLATES.includes(name);
|
|
715
|
+
}
|
|
716
|
+
function packageName(name) {
|
|
717
|
+
const slug = name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
718
|
+
return slug || "app";
|
|
719
|
+
}
|
|
720
|
+
async function isEffectivelyEmpty(dir) {
|
|
721
|
+
try {
|
|
722
|
+
return (await readdir2(dir)).every((e) => HARMLESS.has(e));
|
|
723
|
+
} catch (err) {
|
|
724
|
+
if (err.code === "ENOENT") return true;
|
|
725
|
+
throw err;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function scaffold(opts) {
|
|
729
|
+
if (!opts.force && !await isEffectivelyEmpty(opts.dir)) {
|
|
730
|
+
throw new Error(`${opts.dir} is not empty. Use an empty folder, or pass --force.`);
|
|
731
|
+
}
|
|
732
|
+
const source = join2(opts.templatesRoot ?? templatesDir(), opts.template);
|
|
733
|
+
await cp(source, opts.dir, {
|
|
734
|
+
recursive: true,
|
|
735
|
+
force: true,
|
|
736
|
+
filter: (src) => {
|
|
737
|
+
const name = src.slice(source.length).split(/[\\/]/).pop() ?? "";
|
|
738
|
+
return src === source || !skipped(name);
|
|
739
|
+
}
|
|
740
|
+
}).catch((err) => {
|
|
741
|
+
if (err.code === "ENOENT") {
|
|
742
|
+
throw new Error(`Template "${opts.template}" not found in ${dirname(source)}`);
|
|
743
|
+
}
|
|
744
|
+
throw err;
|
|
745
|
+
});
|
|
746
|
+
for (const dotfile of PACKED_DOTFILES) {
|
|
747
|
+
await rename(join2(opts.dir, packedName(dotfile)), join2(opts.dir, dotfile)).catch((err) => {
|
|
748
|
+
if (err.code !== "ENOENT") throw err;
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
await setAppName(opts.dir, opts.name);
|
|
752
|
+
}
|
|
753
|
+
async function setAppName(dir, name) {
|
|
754
|
+
const pkgPath = join2(dir, "package.json");
|
|
755
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
756
|
+
pkg.name = packageName(name);
|
|
757
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
758
|
+
`);
|
|
759
|
+
const lockPath = join2(dir, "package-lock.json");
|
|
760
|
+
const lockText = await readFile(lockPath, "utf8").catch(() => null);
|
|
761
|
+
if (lockText !== null) {
|
|
762
|
+
const lock = JSON.parse(lockText);
|
|
763
|
+
lock.name = pkg.name;
|
|
764
|
+
const root = lock.packages?.[""];
|
|
765
|
+
if (root) root.name = pkg.name;
|
|
766
|
+
await writeFile(lockPath, `${JSON.stringify(lock, null, 2)}
|
|
767
|
+
`);
|
|
768
|
+
}
|
|
769
|
+
const configPath = join2(dir, "app.config.ts");
|
|
770
|
+
const config = await readFile(configPath, "utf8").catch(() => null);
|
|
771
|
+
if (config === null) return;
|
|
772
|
+
const literal = `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
773
|
+
const next = config.replace(/^(\s*name:\s*)(['"`]).*?\2/m, (_m, lead) => lead + literal);
|
|
774
|
+
if (next === config) throw new Error(`Could not set the app name in ${configPath}`);
|
|
775
|
+
await writeFile(configPath, next);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// packages/cli/src/setup.ts
|
|
779
|
+
import { accessSync, constants } from "node:fs";
|
|
780
|
+
import { homedir, platform as platform2 } from "node:os";
|
|
781
|
+
import { delimiter, dirname as dirname2, join as join3 } from "node:path";
|
|
782
|
+
var CLIENT_NAMES = {
|
|
783
|
+
"claude code": "claude",
|
|
784
|
+
claude: "claude",
|
|
785
|
+
codex: "codex",
|
|
786
|
+
"codex cli": "codex",
|
|
787
|
+
cursor: "cursor",
|
|
788
|
+
"vs code with copilot": "vscode",
|
|
789
|
+
"vs code": "vscode",
|
|
790
|
+
vscode: "vscode",
|
|
791
|
+
windsurf: "windsurf",
|
|
792
|
+
"gemini cli": "gemini",
|
|
793
|
+
gemini: "gemini",
|
|
794
|
+
opencode: "opencode"
|
|
795
|
+
};
|
|
796
|
+
var LABEL = {
|
|
797
|
+
claude: "Claude Code",
|
|
798
|
+
codex: "Codex",
|
|
799
|
+
cursor: "Cursor",
|
|
800
|
+
vscode: "VS Code with Copilot",
|
|
801
|
+
windsurf: "Windsurf",
|
|
802
|
+
gemini: "Gemini CLI",
|
|
803
|
+
opencode: "OpenCode"
|
|
804
|
+
};
|
|
805
|
+
var homeOf = (io) => io.home ?? homedir();
|
|
806
|
+
function onPath(cmd, env) {
|
|
807
|
+
const exts = platform2() === "win32" ? [".cmd", ".exe", ""] : [""];
|
|
808
|
+
for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
809
|
+
for (const ext of exts) {
|
|
810
|
+
try {
|
|
811
|
+
accessSync(join3(dir, cmd + ext), constants.X_OK);
|
|
812
|
+
return true;
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
function parseClients(raw) {
|
|
820
|
+
if (!raw) {
|
|
821
|
+
throw new Error(
|
|
822
|
+
`usage: vibiz mcp add --clients "<client>" (${Object.values(LABEL).map((l) => `"${l}"`).join(", ")})`
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
const out = [];
|
|
826
|
+
for (const part of raw.split(",")) {
|
|
827
|
+
const c = CLIENT_NAMES[part.trim().toLowerCase()];
|
|
828
|
+
if (!c)
|
|
829
|
+
throw new Error(
|
|
830
|
+
`Unknown client "${part.trim()}". Use one of: ${Object.values(LABEL).join(", ")}`
|
|
831
|
+
);
|
|
832
|
+
if (!out.includes(c)) out.push(c);
|
|
833
|
+
}
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
function mcpConfigPath(client, home) {
|
|
837
|
+
switch (client) {
|
|
838
|
+
case "cursor":
|
|
839
|
+
return join3(home, ".cursor", "mcp.json");
|
|
840
|
+
case "windsurf":
|
|
841
|
+
return join3(home, ".codeium", "windsurf", "mcp_config.json");
|
|
842
|
+
case "gemini":
|
|
843
|
+
return join3(home, ".gemini", "settings.json");
|
|
844
|
+
case "opencode":
|
|
845
|
+
return join3(home, ".config", "opencode", "opencode.json");
|
|
846
|
+
case "vscode":
|
|
847
|
+
if (platform2() === "darwin")
|
|
848
|
+
return join3(home, "Library", "Application Support", "Code", "User", "mcp.json");
|
|
849
|
+
if (platform2() === "win32")
|
|
850
|
+
return join3(
|
|
851
|
+
process.env.APPDATA ?? join3(home, "AppData", "Roaming"),
|
|
852
|
+
"Code",
|
|
853
|
+
"User",
|
|
854
|
+
"mcp.json"
|
|
855
|
+
);
|
|
856
|
+
return join3(home, ".config", "Code", "User", "mcp.json");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function jsonEntry(client, url) {
|
|
860
|
+
switch (client) {
|
|
861
|
+
case "cursor":
|
|
862
|
+
return { key: "mcpServers", entry: { url } };
|
|
863
|
+
case "windsurf":
|
|
864
|
+
return { key: "mcpServers", entry: { serverUrl: url } };
|
|
865
|
+
case "gemini":
|
|
866
|
+
return { key: "mcpServers", entry: { command: "npx", args: ["mcp-remote", url] } };
|
|
867
|
+
case "opencode":
|
|
868
|
+
return { key: "mcp", entry: { type: "remote", url, enabled: true } };
|
|
869
|
+
case "vscode":
|
|
870
|
+
return { key: "servers", entry: { type: "http", url } };
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function readText(io, path) {
|
|
874
|
+
try {
|
|
875
|
+
return String(await io.local().readFile(path, "utf8"));
|
|
876
|
+
} catch (e) {
|
|
877
|
+
if (e.code === "ENOENT") return null;
|
|
878
|
+
throw e;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
async function mergeJsonServer(io, path, key, entry) {
|
|
882
|
+
const text = await readText(io, path);
|
|
883
|
+
let doc = {};
|
|
884
|
+
if (text?.trim()) {
|
|
885
|
+
try {
|
|
886
|
+
doc = JSON.parse(text);
|
|
887
|
+
} catch {
|
|
888
|
+
throw new Error(
|
|
889
|
+
`${path} isn't plain JSON (comments?), so it was left as it is. Add the vibiz entry by hand: ${JSON.stringify({ [key]: { vibiz: entry } })}`
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
|
|
893
|
+
throw new Error(`${path} isn't a JSON object, so it was left as it is.`);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const servers = doc[key] ?? {};
|
|
897
|
+
if (typeof servers !== "object" || Array.isArray(servers)) {
|
|
898
|
+
throw new Error(`${path}: "${key}" isn't an object, so it was left as it is.`);
|
|
899
|
+
}
|
|
900
|
+
const before = servers.vibiz;
|
|
901
|
+
if (before && JSON.stringify(before) === JSON.stringify(entry)) return "unchanged";
|
|
902
|
+
doc[key] = { ...servers, vibiz: entry };
|
|
903
|
+
const local = io.local();
|
|
904
|
+
await local.mkdir(dirname2(path), { recursive: true });
|
|
905
|
+
await local.writeFile(path, `${JSON.stringify(doc, null, 2)}
|
|
906
|
+
`);
|
|
907
|
+
return before ? "updated" : "added";
|
|
908
|
+
}
|
|
909
|
+
async function mergeCodexToml(io, path, url) {
|
|
910
|
+
const text = await readText(io, path) ?? "";
|
|
911
|
+
if (/^\s*\[mcp_servers\.vibiz\]\s*$/m.test(text)) return "unchanged";
|
|
912
|
+
const local = io.local();
|
|
913
|
+
await local.mkdir(dirname2(path), { recursive: true });
|
|
914
|
+
const sep = text && !text.endsWith("\n") ? "\n\n" : text ? "\n" : "";
|
|
915
|
+
await local.writeFile(path, `${text}${sep}[mcp_servers.vibiz]
|
|
916
|
+
url = "${url}"
|
|
917
|
+
`);
|
|
918
|
+
return "added";
|
|
919
|
+
}
|
|
920
|
+
var run = (io, cmd, args) => io.local().exec(cmd, args, io.env);
|
|
921
|
+
async function mcpAdd(io, api, rawClients) {
|
|
922
|
+
const url = `${api}/mcp`;
|
|
923
|
+
const home = homeOf(io);
|
|
924
|
+
let failed = false;
|
|
925
|
+
for (const client of parseClients(rawClients)) {
|
|
926
|
+
const name = LABEL[client];
|
|
927
|
+
try {
|
|
928
|
+
if (client === "claude" && onPath("claude", io.env)) {
|
|
929
|
+
if (await run(io, "claude", ["mcp", "get", "vibiz"]) === 0) {
|
|
930
|
+
io.log(`\u25CF ${name}: already has a vibiz server (claude mcp get vibiz)`);
|
|
931
|
+
} else {
|
|
932
|
+
const code = await run(io, "claude", [
|
|
933
|
+
"mcp",
|
|
934
|
+
"add",
|
|
935
|
+
"--transport",
|
|
936
|
+
"http",
|
|
937
|
+
"vibiz",
|
|
938
|
+
"--scope",
|
|
939
|
+
"user",
|
|
940
|
+
url
|
|
941
|
+
]);
|
|
942
|
+
if (code !== 0) throw new Error(`claude mcp add exited with ${code}`);
|
|
943
|
+
io.log(`\u25CF ${name}: added ${url} (user scope)`);
|
|
944
|
+
}
|
|
945
|
+
io.log(" To sign in: open /mcp in Claude Code, select vibiz, choose Authenticate.");
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (client === "claude") {
|
|
949
|
+
const path2 = join3(home, ".claude.json");
|
|
950
|
+
const out2 = await mergeJsonServer(io, path2, "mcpServers", { type: "http", url });
|
|
951
|
+
io.log(`\u25CF ${name}: ${out2} in ${path2}`);
|
|
952
|
+
io.log(" To sign in: open /mcp in Claude Code, select vibiz, choose Authenticate.");
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (client === "codex" && onPath("codex", io.env)) {
|
|
956
|
+
if (await run(io, "codex", ["mcp", "get", "vibiz"]) === 0) {
|
|
957
|
+
io.log(`\u25CF ${name}: already has a vibiz server (codex mcp get vibiz)`);
|
|
958
|
+
} else {
|
|
959
|
+
const code = await run(io, "codex", ["mcp", "add", "vibiz", "--url", url]);
|
|
960
|
+
if (code !== 0) throw new Error(`codex mcp add exited with ${code}`);
|
|
961
|
+
io.log(`\u25CF ${name}: added ${url}`);
|
|
962
|
+
}
|
|
963
|
+
io.log(" Not signed in yet? Run: codex mcp login vibiz");
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (client === "codex") {
|
|
967
|
+
const path2 = join3(io.env.CODEX_HOME || join3(home, ".codex"), "config.toml");
|
|
968
|
+
const out2 = await mergeCodexToml(io, path2, url);
|
|
969
|
+
io.log(`\u25CF ${name}: ${out2} in ${path2}`);
|
|
970
|
+
io.log(" To sign in: codex mcp login vibiz");
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const path = mcpConfigPath(client, home);
|
|
974
|
+
const { key, entry } = jsonEntry(client, url);
|
|
975
|
+
const out = await mergeJsonServer(io, path, key, entry);
|
|
976
|
+
io.log(`\u25CF ${name}: ${out} in ${path}`);
|
|
977
|
+
io.log(` ${SIGN_IN_HINT[client]}`);
|
|
978
|
+
} catch (e) {
|
|
979
|
+
failed = true;
|
|
980
|
+
io.error(`\u2715 ${name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (failed) io.setExitCode(1);
|
|
984
|
+
}
|
|
985
|
+
var SIGN_IN_HINT = {
|
|
986
|
+
cursor: 'Cursor shows "Needs login" next to vibiz: click it to sign in.',
|
|
987
|
+
vscode: 'Run "MCP: List Servers", start vibiz and allow authentication.',
|
|
988
|
+
windsurf: "Refresh the MCP configuration in Windsurf; it opens the sign-in.",
|
|
989
|
+
gemini: "Run Gemini CLI and /mcp list: mcp-remote opens the sign-in.",
|
|
990
|
+
opencode: "Run /mcps in OpenCode, select vibiz and sign in."
|
|
991
|
+
};
|
|
992
|
+
var AGENTS = ["claude", "codex", "cursor"];
|
|
993
|
+
var AGENT_DIR = {
|
|
994
|
+
claude: ".claude/skills",
|
|
995
|
+
codex: ".agents/skills",
|
|
996
|
+
cursor: ".cursor/skills"
|
|
997
|
+
};
|
|
998
|
+
function detectAgent(env) {
|
|
999
|
+
if (env.CLAUDECODE) return "claude";
|
|
1000
|
+
if (Object.keys(env).some((k) => k.startsWith("CODEX_"))) return "codex";
|
|
1001
|
+
if (Object.keys(env).some((k) => k.startsWith("CURSOR_"))) return "cursor";
|
|
1002
|
+
return "claude";
|
|
1003
|
+
}
|
|
1004
|
+
function skillsDir(io, agent, global) {
|
|
1005
|
+
return join3(global ? homeOf(io) : io.local().cwd(), AGENT_DIR[agent]);
|
|
1006
|
+
}
|
|
1007
|
+
async function skillIndex(io, api) {
|
|
1008
|
+
const f = io.fetch ?? fetch;
|
|
1009
|
+
const res = await f(`${api}/skills/index.json`);
|
|
1010
|
+
if (!res.ok)
|
|
1011
|
+
throw new Error(`Could not list the skills (${res.status} from ${api}/skills/index.json)`);
|
|
1012
|
+
return await res.json();
|
|
1013
|
+
}
|
|
1014
|
+
function parseAgent(raw, env) {
|
|
1015
|
+
if (!raw) return detectAgent(env);
|
|
1016
|
+
const a = raw.toLowerCase().replace(/ code$/, "");
|
|
1017
|
+
if (!AGENTS.includes(a)) throw new Error("--agent is claude, codex or cursor");
|
|
1018
|
+
return a;
|
|
1019
|
+
}
|
|
1020
|
+
async function installed(io, dir, name) {
|
|
1021
|
+
return await readText(io, join3(dir, name, "SKILL.md")) !== null;
|
|
1022
|
+
}
|
|
1023
|
+
async function skillsList(io, api, opts) {
|
|
1024
|
+
const agent = parseAgent(opts.agent, io.env);
|
|
1025
|
+
const { skills } = await skillIndex(io, api);
|
|
1026
|
+
const global = skillsDir(io, agent, true);
|
|
1027
|
+
const project = skillsDir(io, agent, false);
|
|
1028
|
+
const rows = [];
|
|
1029
|
+
for (const s of skills) {
|
|
1030
|
+
const where = await installed(io, global, s.name) ? "global" : await installed(io, project, s.name) ? "project" : null;
|
|
1031
|
+
rows.push({ name: s.name, description: s.description, installed: where });
|
|
1032
|
+
}
|
|
1033
|
+
if (opts.json) {
|
|
1034
|
+
io.log(JSON.stringify({ agent, skills: rows }, null, 2));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
io.log(
|
|
1038
|
+
`Vibiz skills for ${agent === "claude" ? "Claude Code" : agent === "codex" ? "Codex" : "Cursor"}:`
|
|
1039
|
+
);
|
|
1040
|
+
for (const r of rows) {
|
|
1041
|
+
io.log(` ${r.installed ? "\u25CF" : "\u25CB"} ${r.name.padEnd(18)} ${r.installed ?? "not installed"}`);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function skillsAdd(io, api, names, opts) {
|
|
1045
|
+
const agent = parseAgent(opts.agent, io.env);
|
|
1046
|
+
const { skills } = await skillIndex(io, api);
|
|
1047
|
+
const pick = names.length ? skills.filter((s) => names.includes(s.name)) : skills;
|
|
1048
|
+
const unknown = names.filter((n) => !skills.some((s) => s.name === n));
|
|
1049
|
+
if (unknown.length) {
|
|
1050
|
+
throw new Error(`No skill named ${unknown.join(", ")}. Run: vibiz skills list`);
|
|
1051
|
+
}
|
|
1052
|
+
const dir = skillsDir(io, agent, Boolean(opts.global));
|
|
1053
|
+
const f = io.fetch ?? fetch;
|
|
1054
|
+
const local = io.local();
|
|
1055
|
+
const prefix = `${api}/skills/`;
|
|
1056
|
+
for (const s of pick) {
|
|
1057
|
+
for (const url of s.files) {
|
|
1058
|
+
const rel = url.startsWith(`${prefix}${s.name}/`) ? url.slice(prefix.length) : null;
|
|
1059
|
+
if (!rel || rel.split("/").some((p) => p === ".." || p === "")) {
|
|
1060
|
+
throw new Error(`Unexpected file in ${s.name}: ${url}`);
|
|
1061
|
+
}
|
|
1062
|
+
const res = await f(url);
|
|
1063
|
+
if (!res.ok) throw new Error(`Could not download ${url} (${res.status})`);
|
|
1064
|
+
const target = join3(dir, rel);
|
|
1065
|
+
await local.mkdir(dirname2(target), { recursive: true });
|
|
1066
|
+
await local.writeFile(target, await res.text());
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
io.log(`\u25CF Installed ${pick.length} skill${pick.length === 1 ? "" : "s"} in ${dir}`);
|
|
1070
|
+
for (const s of pick) io.log(` ${s.name}`);
|
|
1071
|
+
io.log(
|
|
1072
|
+
agent === "cursor" ? " Cursor loads them in a new chat." : " New skills load on their own; restart the session if one does not show up."
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// packages/cli/src/usage.ts
|
|
1077
|
+
var money = formatUsd;
|
|
1078
|
+
var unitsText = formatUnits;
|
|
1079
|
+
function table(head, rows, numeric = []) {
|
|
1080
|
+
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
1081
|
+
const cell = (v, i) => numeric.includes(i) ? v.padStart(widths[i] ?? 0) : v.padEnd(widths[i] ?? 0);
|
|
1082
|
+
const line = (r) => ` ${r.map((v, i) => cell(v, i)).join(" ").trimEnd()}`;
|
|
1083
|
+
return [line(head), line(widths.map((w) => "\u2500".repeat(w))), ...rows.map(line)].join("\n");
|
|
1084
|
+
}
|
|
1085
|
+
function rowLabel(r, groups) {
|
|
1086
|
+
return groups.map((g) => {
|
|
1087
|
+
if (g === "project") return r.project_name ?? r.project ?? "account";
|
|
1088
|
+
if (g === "api_key") return r.api_key_name ?? r.api_key ?? "system";
|
|
1089
|
+
if (g === "end_user") return r.end_user ?? "unattributed";
|
|
1090
|
+
const v = r[g];
|
|
1091
|
+
return v === null || v === void 0 ? "\u2013" : String(v);
|
|
1092
|
+
}).join(" \xB7 ");
|
|
1093
|
+
}
|
|
1094
|
+
var bar = (used, limit, width = 20) => {
|
|
1095
|
+
const f = limit > 0 ? Math.min(1, used / limit) : 0;
|
|
1096
|
+
const n = Math.round(f * width);
|
|
1097
|
+
return `${"\u2588".repeat(n)}${"\u2591".repeat(width - n)} ${Math.round(f * 100)}%`;
|
|
1098
|
+
};
|
|
1099
|
+
function formatUsage(r) {
|
|
1100
|
+
const out = [];
|
|
1101
|
+
out.push(`Usage, ${r.period} (${r.from.slice(0, 10)} \u2192 ${r.to.slice(0, 10)})`);
|
|
1102
|
+
out.push(
|
|
1103
|
+
` billed ${money(r.totals.billed_usd)} \xB7 cost ${money(r.totals.cost_usd)} \xB7 ${r.totals.records} records`
|
|
1104
|
+
);
|
|
1105
|
+
out.push("");
|
|
1106
|
+
if (!r.data.length) out.push(" No usage in this period.");
|
|
1107
|
+
else {
|
|
1108
|
+
out.push(
|
|
1109
|
+
table(
|
|
1110
|
+
[r.group_by.join(" \xB7 "), "units", "cost", "billed"],
|
|
1111
|
+
r.data.map((row) => [
|
|
1112
|
+
rowLabel(row, r.group_by),
|
|
1113
|
+
unitsText(row.units),
|
|
1114
|
+
money(row.cost_usd),
|
|
1115
|
+
money(row.billed_usd)
|
|
1116
|
+
]),
|
|
1117
|
+
[2, 3]
|
|
1118
|
+
)
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
const l = r.limits;
|
|
1122
|
+
if (l) {
|
|
1123
|
+
out.push("", " limits (today, resets at UTC midnight)");
|
|
1124
|
+
out.push(
|
|
1125
|
+
` daily spend ${bar(l.daily_spend.spent_usd, l.daily_spend.limit_usd)} ${money(l.daily_spend.spent_usd)} of ${money(l.daily_spend.limit_usd)}`
|
|
1126
|
+
);
|
|
1127
|
+
out.push(
|
|
1128
|
+
` projects ${bar(l.projects.used, l.projects.limit)} ${l.projects.used} of ${l.projects.limit}`
|
|
1129
|
+
);
|
|
1130
|
+
for (const b of l.builds_today.filter((x) => x.used > 0)) {
|
|
1131
|
+
out.push(
|
|
1132
|
+
` builds ${bar(b.used, b.limit)} ${b.used} of ${b.limit} ${b.project_name}`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
for (const a of l.ai_budgets.filter((x) => x.daily_budget_usd !== null)) {
|
|
1136
|
+
out.push(
|
|
1137
|
+
` AI budget ${bar(a.spent_today_usd, a.daily_budget_usd ?? 0)} ${money(a.spent_today_usd)} of ${money(a.daily_budget_usd ?? 0)} ${a.project_id}`
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
for (const e of l.email_today) {
|
|
1141
|
+
out.push(` emails ${bar(e.used, e.limit)} ${e.used} of ${e.limit} ${e.project_id}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return out.join("\n");
|
|
1145
|
+
}
|
|
1146
|
+
var ms = (v) => v === null ? "\u2013" : `${v} ms`;
|
|
1147
|
+
var pct = (v) => `${(v * 100).toFixed(1)}%`;
|
|
1148
|
+
function formatMetrics(r) {
|
|
1149
|
+
const t = r.totals;
|
|
1150
|
+
const out = [
|
|
1151
|
+
`Requests, ${r.period}`,
|
|
1152
|
+
` ${t.requests} requests \xB7 ${pct(t.error_rate)} errors (5xx) \xB7 p50 ${ms(t.p50_ms)} \xB7 p95 ${ms(t.p95_ms)}`,
|
|
1153
|
+
""
|
|
1154
|
+
];
|
|
1155
|
+
if (!r.data.length) out.push(" No requests in this period.");
|
|
1156
|
+
else {
|
|
1157
|
+
out.push(
|
|
1158
|
+
table(
|
|
1159
|
+
["capability", "requests", "5xx", "4xx", "p50", "p95", "p99"],
|
|
1160
|
+
r.data.map((d) => [
|
|
1161
|
+
d.capability,
|
|
1162
|
+
String(d.requests),
|
|
1163
|
+
pct(d.error_rate),
|
|
1164
|
+
String(d.errors_4xx),
|
|
1165
|
+
ms(d.p50_ms),
|
|
1166
|
+
ms(d.p95_ms),
|
|
1167
|
+
ms(d.p99_ms)
|
|
1168
|
+
]),
|
|
1169
|
+
[1, 2, 3, 4, 5, 6]
|
|
1170
|
+
)
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
return out.join("\n");
|
|
1174
|
+
}
|
|
1175
|
+
function formatAdminMetrics(m) {
|
|
1176
|
+
const out = [
|
|
1177
|
+
`Platform, ${m.period}`,
|
|
1178
|
+
` tenants ${m.tenants.active} active of ${m.tenants.total}`,
|
|
1179
|
+
` revenue ${money(m.revenue.billed_usd)} \xB7 cost ${money(m.revenue.cost_usd)} \xB7 margin ${money(m.revenue.margin_usd)}${m.revenue.margin_pct === null ? "" : ` (${m.revenue.margin_pct}%)`} \xB7 ${m.pricing.default_markup_pct === 0 && m.pricing.rules === 0 ? "markup 0 % (pricing not set)" : `markup ${m.pricing.default_markup_pct} %`}`,
|
|
1180
|
+
` north-star ${m.north_star.value}: ${m.north_star.definition}`,
|
|
1181
|
+
` queue ${m.queue.queued} queued, oldest ${m.queue.oldest_queued_age_seconds}s \xB7 ${m.queue.running} running \xB7 ${m.queue.failed_24h} failed in 24 h`,
|
|
1182
|
+
""
|
|
1183
|
+
];
|
|
1184
|
+
const section = (title, rows, groups) => {
|
|
1185
|
+
if (!rows.length) return;
|
|
1186
|
+
out.push(
|
|
1187
|
+
` ${title}`,
|
|
1188
|
+
table(
|
|
1189
|
+
[groups.join(" \xB7 "), "units", "cost", "billed"],
|
|
1190
|
+
rows.map((r) => [
|
|
1191
|
+
groups.map((g) => (g === "tenant" ? r.tenant_name ?? r.tenant : r[g]) || "\u2013").join(" \xB7 "),
|
|
1192
|
+
unitsText(r.units),
|
|
1193
|
+
money(r.cost_usd),
|
|
1194
|
+
money(r.billed_usd)
|
|
1195
|
+
]),
|
|
1196
|
+
[2, 3]
|
|
1197
|
+
),
|
|
1198
|
+
""
|
|
1199
|
+
);
|
|
1200
|
+
};
|
|
1201
|
+
section("by capability", m.revenue.by_capability, ["capability"]);
|
|
1202
|
+
section("by partner", m.spend.by_provider, ["provider"]);
|
|
1203
|
+
section("by model", m.spend.by_model, ["model", "provider"]);
|
|
1204
|
+
section("top tenants", m.top_tenants, ["tenant"]);
|
|
1205
|
+
if (m.providers.length) {
|
|
1206
|
+
out.push(
|
|
1207
|
+
" partner health",
|
|
1208
|
+
table(
|
|
1209
|
+
["partner", "calls", "errors", "p50", "p95"],
|
|
1210
|
+
m.providers.map((p) => [
|
|
1211
|
+
p.provider,
|
|
1212
|
+
String(p.calls),
|
|
1213
|
+
pct(p.error_rate),
|
|
1214
|
+
ms(p.p50_ms),
|
|
1215
|
+
ms(p.p95_ms)
|
|
1216
|
+
]),
|
|
1217
|
+
[1, 2, 3, 4]
|
|
1218
|
+
),
|
|
1219
|
+
""
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (m.requests.rows.length) {
|
|
1223
|
+
out.push(
|
|
1224
|
+
" routes",
|
|
1225
|
+
table(
|
|
1226
|
+
["route", "requests", "5xx", "p50", "p95", "p99"],
|
|
1227
|
+
m.requests.rows.slice(0, 20).map((r) => [
|
|
1228
|
+
`${r.method} ${r.route}`,
|
|
1229
|
+
String(r.requests),
|
|
1230
|
+
pct(r.error_rate),
|
|
1231
|
+
ms(r.p50_ms),
|
|
1232
|
+
ms(r.p95_ms),
|
|
1233
|
+
ms(r.p99_ms)
|
|
1234
|
+
]),
|
|
1235
|
+
[1, 2, 3, 4, 5]
|
|
1236
|
+
)
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
return out.join("\n");
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// packages/cli/src/commands.ts
|
|
1243
|
+
var CONFIG_DIR = join4(homedir2(), ".vibiz");
|
|
1244
|
+
var CONFIG_FILE = join4(CONFIG_DIR, "config.json");
|
|
1245
|
+
var PROJECT_FILE = "vibiz.json";
|
|
1246
|
+
var HELP = `vibiz \u2014 website, database and sign-in for a new business, in one command
|
|
1247
|
+
|
|
1248
|
+
vibiz login log in from the browser: prints a link, you click
|
|
1249
|
+
Authorize, a new agent key is saved here
|
|
1250
|
+
[--key <vz_sk_\u2026>] [--api <url>] [--no-browser] or save a key you already have
|
|
1251
|
+
vibiz signup the same as login: no account yet? one is opened
|
|
1252
|
+
for you on the way
|
|
1253
|
+
vibiz whoami the account and key this machine uses
|
|
1254
|
+
vibiz logout forget this machine's key (a browser login's key is
|
|
1255
|
+
revoked too)
|
|
1256
|
+
vibiz init <name> [--resources a,b] create a project here (default: hosting,database,auth)
|
|
1257
|
+
[--template nextjs-saas] [--force] \u2026first copying a starter app into this folder
|
|
1258
|
+
vibiz status this project, its resources and deployments
|
|
1259
|
+
vibiz deploy [dir] [--preview] [--project <id>]
|
|
1260
|
+
upload and deploy live (or a preview with --preview)
|
|
1261
|
+
vibiz promote <deployment-id> make a deployment the live site
|
|
1262
|
+
vibiz rollback go back to the previous production deployment
|
|
1263
|
+
vibiz dev [--write <file>] [--reset] the development env (own database branch with your
|
|
1264
|
+
schema and no production rows, own sign-in, own AI
|
|
1265
|
+
key) into .env.development.local
|
|
1266
|
+
vibiz dev -- <command\u2026> run a command with the development env, no file
|
|
1267
|
+
vibiz env [--write <file>] the PRODUCTION env (owner keys only), to paste into
|
|
1268
|
+
a host outside Vibiz; agents use \`vibiz dev\`
|
|
1269
|
+
vibiz domains [add|rm <domain>] where the app runs outside Vibiz hosting; sign-in
|
|
1270
|
+
trusts these (owner keys only)
|
|
1271
|
+
vibiz auth [--dev] [--json] how people sign in: providers, callback URLs, email
|
|
1272
|
+
(production for owner keys; --dev the development copy)
|
|
1273
|
+
vibiz auth providers add <google|github|vercel> [--own --client-id <id>] [--dev]
|
|
1274
|
+
turn on a social sign-in; --own uses your OAuth app,
|
|
1275
|
+
its secret from <PROVIDER>_CLIENT_SECRET or a prompt
|
|
1276
|
+
vibiz auth providers rm <provider> [--dev] turn one off
|
|
1277
|
+
vibiz auth domains <a.com,b.com|none> [--dev] only these email domains may sign up
|
|
1278
|
+
vibiz company [--dev] [--json] the business behind this project: status, what blocks
|
|
1279
|
+
filing, the next step (production for owner keys)
|
|
1280
|
+
vibiz company draft --file company.json [--dev] create or replace the draft (docs/COMPANY.md)
|
|
1281
|
+
vibiz company submit [--dev] file it; production shows the price and asks you to
|
|
1282
|
+
type the company's name (real filing, real fees)
|
|
1283
|
+
vibiz company sign <ss4|form_8821> [--dev] a fresh signing link, for you only
|
|
1284
|
+
vibiz company docs [download <id>] [--dev] formation documents, or a short-lived download link
|
|
1285
|
+
vibiz company simulate <formation|ein> development only: complete that step at the sandbox
|
|
1286
|
+
vibiz payments [--dev] [--json] take payments: balance, getting paid, the next step
|
|
1287
|
+
(production for owner keys; --dev test mode)
|
|
1288
|
+
vibiz payments products [--dev] the catalog; add one with
|
|
1289
|
+
products add <name> --amount <cents> [--interval month|year] [--lookup-key k]
|
|
1290
|
+
vibiz payments checkout <price_id|lookup_key> --success-url <u> --cancel-url <u> [--user id]
|
|
1291
|
+
a checkout link to pay right now
|
|
1292
|
+
vibiz payments list [--dev] payments, newest first
|
|
1293
|
+
vibiz payments webhook <url|none> [--dev] where Vibiz POSTs payment events (the app verifies
|
|
1294
|
+
them with VIBIZ_WEBHOOK_SECRET: vibiz docs webhooks)
|
|
1295
|
+
vibiz payments withdraw [--amount <cents>] [--dev]
|
|
1296
|
+
send the balance to your bank; shows the amount and
|
|
1297
|
+
asks you to confirm
|
|
1298
|
+
vibiz payments simulate <payment|dispute|release_hold> [--amount <cents>]
|
|
1299
|
+
vibiz payments simulate refund <payment_id> development only: a test-mode payment, dispute,
|
|
1300
|
+
refund, or release the hold
|
|
1301
|
+
(fees, the hold, refunds and webhooks: vibiz docs payments)
|
|
1302
|
+
vibiz social [--dev] [--json] the project's social and ad accounts, posts waiting for
|
|
1303
|
+
you, scheduled and failed (docs/SOCIAL.md); --dev is the
|
|
1304
|
+
sandbox: nothing is posted, nothing is spent
|
|
1305
|
+
vibiz social connect <platform> [--ads] [--dev] [--open]
|
|
1306
|
+
a link to sign in to instagram, facebook, tiktok, linkedin,
|
|
1307
|
+
x, youtube, pinterest, threads, bluesky, reddit,
|
|
1308
|
+
google_business, telegram (--ads: meta, google, tiktok,
|
|
1309
|
+
linkedin, pinterest, x)
|
|
1310
|
+
vibiz social accounts [--dev] [--json] connected accounts and their ids
|
|
1311
|
+
vibiz social post "<text>" --to <account,\u2026> [--media <file|url>]\u2026 [--at <iso>] [--draft]
|
|
1312
|
+
[--opt key=value]\u2026 [--dev] publish now, schedule or draft; a live post asks you to
|
|
1313
|
+
confirm (an agent key's post waits for the owner)
|
|
1314
|
+
vibiz social posts [--status s] [--dev] posts, newest first
|
|
1315
|
+
vibiz social approve|reject|cancel <post> [--dev] decide a waiting post, or cancel one
|
|
1316
|
+
vibiz social campaigns [--dev] ad campaigns with budget and spend
|
|
1317
|
+
vibiz social pause|resume <campaign> [--dev] | budget <campaign> --budget <cents>
|
|
1318
|
+
change a campaign (live: you confirm; agents ask the owner)
|
|
1319
|
+
vibiz connectors [list] [--json] your own APIs, called through Vibiz, which holds their
|
|
1320
|
+
keys (docs/CONNECTORS.md)
|
|
1321
|
+
vibiz connectors import <spec-url|file> [--name n] [--enable op,op] [--add]
|
|
1322
|
+
preview an OpenAPI 3 spec (JSON or YAML): its operations
|
|
1323
|
+
and what could not be taken; --add saves it (reads on)
|
|
1324
|
+
vibiz connectors add --file manifest.json add one from a manifest, or a short one:
|
|
1325
|
+
add <name> <base_url> "GET /path/{id}?q" "POST /path" \u2026 [--auth none|bearer|header:X-Api-Key|query:appid] [--var NAME]
|
|
1326
|
+
vibiz connectors ops <name> [--json] its operations: risk, on/off, parameters
|
|
1327
|
+
vibiz connectors enable|disable <name> <op\u2026> allow (or stop) operations
|
|
1328
|
+
vibiz connectors secret <name> <VAR> [--dev] set a key from $VAR or a hidden prompt; never printed.
|
|
1329
|
+
Production needs an owner key
|
|
1330
|
+
vibiz connectors call <name> <op> [key=value\u2026] [--body '<json>'] [--dev] [--approval <id>]
|
|
1331
|
+
call it; dangerous operations (and production writes)
|
|
1332
|
+
by an agent key wait for the owner's approval
|
|
1333
|
+
vibiz connectors check <name> [--dev] does the key work
|
|
1334
|
+
vibiz connectors logs <name> [--dev] recent calls (no content)
|
|
1335
|
+
vibiz connectors approvals | approve <id> | deny <id> calls waiting for the owner (owner key)
|
|
1336
|
+
vibiz connect where does this app deploy? how that host gets its env
|
|
1337
|
+
vibiz run -- <command\u2026> with VIBIZ_APP_KEY set (a server, a container, CI): run
|
|
1338
|
+
the command with the app's env; any language
|
|
1339
|
+
vibiz env pull [--format dotenv|shell|docker|json] [-o file]
|
|
1340
|
+
the app's env from VIBIZ_APP_KEY, for a build step
|
|
1341
|
+
vibiz hosts hosts outside Vibiz that get the env server to server
|
|
1342
|
+
vibiz hosts add <host> <app> [field=value\u2026] [--dev]
|
|
1343
|
+
connect your own host so Vibiz keeps its env in sync
|
|
1344
|
+
(owner key): vercel, netlify, render, railway, fly,
|
|
1345
|
+
heroku, cloudflare-workers, cloudflare-pages,
|
|
1346
|
+
github-actions. The token comes from the host's usual
|
|
1347
|
+
variable (VERCEL_TOKEN, FLY_API_TOKEN\u2026) or a hidden
|
|
1348
|
+
prompt. Any other host: vibiz connect
|
|
1349
|
+
vibiz hosts sync | rm <id> [--keep-values] push again now, or disconnect
|
|
1350
|
+
vibiz app-key [rotate [--dev] [--grace <s>]] the app's one key (VIBIZ_APP_KEY); never printed
|
|
1351
|
+
vibiz add <hosting|database|auth|email|analytics|replay> add a resource to this project
|
|
1352
|
+
vibiz analytics [--period 24h|7d|30d] [--json] visitors, pages, sources, events
|
|
1353
|
+
vibiz replays [--errors] [--rage] [--dead] [--path /x] [--period 7d] [--json]
|
|
1354
|
+
what went wrong for visitors, then sessions to watch
|
|
1355
|
+
vibiz replay <id> [--json] one session's signals and a link to watch it
|
|
1356
|
+
vibiz projects list your projects
|
|
1357
|
+
vibiz run <action> --input '<json>' run an action (web.scrape, \u2026)
|
|
1358
|
+
vibiz send --to <addr> --subject <s> --text <body> [--from info]
|
|
1359
|
+
send email from this project's address
|
|
1360
|
+
vibiz ai "<prompt>" [--model m] [--system s] stream an answer from a language model (metered)
|
|
1361
|
+
--model: a Vibiz model (vibiz/smart) or any lab model
|
|
1362
|
+
(anthropic/claude-sonnet-5.5): app.vibiz.app/docs/ai
|
|
1363
|
+
vibiz ai models [search\u2026] [--all] [--json] the Vibiz models and the model each runs, with prices;
|
|
1364
|
+
--all (or a search) lists every model you can call
|
|
1365
|
+
vibiz ai keys [set|test|remove google] [--project id]
|
|
1366
|
+
your own Google key: calls it serves run only on it
|
|
1367
|
+
and cost 0 here. set reads GEMINI_API_KEY or a hidden
|
|
1368
|
+
prompt; --vertex sa.json [--gcp-project p] [--region r]
|
|
1369
|
+
for Vertex AI (Google Cloud credits)
|
|
1370
|
+
vibiz image "<prompt>" -o out.png [--size 1024x1024] [--model m] generate an image
|
|
1371
|
+
vibiz say "<text>" -o out.mp3 [--voice alloy] text to speech
|
|
1372
|
+
vibiz transcribe <audio-file> [--language en] speech to text
|
|
1373
|
+
vibiz usage [--period 24h|7d|30d|month] [--by tool|model|alias|project|api_key|end_user|day|capability]
|
|
1374
|
+
[--project id] [--model m] [--tool t] [--end-user u] [--api-key k] [--json] [--open]
|
|
1375
|
+
what you used, what it cost, how close you are to limits
|
|
1376
|
+
vibiz billing [--json] the account's credits: balance, card, auto-recharge
|
|
1377
|
+
vibiz billing top-up <dollars> [--open] a payment link to add credits (a person pays there)
|
|
1378
|
+
vibiz billing history [--kind usage,topup] what came in and what used it
|
|
1379
|
+
vibiz billing auto-recharge on|off [--threshold 5] [--amount 25] (owner key)
|
|
1380
|
+
vibiz metrics [--period 7d] [--json] request counts, error rate and latency by capability
|
|
1381
|
+
vibiz keys [create <name> [--owner] | revoke <id>]
|
|
1382
|
+
named API keys, one per person or agent; agent keys
|
|
1383
|
+
(the default) never read production secrets
|
|
1384
|
+
vibiz admin metrics [--period 7d] [--json] [--open] the platform view (admin key)
|
|
1385
|
+
vibiz mcp serve the Vibiz tools to AI agents (MCP, stdio)
|
|
1386
|
+
vibiz mcp add --clients "<client>[,\u2026]" connect a coding agent to the remote MCP server once:
|
|
1387
|
+
"Claude Code", Codex, Cursor, "VS Code with Copilot",
|
|
1388
|
+
Windsurf, "Gemini CLI", OpenCode (merges, never replaces)
|
|
1389
|
+
vibiz skills [list] the agent skills, and which this agent has
|
|
1390
|
+
vibiz skills add [name\u2026] [--global] [--agent claude|codex|cursor]
|
|
1391
|
+
install them (default: all, this folder, this agent)
|
|
1392
|
+
vibiz docs [topic] short guides: payments, app-payments (checkout and
|
|
1393
|
+
subscription code), webhooks, company, keys, usage,
|
|
1394
|
+
connectors, social
|
|
1395
|
+
|
|
1396
|
+
Env: VIBIZ_API_KEY, VIBIZ_API_URL override the saved login.
|
|
1397
|
+
AI commands bill the account (or --project <id>); each prints what it cost.`;
|
|
1398
|
+
var TOKEN_ENV = {
|
|
1399
|
+
vercel: "VERCEL_TOKEN",
|
|
1400
|
+
netlify: "NETLIFY_AUTH_TOKEN",
|
|
1401
|
+
render: "RENDER_API_KEY",
|
|
1402
|
+
railway: "RAILWAY_TOKEN",
|
|
1403
|
+
fly: "FLY_API_TOKEN",
|
|
1404
|
+
heroku: "HEROKU_API_KEY",
|
|
1405
|
+
"cloudflare-workers": "CLOUDFLARE_API_TOKEN",
|
|
1406
|
+
"cloudflare-pages": "CLOUDFLARE_API_TOKEN",
|
|
1407
|
+
"github-actions": "GITHUB_TOKEN"
|
|
1408
|
+
};
|
|
1409
|
+
var usd = (micro) => `$${(micro / 1e6).toFixed(micro < 1e4 ? 6 : 4)}`;
|
|
1410
|
+
var dot = (status) => ({ ready: "\u25CF", failed: "\u2715", succeeded: "\u25CF", building: "\u25D0", provisioning: "\u25D0" })[status] ?? "\u25CB";
|
|
1411
|
+
function printProject(io, p) {
|
|
1412
|
+
io.log(`${p.name} (${p.id})`);
|
|
1413
|
+
if (p.url) io.log(` ${p.url}`);
|
|
1414
|
+
for (const r of p.resources) {
|
|
1415
|
+
const detail = r.error ? ` ${r.error.message}` : "";
|
|
1416
|
+
io.log(` ${dot(r.status)} ${r.type.padEnd(9)} ${r.status}${detail}`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
function printAuth(io, a) {
|
|
1420
|
+
const on = (b) => b ? "on" : "off";
|
|
1421
|
+
io.log(`Sign-in (${a.environment}) ${a.base_url}`);
|
|
1422
|
+
if (a.app_name) io.log(` app name in emails: ${a.app_name}`);
|
|
1423
|
+
const ep = a.email_password;
|
|
1424
|
+
io.log(
|
|
1425
|
+
` email + password ${on(ep.enabled)} \xB7 verification ${ep.require_email_verification ? `required (${ep.email_verification_method})` : "optional"} \xB7 sign-up ${ep.disable_sign_up ? "closed" : "open"}`
|
|
1426
|
+
);
|
|
1427
|
+
io.log(
|
|
1428
|
+
` magic link ${on(a.magic_link.enabled)}${a.magic_link.enabled ? ` (${a.magic_link.expires_in_minutes} min)` : ""} \xB7 localhost ${on(a.allow_localhost)}`
|
|
1429
|
+
);
|
|
1430
|
+
io.log("\n providers");
|
|
1431
|
+
if (!a.providers.length) io.log(" none");
|
|
1432
|
+
for (const p of a.providers) {
|
|
1433
|
+
io.log(
|
|
1434
|
+
` ${dot("ready")} ${p.id.padEnd(7)} ${p.credentials === "own" ? `own app ${p.client_id}` : "shared keys"}`
|
|
1435
|
+
);
|
|
1436
|
+
if (p.credentials === "own") io.log(` callback: ${p.callback_url}`);
|
|
1437
|
+
}
|
|
1438
|
+
io.log(
|
|
1439
|
+
`
|
|
1440
|
+
sign-up: ${a.allowed_email_domains.length ? a.allowed_email_domains.map((d) => `@${d}`).join(", ") : "any email"}`
|
|
1441
|
+
);
|
|
1442
|
+
io.log(
|
|
1443
|
+
` email from: ${a.email_sender.type === "smtp" ? `${a.email_sender.sender_email} via ${a.email_sender.host}:${a.email_sender.port}` : "the shared sender"}`
|
|
1444
|
+
);
|
|
1445
|
+
if (a.trusted_origins.length) io.log(` trusted: ${a.trusted_origins.join(", ")}`);
|
|
1446
|
+
}
|
|
1447
|
+
var cents = (n) => `$${(n / 100).toFixed(2)}`;
|
|
1448
|
+
var companyName = (c) => c.legal_name ?? (c.names[0] ? `${c.names[0].name} ${c.names[0].ending}` : "(no name yet)");
|
|
1449
|
+
function printCompany(io, c) {
|
|
1450
|
+
const label = c.entity_type === "llc" ? "LLC" : "C-Corp";
|
|
1451
|
+
io.log(`${companyName(c)} ${label} in ${c.state} (${c.environment}) ${c.status}`);
|
|
1452
|
+
if (c.filing_number) io.log(` filed ${c.formed_on ?? ""} \xB7 filing number ${c.filing_number}`);
|
|
1453
|
+
if (c.ein_number) io.log(` EIN ${c.ein_number}`);
|
|
1454
|
+
const price = c.price ?? c.quote;
|
|
1455
|
+
if (price) {
|
|
1456
|
+
io.log(
|
|
1457
|
+
` ${c.price ? "price" : "quote"}: ${cents(price.total_cents)} (state ${cents(price.state_fee_cents)} + service ${cents(price.service_fee_cents)})`
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
const open = c.actions.filter((a) => a.open);
|
|
1461
|
+
const pending = c.signatures.filter((x) => x.status === "pending");
|
|
1462
|
+
if (open.length || pending.length) {
|
|
1463
|
+
io.log("\n needs you");
|
|
1464
|
+
for (const a of open) io.log(` ! ${a.code}: ${a.reason}`);
|
|
1465
|
+
for (const x of pending) io.log(` ! sign ${x.document}: vibiz company sign ${x.document}`);
|
|
1466
|
+
}
|
|
1467
|
+
if (c.problems.length) {
|
|
1468
|
+
io.log("\n before it can be filed");
|
|
1469
|
+
for (const p of c.problems) io.log(` - ${p.path}: ${p.message}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (c.documents.length) io.log(`
|
|
1472
|
+
documents: ${c.documents.length} (vibiz company docs)`);
|
|
1473
|
+
const next = c.status === "draft" ? c.problems.length ? "fix the draft, then vibiz company draft --file company.json" : "vibiz company submit" : c.status === "failed" ? "it could not be filed: contact support, or start a new draft" : c.status === "completed" ? null : "wait: it updates by itself (vibiz company to check)";
|
|
1474
|
+
if (next) io.log(`
|
|
1475
|
+
next: ${next}`);
|
|
1476
|
+
}
|
|
1477
|
+
function printPayments(io, a) {
|
|
1478
|
+
const b = a.balance;
|
|
1479
|
+
io.log(`Payments (${a.environment}) ${a.status} statement: ${a.statement_descriptor}`);
|
|
1480
|
+
io.log(
|
|
1481
|
+
` available ${cents(b.available_cents)} \xB7 pending ${cents(b.pending_cents)}${b.next_available_at ? ` (from ${b.next_available_at.slice(0, 10)})` : ""}`
|
|
1482
|
+
);
|
|
1483
|
+
if (b.negative_cents) {
|
|
1484
|
+
io.log(
|
|
1485
|
+
` negative ${cents(-b.negative_cents)}: refunds or disputes, earned back before you can withdraw`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
io.log(` in transit ${cents(b.in_transit_cents)} \xB7 withdrawn ${cents(b.withdrawn_cents)}`);
|
|
1489
|
+
const p = a.payouts;
|
|
1490
|
+
io.log(`
|
|
1491
|
+
getting paid: ${p.status}${p.bank ? ` (bank \u2022\u2022\u2022\u2022 ${p.bank.last4})` : ""}`);
|
|
1492
|
+
for (const r of p.requirements) io.log(` - ${r}`);
|
|
1493
|
+
if (a.webhook.url) io.log(`
|
|
1494
|
+
webhook: ${a.webhook.url}`);
|
|
1495
|
+
const next = a.status === "frozen" ? "payments are paused by Vibiz: contact support" : p.status !== "ready" ? "verify your identity and add a bank in the console (Payments \u25B8 Get paid); your app can already sell" : b.available_cents > 0 ? "vibiz payments withdraw" : null;
|
|
1496
|
+
if (next) io.log(`
|
|
1497
|
+
next: ${next}`);
|
|
1498
|
+
}
|
|
1499
|
+
var pct2 = (n) => n === null ? "\u2013" : `${Math.round(n * 100)}%`;
|
|
1500
|
+
function printAnalytics(io, r) {
|
|
1501
|
+
const t = r.totals;
|
|
1502
|
+
io.log(`Last ${r.period}`);
|
|
1503
|
+
io.log(
|
|
1504
|
+
` ${t.visitors} visitors \xB7 ${t.pageviews} pageviews \xB7 bounce ${pct2(t.bounce_rate)} \xB7 ${t.events} events`
|
|
1505
|
+
);
|
|
1506
|
+
const peak = Math.max(1, ...r.timeseries.map((p) => p.visitors));
|
|
1507
|
+
const bars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1508
|
+
io.log(
|
|
1509
|
+
` ${r.timeseries.map((p) => bars[Math.round(p.visitors / peak * (bars.length - 1))]).join("")}`
|
|
1510
|
+
);
|
|
1511
|
+
const table2 = (title, rows) => {
|
|
1512
|
+
if (!rows.length) return;
|
|
1513
|
+
io.log(`
|
|
1514
|
+
${title}`);
|
|
1515
|
+
for (const [label, n] of rows) io.log(` ${String(n).padStart(6)} ${label}`);
|
|
1516
|
+
};
|
|
1517
|
+
table2(
|
|
1518
|
+
"top pages (pageviews)",
|
|
1519
|
+
r.top_pages.map((p) => [p.path, p.pageviews])
|
|
1520
|
+
);
|
|
1521
|
+
table2(
|
|
1522
|
+
"sources (visitors)",
|
|
1523
|
+
r.sources.map((s) => [s.source, s.visitors])
|
|
1524
|
+
);
|
|
1525
|
+
table2(
|
|
1526
|
+
"countries (visitors)",
|
|
1527
|
+
r.countries.map((c) => [c.country, c.visitors])
|
|
1528
|
+
);
|
|
1529
|
+
table2(
|
|
1530
|
+
"events",
|
|
1531
|
+
r.events.map((e) => [e.name, e.count])
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
var clock = (ms2) => {
|
|
1535
|
+
const sec = Math.round(ms2 / 1e3);
|
|
1536
|
+
return `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`;
|
|
1537
|
+
};
|
|
1538
|
+
function printInsights(io, r) {
|
|
1539
|
+
io.log(`Session replay, last ${r.period}: ${r.recorded_sessions} recorded sessions`);
|
|
1540
|
+
if (!r.insights.length) {
|
|
1541
|
+
io.log(" nothing stands out");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
for (const i of r.insights.slice(0, 10)) {
|
|
1545
|
+
io.log(` \u2022 ${i.summary}`);
|
|
1546
|
+
io.log(` e.g. ${i.examples.map((e) => e.id).join(", ")}`);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function printReplay(io, d, url, expires) {
|
|
1550
|
+
io.log(
|
|
1551
|
+
`${d.id} ${new Date(d.started_at).toLocaleString()} (${d.device ?? "unknown device"}${d.country ? `, ${d.country}` : ""})`
|
|
1552
|
+
);
|
|
1553
|
+
io.log(
|
|
1554
|
+
` ${clock(d.duration_ms)} long, ${clock(d.active_ms)} active \xB7 recorded because: ${d.trigger}`
|
|
1555
|
+
);
|
|
1556
|
+
io.log(` ${d.pages.join(" \u2192 ")}`);
|
|
1557
|
+
if (!d.completeness.complete) {
|
|
1558
|
+
const gaps = d.completeness.missing_seqs?.map(([a, b]) => a === b ? `${a}` : `${a}-${b}`).join(", ");
|
|
1559
|
+
io.log(
|
|
1560
|
+
` partial recording${gaps ? `: missing chunks ${gaps}` : ""}${d.completeness.dropped_events ? `, ${d.completeness.dropped_events} events dropped` : ""}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
const s = d.signals;
|
|
1564
|
+
for (const e of s.errors)
|
|
1565
|
+
io.log(
|
|
1566
|
+
` \u2715 error ${clock(e.offset_ms)} on ${e.page}: "${e.recorded_message}"${e.count > 1 ? ` \xD7${e.count}` : ""}`
|
|
1567
|
+
);
|
|
1568
|
+
for (const r of s.rage_clicks)
|
|
1569
|
+
io.log(
|
|
1570
|
+
` ! rage click ${clock(r.offset_ms)} on ${r.page}: ${r.selector} "${r.recorded_text}" (${r.clicks} clicks)`
|
|
1571
|
+
);
|
|
1572
|
+
for (const x of s.dead_clicks)
|
|
1573
|
+
io.log(` \xB7 dead click ${clock(x.offset_ms)} on ${x.page}: ${x.selector} "${x.recorded_text}"`);
|
|
1574
|
+
for (const f of s.forms_abandoned) io.log(` \xB7 left ${f.form} on ${f.page} unsent`);
|
|
1575
|
+
io.log(`
|
|
1576
|
+
timeline`);
|
|
1577
|
+
for (const m of d.timeline.slice(0, 40))
|
|
1578
|
+
io.log(` ${clock(m.offset_ms).padStart(6)} ${m.kind.padEnd(14)} ${m.label}`);
|
|
1579
|
+
io.log(`
|
|
1580
|
+
Watch (until ${new Date(expires).toLocaleTimeString()}): ${url}`);
|
|
1581
|
+
}
|
|
1582
|
+
function commandHelp(command) {
|
|
1583
|
+
const lines = HELP.split("\n");
|
|
1584
|
+
const out = [];
|
|
1585
|
+
let inside = false;
|
|
1586
|
+
for (const line of lines) {
|
|
1587
|
+
if (/^ {2}vibiz /.test(line)) inside = line.startsWith(` vibiz ${command}`);
|
|
1588
|
+
else if (!/^ {5,}/.test(line)) inside = false;
|
|
1589
|
+
if (inside) out.push(line);
|
|
1590
|
+
}
|
|
1591
|
+
return out.length ? `${out.join("\n")}
|
|
1592
|
+
|
|
1593
|
+
Everything: vibiz help` : HELP;
|
|
1594
|
+
}
|
|
1595
|
+
async function runCli(argv2, io) {
|
|
1596
|
+
const [command, ...all] = argv2;
|
|
1597
|
+
const cut = all.indexOf("--");
|
|
1598
|
+
const rest = cut < 0 ? all : all.slice(0, cut);
|
|
1599
|
+
const passthrough = cut < 0 ? [] : all.slice(cut + 1);
|
|
1600
|
+
if (command && (rest.includes("--help") || rest.includes("-h"))) {
|
|
1601
|
+
io.log(commandHelp(command));
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const { values, positionals } = parseArgs({
|
|
1605
|
+
args: rest,
|
|
1606
|
+
allowPositionals: true,
|
|
1607
|
+
options: {
|
|
1608
|
+
key: { type: "string" },
|
|
1609
|
+
api: { type: "string" },
|
|
1610
|
+
resources: { type: "string" },
|
|
1611
|
+
preview: { type: "boolean", default: false },
|
|
1612
|
+
write: { type: "string" },
|
|
1613
|
+
template: { type: "string" },
|
|
1614
|
+
force: { type: "boolean", default: false },
|
|
1615
|
+
input: { type: "string" },
|
|
1616
|
+
to: { type: "string" },
|
|
1617
|
+
subject: { type: "string" },
|
|
1618
|
+
text: { type: "string" },
|
|
1619
|
+
from: { type: "string" },
|
|
1620
|
+
"no-wait": { type: "boolean", default: false },
|
|
1621
|
+
period: { type: "string" },
|
|
1622
|
+
json: { type: "boolean", default: false },
|
|
1623
|
+
errors: { type: "boolean", default: false },
|
|
1624
|
+
rage: { type: "boolean", default: false },
|
|
1625
|
+
dead: { type: "boolean", default: false },
|
|
1626
|
+
path: { type: "string" },
|
|
1627
|
+
output: { type: "string", short: "o" },
|
|
1628
|
+
model: { type: "string" },
|
|
1629
|
+
system: { type: "string" },
|
|
1630
|
+
size: { type: "string" },
|
|
1631
|
+
voice: { type: "string" },
|
|
1632
|
+
language: { type: "string" },
|
|
1633
|
+
project: { type: "string" },
|
|
1634
|
+
by: { type: "string" },
|
|
1635
|
+
tool: { type: "string" },
|
|
1636
|
+
"end-user": { type: "string" },
|
|
1637
|
+
"api-key": { type: "string" },
|
|
1638
|
+
open: { type: "boolean", default: false },
|
|
1639
|
+
reset: { type: "boolean", default: false },
|
|
1640
|
+
format: { type: "string" },
|
|
1641
|
+
team: { type: "string" },
|
|
1642
|
+
dev: { type: "boolean", default: false },
|
|
1643
|
+
all: { type: "boolean", default: false },
|
|
1644
|
+
"keep-values": { type: "boolean", default: false },
|
|
1645
|
+
grace: { type: "string" },
|
|
1646
|
+
owner: { type: "boolean", default: false },
|
|
1647
|
+
own: { type: "boolean", default: false },
|
|
1648
|
+
"client-id": { type: "string" },
|
|
1649
|
+
file: { type: "string" },
|
|
1650
|
+
amount: { type: "string" },
|
|
1651
|
+
interval: { type: "string" },
|
|
1652
|
+
"lookup-key": { type: "string" },
|
|
1653
|
+
user: { type: "string" },
|
|
1654
|
+
"success-url": { type: "string" },
|
|
1655
|
+
"cancel-url": { type: "string" },
|
|
1656
|
+
name: { type: "string" },
|
|
1657
|
+
enable: { type: "string" },
|
|
1658
|
+
add: { type: "boolean", default: false },
|
|
1659
|
+
auth: { type: "string" },
|
|
1660
|
+
var: { type: "string" },
|
|
1661
|
+
body: { type: "string" },
|
|
1662
|
+
approval: { type: "string" },
|
|
1663
|
+
vertex: { type: "string" },
|
|
1664
|
+
region: { type: "string" },
|
|
1665
|
+
"gcp-project": { type: "string" },
|
|
1666
|
+
"use-for": { type: "string" },
|
|
1667
|
+
ads: { type: "boolean", default: false },
|
|
1668
|
+
media: { type: "string", multiple: true },
|
|
1669
|
+
at: { type: "string" },
|
|
1670
|
+
draft: { type: "boolean", default: false },
|
|
1671
|
+
opt: { type: "string", multiple: true },
|
|
1672
|
+
status: { type: "string" },
|
|
1673
|
+
budget: { type: "string" },
|
|
1674
|
+
threshold: { type: "string" },
|
|
1675
|
+
kind: { type: "string" },
|
|
1676
|
+
"no-browser": { type: "boolean", default: false },
|
|
1677
|
+
clients: { type: "string" },
|
|
1678
|
+
agent: { type: "string" },
|
|
1679
|
+
global: { type: "boolean", default: false }
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
const usagePeriod = (fallback) => {
|
|
1683
|
+
const p = values.period ?? fallback;
|
|
1684
|
+
if (!["24h", "7d", "30d", "month"].includes(p)) {
|
|
1685
|
+
throw new Error("--period is 24h, 7d, 30d or month");
|
|
1686
|
+
}
|
|
1687
|
+
return p;
|
|
1688
|
+
};
|
|
1689
|
+
const apiBase = async () => (values.api || io.env.VIBIZ_API_URL || (await readLogin(io, io.configDir ?? CONFIG_DIR)).apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
|
|
1690
|
+
switch (command) {
|
|
1691
|
+
case "signup":
|
|
1692
|
+
case "login": {
|
|
1693
|
+
const dir = io.configDir ?? CONFIG_DIR;
|
|
1694
|
+
const api = (values.api || io.env.VIBIZ_API_URL || DEFAULT_API_URL).replace(/\/$/, "");
|
|
1695
|
+
if (values.key) {
|
|
1696
|
+
await saveLogin(io, dir, { apiKey: values.key, apiUrl: api, source: "key" });
|
|
1697
|
+
} else {
|
|
1698
|
+
await browserLogin(io, api, {
|
|
1699
|
+
configDir: dir,
|
|
1700
|
+
open: !values["no-browser"],
|
|
1701
|
+
...io.fetch ? { fetch: io.fetch } : {},
|
|
1702
|
+
...io.sleep ? { sleep: io.sleep } : {}
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
const caps = await (await io.client()).capabilities();
|
|
1706
|
+
io.log(
|
|
1707
|
+
`${values.key ? "Logged in. " : ""}Available: ${[...caps.resources, ...caps.actions].join(", ")}`
|
|
1708
|
+
);
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
case "whoami": {
|
|
1712
|
+
const me = await (await io.client()).whoami();
|
|
1713
|
+
if (values.json) {
|
|
1714
|
+
io.log(JSON.stringify(me, null, 2));
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
io.log(`${me.account.name} (${me.account.id})`);
|
|
1718
|
+
io.log(` key ${me.key.name} ${me.key.prefix}\u2026 ${me.key.access}`);
|
|
1719
|
+
const saved = await readLogin(io, io.configDir ?? CONFIG_DIR);
|
|
1720
|
+
io.log(` api ${io.env.VIBIZ_API_URL || saved.apiUrl || DEFAULT_API_URL}`);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
case "logout": {
|
|
1724
|
+
const dir = io.configDir ?? CONFIG_DIR;
|
|
1725
|
+
const saved = await readLogin(io, dir);
|
|
1726
|
+
if (!saved.apiKey) {
|
|
1727
|
+
io.log("Not logged in on this machine.");
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (saved.source === "browser") {
|
|
1731
|
+
await (await io.client()).logout().catch((e) => {
|
|
1732
|
+
if (!(e instanceof VibizApiError && e.status === 401)) throw e;
|
|
1733
|
+
});
|
|
1734
|
+
io.log("Revoked this machine's key.");
|
|
1735
|
+
} else {
|
|
1736
|
+
io.log(
|
|
1737
|
+
"Forgot the key here. It still works elsewhere: revoke it in the console (API keys) if you meant to."
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
await io.local().writeFile(configFile(dir), "{}\n", { mode: 384 });
|
|
1741
|
+
if (io.env.VIBIZ_API_KEY)
|
|
1742
|
+
io.log("Note: VIBIZ_API_KEY is set in this shell and still logs you in.");
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
case "init": {
|
|
1746
|
+
const name = positionals[0];
|
|
1747
|
+
if (!name) throw new Error("usage: vibiz init <name> [--template nextjs-saas]");
|
|
1748
|
+
const template = values.template;
|
|
1749
|
+
if (template !== void 0 && !isTemplate(template)) {
|
|
1750
|
+
throw new Error(`Unknown template "${template}". Available: ${TEMPLATES.join(", ")}`);
|
|
1751
|
+
}
|
|
1752
|
+
const local = io.local();
|
|
1753
|
+
const vibiz2 = await io.client();
|
|
1754
|
+
if (template) {
|
|
1755
|
+
await scaffold({ template, name, dir: local.cwd(), force: values.force });
|
|
1756
|
+
io.log(`Copied the ${template} template into ${local.cwd()}`);
|
|
1757
|
+
}
|
|
1758
|
+
const resources = values.resources?.split(",").map((s) => s.trim());
|
|
1759
|
+
const { project } = await vibiz2.projects.create({
|
|
1760
|
+
name,
|
|
1761
|
+
...resources ? { resources } : {}
|
|
1762
|
+
});
|
|
1763
|
+
await io.local().writeFile(PROJECT_FILE, `${JSON.stringify({ project_id: project.id }, null, 2)}
|
|
1764
|
+
`);
|
|
1765
|
+
io.log(`Created ${project.name} \u2192 ${project.url ?? project.slug}. Provisioning\u2026`);
|
|
1766
|
+
if (template) {
|
|
1767
|
+
io.log(" Next: npm install && vibiz dev && npm run dev");
|
|
1768
|
+
}
|
|
1769
|
+
if (values["no-wait"]) return;
|
|
1770
|
+
printProject(io, await vibiz2.projects.waitUntilReady(project.id));
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
case "status": {
|
|
1774
|
+
const vibiz2 = await io.client();
|
|
1775
|
+
const id = await io.project();
|
|
1776
|
+
const project = await vibiz2.projects.get(id);
|
|
1777
|
+
printProject(io, project);
|
|
1778
|
+
const deployments = (await vibiz2.deployments.list(id)).slice(0, 5);
|
|
1779
|
+
if (deployments.length) io.log("\n deployments");
|
|
1780
|
+
for (const d of deployments) {
|
|
1781
|
+
const live = d.id === project.production_deployment_id ? " \u2190 live" : "";
|
|
1782
|
+
io.log(
|
|
1783
|
+
` ${dot(d.status)} ${d.id} ${d.target.padEnd(10)} ${d.status.padEnd(8)} ${d.url ?? ""}${live}`
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
case "deploy": {
|
|
1789
|
+
const local = io.local();
|
|
1790
|
+
const vibiz2 = await io.client();
|
|
1791
|
+
const id = values.project ?? await io.project();
|
|
1792
|
+
const dir = resolve(local.cwd(), positionals[0] ?? ".");
|
|
1793
|
+
const { deployment, uploaded, total } = await deployDirectory(vibiz2, id, dir, {
|
|
1794
|
+
target: values.preview ? "preview" : "production",
|
|
1795
|
+
onProgress: (m) => io.log(m)
|
|
1796
|
+
});
|
|
1797
|
+
io.log(`Deploying ${deployment.id} (${uploaded}/${total} files uploaded)\u2026`);
|
|
1798
|
+
if (values["no-wait"]) return;
|
|
1799
|
+
const done = await vibiz2.deployments.waitUntilDone(deployment.id);
|
|
1800
|
+
if (done.status !== "ready") throw new Error(done.error ?? `Deployment ${done.status}`);
|
|
1801
|
+
if (done.target === "production") {
|
|
1802
|
+
const project = await vibiz2.projects.get(id);
|
|
1803
|
+
io.log(`${dot("ready")} Live: ${project.url ?? done.url}`);
|
|
1804
|
+
} else {
|
|
1805
|
+
io.log(`${dot("ready")} Preview: ${done.url}`);
|
|
1806
|
+
}
|
|
1807
|
+
if (done.target === "preview") io.log(` Make it live: vibiz promote ${done.id}`);
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
case "promote": {
|
|
1811
|
+
const depId = positionals[0];
|
|
1812
|
+
if (!depId) throw new Error("usage: vibiz promote <deployment-id>");
|
|
1813
|
+
const vibiz2 = await io.client();
|
|
1814
|
+
const out = await vibiz2.deployments.promote(depId);
|
|
1815
|
+
if (out.instant) {
|
|
1816
|
+
io.log(`${dot("ready")} ${depId} is live.`);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
io.log(`Building ${out.deployment.id} for production\u2026`);
|
|
1820
|
+
const done = await vibiz2.deployments.waitUntilDone(out.deployment.id);
|
|
1821
|
+
if (done.status !== "ready") throw new Error(done.error ?? `Deployment ${done.status}`);
|
|
1822
|
+
const project = await vibiz2.projects.get(done.project_id);
|
|
1823
|
+
io.log(`${dot("ready")} Live: ${project.url}`);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
case "rollback": {
|
|
1827
|
+
const vibiz2 = await io.client();
|
|
1828
|
+
const id = await io.project();
|
|
1829
|
+
const project = await vibiz2.projects.get(id);
|
|
1830
|
+
const previous = (await vibiz2.deployments.list(id)).find(
|
|
1831
|
+
(d) => d.target === "production" && d.status === "ready" && d.id !== project.production_deployment_id
|
|
1832
|
+
);
|
|
1833
|
+
if (!previous) throw new Error("No earlier production deployment to roll back to.");
|
|
1834
|
+
await vibiz2.deployments.promote(previous.id);
|
|
1835
|
+
io.log(`${dot("ready")} Rolled back to ${previous.id}.`);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
case "dev": {
|
|
1839
|
+
const vibiz2 = await io.client();
|
|
1840
|
+
const id = await io.project();
|
|
1841
|
+
if (values.reset) {
|
|
1842
|
+
await vibiz2.projects.dev.remove(id);
|
|
1843
|
+
io.log("Removed the development environment. Run `vibiz dev` for a fresh one.");
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
const local = io.local();
|
|
1847
|
+
io.err("Preparing the development environment\u2026\n");
|
|
1848
|
+
const env = await vibiz2.projects.dev.ensure(id);
|
|
1849
|
+
if (passthrough.length) {
|
|
1850
|
+
const [cmd, ...args] = passthrough;
|
|
1851
|
+
local.exit(await local.exec(cmd, args, { ...io.env, ...env }));
|
|
1852
|
+
}
|
|
1853
|
+
const file = values.write ?? ".env.development.local";
|
|
1854
|
+
const existing = await io.local().readFile(file, "utf8").catch(() => "");
|
|
1855
|
+
await io.local().writeFile(file, mergeEnvFile(existing, env), { mode: 384 });
|
|
1856
|
+
await io.local().chmod(file, 384);
|
|
1857
|
+
io.log(`Wrote the development env to ${file}: ${Object.keys(env).sort().join(", ")}`);
|
|
1858
|
+
io.log("Its own database branch (your schema, no production rows), sign-in and AI key.");
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
case "auth": {
|
|
1862
|
+
const vibiz2 = await io.client();
|
|
1863
|
+
const id = await io.project();
|
|
1864
|
+
const env = values.dev ? "development" : void 0;
|
|
1865
|
+
const [sub, verb, arg] = positionals;
|
|
1866
|
+
let settings;
|
|
1867
|
+
if (sub === "providers" && (verb === "add" || verb === "rm")) {
|
|
1868
|
+
const ids = ["google", "github", "vercel"];
|
|
1869
|
+
const provider = ids.find((p) => p === arg);
|
|
1870
|
+
if (!provider) {
|
|
1871
|
+
throw new Error(`usage: vibiz auth providers ${verb} <${ids.join("|")}>`);
|
|
1872
|
+
}
|
|
1873
|
+
if (verb === "rm") settings = await vibiz2.projects.auth.removeProvider(id, provider, env);
|
|
1874
|
+
else if (values.own) {
|
|
1875
|
+
if (!values["client-id"]) throw new Error("--own needs --client-id <id>");
|
|
1876
|
+
const envName = `${provider.toUpperCase()}_CLIENT_SECRET`;
|
|
1877
|
+
const secret = io.env[envName] || await io.promptHidden(`${provider} client secret: `);
|
|
1878
|
+
if (!secret) throw new Error(`The client secret is needed (${envName}, or type it).`);
|
|
1879
|
+
settings = await vibiz2.projects.auth.setProvider(
|
|
1880
|
+
id,
|
|
1881
|
+
provider,
|
|
1882
|
+
{ credentials: "own", client_id: values["client-id"], client_secret: secret },
|
|
1883
|
+
env
|
|
1884
|
+
);
|
|
1885
|
+
const cb = settings.providers.find((p) => p.id === provider)?.callback_url;
|
|
1886
|
+
io.log(`Register this callback URL in your ${provider} OAuth app: ${cb}
|
|
1887
|
+
`);
|
|
1888
|
+
} else {
|
|
1889
|
+
settings = await vibiz2.projects.auth.setProvider(
|
|
1890
|
+
id,
|
|
1891
|
+
provider,
|
|
1892
|
+
{ credentials: "shared" },
|
|
1893
|
+
env
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
} else if (sub === "domains" && verb) {
|
|
1897
|
+
const domains = verb === "none" ? [] : verb.split(",").filter(Boolean);
|
|
1898
|
+
settings = await vibiz2.projects.auth.update(id, { allowed_email_domains: domains }, env);
|
|
1899
|
+
} else if (sub) {
|
|
1900
|
+
throw new Error("usage: vibiz auth [providers add|rm <provider> | domains <list|none>]");
|
|
1901
|
+
} else settings = await vibiz2.projects.auth.get(id, env);
|
|
1902
|
+
if (values.json) io.log(JSON.stringify(settings, null, 2));
|
|
1903
|
+
else printAuth(io, settings);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
case "company": {
|
|
1907
|
+
const vibiz2 = await io.client();
|
|
1908
|
+
const id = await io.project();
|
|
1909
|
+
const env = values.dev ? "development" : void 0;
|
|
1910
|
+
const [sub, arg, arg2] = positionals;
|
|
1911
|
+
const show = (c) => values.json ? io.log(JSON.stringify(c, null, 2)) : printCompany(io, c);
|
|
1912
|
+
if (!sub) {
|
|
1913
|
+
try {
|
|
1914
|
+
show(await vibiz2.projects.company.get(id, env));
|
|
1915
|
+
} catch (e) {
|
|
1916
|
+
if (!(e instanceof VibizApiError) || e.status !== 404) throw e;
|
|
1917
|
+
io.log("No company yet. Draft one: vibiz company draft --file company.json");
|
|
1918
|
+
}
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (sub === "draft") {
|
|
1922
|
+
if (!values.file) throw new Error("usage: vibiz company draft --file company.json");
|
|
1923
|
+
const draft = JSON.parse(await io.local().readFile(values.file, "utf8"));
|
|
1924
|
+
show(await vibiz2.projects.company.draft(id, draft, env));
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
if (sub === "submit") {
|
|
1928
|
+
const c = await vibiz2.projects.company.get(id, env);
|
|
1929
|
+
if (c.environment === "production") {
|
|
1930
|
+
const name = companyName(c);
|
|
1931
|
+
if (c.status !== "draft") throw new Error(`This company is ${c.status}, not a draft.`);
|
|
1932
|
+
if (!c.quote) throw new Error("The price is unavailable right now: nothing was filed.");
|
|
1933
|
+
const quote = c.quote;
|
|
1934
|
+
const total = cents(quote.total_cents);
|
|
1935
|
+
io.log(
|
|
1936
|
+
`This files ${name} (${c.entity_type === "llc" ? "LLC" : "C-Corp"}, ${c.state}) with the state for real and charges ${total}.`
|
|
1937
|
+
);
|
|
1938
|
+
io.log(
|
|
1939
|
+
"Vibiz files formation documents on your instructions. It is not a law firm and does not give legal or tax advice."
|
|
1940
|
+
);
|
|
1941
|
+
const typed = await io.promptLine(`Type the company's name (${name}) to confirm: `);
|
|
1942
|
+
if (typed !== name) throw new Error("Not confirmed: nothing was filed.");
|
|
1943
|
+
show(
|
|
1944
|
+
await vibiz2.projects.company.submit(
|
|
1945
|
+
id,
|
|
1946
|
+
{
|
|
1947
|
+
accept_terms: true,
|
|
1948
|
+
confirm: true,
|
|
1949
|
+
expected_total_cents: quote.total_cents,
|
|
1950
|
+
expected_updated_at: c.updated_at
|
|
1951
|
+
},
|
|
1952
|
+
"production"
|
|
1953
|
+
)
|
|
1954
|
+
);
|
|
1955
|
+
} else {
|
|
1956
|
+
show(await vibiz2.projects.company.submit(id, { accept_terms: true }, c.environment));
|
|
1957
|
+
}
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (sub === "sign") {
|
|
1961
|
+
if (arg !== "ss4" && arg !== "form_8821") {
|
|
1962
|
+
throw new Error("usage: vibiz company sign <ss4|form_8821>");
|
|
1963
|
+
}
|
|
1964
|
+
const link = await vibiz2.projects.company.sign(id, arg, env);
|
|
1965
|
+
io.log(`Open this to sign (it expires ${link.expires_at}; don't share it):
|
|
1966
|
+
${link.url}`);
|
|
1967
|
+
if (values.open) io.openUrl(link.url);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (sub === "docs") {
|
|
1971
|
+
if (arg === "download") {
|
|
1972
|
+
if (!arg2) throw new Error("usage: vibiz company docs download <id>");
|
|
1973
|
+
const link = await vibiz2.projects.company.document(id, arg2, env);
|
|
1974
|
+
io.log(`${link.name} (link expires ${link.expires_at}):
|
|
1975
|
+
${link.url}`);
|
|
1976
|
+
if (values.open) io.openUrl(link.url);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
const c = await vibiz2.projects.company.get(id, env);
|
|
1980
|
+
if (values.json) io.log(JSON.stringify(c.documents, null, 2));
|
|
1981
|
+
else if (!c.documents.length) io.log("No documents yet.");
|
|
1982
|
+
else for (const d of c.documents) io.log(`${d.id} ${d.type.padEnd(19)} ${d.name}`);
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1985
|
+
if (sub === "simulate") {
|
|
1986
|
+
if (arg !== "formation" && arg !== "ein") {
|
|
1987
|
+
throw new Error("usage: vibiz company simulate <formation|ein>");
|
|
1988
|
+
}
|
|
1989
|
+
show(await vibiz2.projects.company.simulate(id, arg));
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
"usage: vibiz company [draft --file f | submit | sign <doc> | docs | simulate <step>]"
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
case "payments": {
|
|
1997
|
+
const vibiz2 = await io.client();
|
|
1998
|
+
const id = await io.project();
|
|
1999
|
+
const env = values.dev ? "development" : void 0;
|
|
2000
|
+
const [sub, arg, arg2] = positionals;
|
|
2001
|
+
const out = (x, text) => values.json ? io.log(JSON.stringify(x, null, 2)) : text();
|
|
2002
|
+
const amount = values.amount !== void 0 ? Number(values.amount) : void 0;
|
|
2003
|
+
if (amount !== void 0 && !Number.isInteger(amount)) {
|
|
2004
|
+
throw new Error("--amount is in cents: --amount 1500");
|
|
2005
|
+
}
|
|
2006
|
+
if (!sub) {
|
|
2007
|
+
const a = await vibiz2.projects.payments.get(id, env);
|
|
2008
|
+
out(a, () => printPayments(io, a));
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
if (sub === "products") {
|
|
2012
|
+
if (arg === "add") {
|
|
2013
|
+
if (!arg2 || amount === void 0) {
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
"usage: vibiz payments products add <name> --amount <cents> [--interval month|year]"
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
const interval = values.interval;
|
|
2019
|
+
const p = await vibiz2.projects.payments.products.create(
|
|
2020
|
+
id,
|
|
2021
|
+
{
|
|
2022
|
+
name: arg2,
|
|
2023
|
+
price: {
|
|
2024
|
+
unit_amount_cents: amount,
|
|
2025
|
+
type: interval ? "recurring" : "one_time",
|
|
2026
|
+
...interval ? { interval } : {},
|
|
2027
|
+
...values["lookup-key"] ? { lookup_key: values["lookup-key"] } : {}
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
2030
|
+
env
|
|
2031
|
+
);
|
|
2032
|
+
out(p, () => io.log(`${p.id} ${p.name} price ${p.prices[0]?.id}`));
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const list = await vibiz2.projects.payments.products.list(id, env);
|
|
2036
|
+
out(list.data, () => {
|
|
2037
|
+
if (!list.data.length)
|
|
2038
|
+
io.log("No products yet: vibiz payments products add <name> --amount <cents>");
|
|
2039
|
+
for (const p of list.data) {
|
|
2040
|
+
io.log(`${p.id} ${p.name}${p.active ? "" : " (archived)"}`);
|
|
2041
|
+
for (const x of p.prices) {
|
|
2042
|
+
const every = x.interval ? ` / ${x.interval_count && x.interval_count > 1 ? `${x.interval_count} ` : ""}${x.interval}` : "";
|
|
2043
|
+
io.log(
|
|
2044
|
+
` ${x.id} ${cents(x.unit_amount_cents)}${every}${x.lookup_key ? ` ${x.lookup_key}` : ""}${x.active ? "" : " (inactive)"}`
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (sub === "checkout") {
|
|
2052
|
+
if (!arg || !values["success-url"] || !values["cancel-url"]) {
|
|
2053
|
+
throw new Error(
|
|
2054
|
+
"usage: vibiz payments checkout <price_id|lookup_key> --success-url <u> --cancel-url <u> [--user id]"
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
const c = await vibiz2.projects.payments.checkout(
|
|
2058
|
+
id,
|
|
2059
|
+
{
|
|
2060
|
+
...arg.startsWith("pprice_") ? { price_id: arg } : { lookup_key: arg },
|
|
2061
|
+
...values.user ? { user: values.user } : {},
|
|
2062
|
+
success_url: values["success-url"],
|
|
2063
|
+
cancel_url: values["cancel-url"]
|
|
2064
|
+
},
|
|
2065
|
+
env
|
|
2066
|
+
);
|
|
2067
|
+
out(c, () => io.log(`Pay here (until ${c.expires_at}):
|
|
2068
|
+
${c.url}`));
|
|
2069
|
+
if (values.open) io.openUrl(c.url);
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (sub === "list") {
|
|
2073
|
+
const list = await vibiz2.projects.payments.list(id, { ...env ? { env } : {} });
|
|
2074
|
+
out(list.data, () => {
|
|
2075
|
+
if (!list.data.length) io.log("No payments yet.");
|
|
2076
|
+
for (const p of list.data) {
|
|
2077
|
+
io.log(
|
|
2078
|
+
`${p.created_at.slice(0, 10)} ${p.id} ${cents(p.amount_cents).padStart(10)} net ${cents(p.net_cents)} ${p.status}${p.customer_email ? ` ${p.customer_email}` : ""}`
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (sub === "webhook") {
|
|
2085
|
+
if (!arg) throw new Error("usage: vibiz payments webhook <url|none> [--dev]");
|
|
2086
|
+
const a = await vibiz2.projects.payments.update(
|
|
2087
|
+
id,
|
|
2088
|
+
{ webhook_url: arg === "none" ? null : arg },
|
|
2089
|
+
env
|
|
2090
|
+
);
|
|
2091
|
+
out(a.webhook, () => {
|
|
2092
|
+
io.log(a.webhook.url ? `Webhook: ${a.webhook.url}` : "No webhook.");
|
|
2093
|
+
if (a.webhook.url) {
|
|
2094
|
+
io.log(
|
|
2095
|
+
` signing secret: VIBIZ_WEBHOOK_SECRET in the ${a.environment} env (${a.environment === "development" ? "vibiz dev writes it" : "on the host already"}); verify with vibiz.webhooks.verify (vibiz docs webhooks)`
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (sub === "withdraw") {
|
|
2102
|
+
const a = await vibiz2.projects.payments.get(id, env);
|
|
2103
|
+
const sending = amount ?? a.balance.available_cents;
|
|
2104
|
+
if (a.payouts.status !== "ready") {
|
|
2105
|
+
throw new Error(
|
|
2106
|
+
"Verify your identity and add a bank first: console \u25B8 Payments \u25B8 Get paid."
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
if (sending <= 0) throw new Error("Nothing is available to withdraw yet.");
|
|
2110
|
+
io.log(
|
|
2111
|
+
`This sends ${cents(sending)} (${a.environment}) to your bank${a.payouts.bank ? ` \u2022\u2022\u2022\u2022 ${a.payouts.bank.last4}` : ""}.`
|
|
2112
|
+
);
|
|
2113
|
+
const typed = await io.promptLine('Type "withdraw" to confirm: ');
|
|
2114
|
+
if (typed !== "withdraw") throw new Error("Not confirmed: nothing was sent.");
|
|
2115
|
+
const w = await vibiz2.projects.payments.withdraw(
|
|
2116
|
+
id,
|
|
2117
|
+
{ amount_cents: sending },
|
|
2118
|
+
a.environment
|
|
2119
|
+
);
|
|
2120
|
+
out(
|
|
2121
|
+
w,
|
|
2122
|
+
() => io.log(
|
|
2123
|
+
`${w.id} ${cents(w.amount_cents)} ${w.status}: it reaches your bank in a few days.`
|
|
2124
|
+
)
|
|
2125
|
+
);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (sub === "simulate") {
|
|
2129
|
+
const event = arg;
|
|
2130
|
+
if (event === "refund" && !arg2)
|
|
2131
|
+
throw new Error("usage: vibiz payments simulate refund <payment_id>");
|
|
2132
|
+
if (!event || !["payment", "dispute", "refund", "release_hold"].includes(event)) {
|
|
2133
|
+
throw new Error(
|
|
2134
|
+
"usage: vibiz payments simulate <payment|dispute|refund <id>|release_hold>"
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
const r = await vibiz2.projects.payments.simulate(
|
|
2138
|
+
id,
|
|
2139
|
+
event === "refund" ? { event, payment_id: arg2 ?? "" } : event === "release_hold" ? { event } : { event, ...amount !== void 0 ? { amount_cents: amount } : {} }
|
|
2140
|
+
);
|
|
2141
|
+
out(r, () => {
|
|
2142
|
+
if (r.object === "payments_account") printPayments(io, r);
|
|
2143
|
+
else io.log(`${r.id} ${cents(r.amount_cents)} ${r.status} (test mode)`);
|
|
2144
|
+
});
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
throw new Error(
|
|
2148
|
+
"usage: vibiz payments [products [add] | checkout <price> | list | webhook <url> | withdraw | simulate <event>]"
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
2151
|
+
case "social": {
|
|
2152
|
+
const vibiz2 = await io.client();
|
|
2153
|
+
const id = await io.project();
|
|
2154
|
+
const env = values.dev ? "development" : void 0;
|
|
2155
|
+
const [sub, arg] = positionals;
|
|
2156
|
+
const out = (v, line) => values.json ? io.log(JSON.stringify(v, null, 2)) : line();
|
|
2157
|
+
const showPost = (p) => out(p, () => {
|
|
2158
|
+
io.log(
|
|
2159
|
+
`${p.id} ${p.status}${p.scheduled_for ? ` at ${p.scheduled_for}` : ""} (${p.environment})`
|
|
2160
|
+
);
|
|
2161
|
+
for (const t of p.targets) {
|
|
2162
|
+
io.log(` ${t.platform.padEnd(15)} ${t.status.padEnd(10)} ${t.url ?? t.error ?? ""}`);
|
|
2163
|
+
}
|
|
2164
|
+
if (p.approval?.status === "pending")
|
|
2165
|
+
io.log(` Waiting for the owner: ${p.console_url ?? ""}`);
|
|
2166
|
+
});
|
|
2167
|
+
const sure = async (what) => {
|
|
2168
|
+
const typed = await io.promptLine(`${what} Type yes to confirm: `);
|
|
2169
|
+
if (typed.trim().toLowerCase() !== "yes")
|
|
2170
|
+
throw new Error("Not confirmed: nothing was sent.");
|
|
2171
|
+
};
|
|
2172
|
+
if (!sub) {
|
|
2173
|
+
const o = await vibiz2.projects.social.get(id, env);
|
|
2174
|
+
out(o, () => {
|
|
2175
|
+
io.log(
|
|
2176
|
+
`Social (${o.environment}${o.sandbox ? ", sandbox: nothing is posted or spent" : ""})`
|
|
2177
|
+
);
|
|
2178
|
+
if (!o.available) io.log(" Not available right now.");
|
|
2179
|
+
io.log(
|
|
2180
|
+
` accounts: ${o.accounts.connected} connected, ${o.accounts.needs_reconnect} to reconnect, ${o.accounts.ads} ads`
|
|
2181
|
+
);
|
|
2182
|
+
io.log(
|
|
2183
|
+
` posts: ${o.posts.pending_approval} waiting for you, ${o.posts.scheduled} scheduled, ${o.posts.failed_7d} failed (7 days), ${o.posts.published_30d} published (30 days)`
|
|
2184
|
+
);
|
|
2185
|
+
if (o.ad_changes_pending) io.log(` ad changes waiting for you: ${o.ad_changes_pending}`);
|
|
2186
|
+
if (o.console_url) io.log(` ${o.console_url}`);
|
|
2187
|
+
});
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
if (sub === "connect") {
|
|
2191
|
+
if (!arg) throw new Error("usage: vibiz social connect <platform> [--ads]");
|
|
2192
|
+
const link = await vibiz2.projects.social.connect(id, arg, { ads: values.ads, env });
|
|
2193
|
+
io.log(
|
|
2194
|
+
`Open this to connect ${arg}${link.ads ? " ads" : ""} (expires ${link.expires_at}):
|
|
2195
|
+
${link.url}`
|
|
2196
|
+
);
|
|
2197
|
+
if (values.open) io.openUrl(link.url);
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
2200
|
+
if (sub === "accounts") {
|
|
2201
|
+
const list = await vibiz2.projects.social.accounts(id, { env });
|
|
2202
|
+
out(list.data, () => {
|
|
2203
|
+
if (!list.data.length) io.log("No accounts yet: vibiz social connect <platform>");
|
|
2204
|
+
for (const a of list.data) {
|
|
2205
|
+
io.log(
|
|
2206
|
+
`${a.id} ${a.kind === "ads" ? `${a.platform} ads` : a.platform} ${a.username ? `@${a.username}` : a.display_name ?? ""} ${a.status}`
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
});
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (sub === "post") {
|
|
2213
|
+
if (!values.to) throw new Error('usage: vibiz social post "<text>" --to <account,\u2026>');
|
|
2214
|
+
const options = {};
|
|
2215
|
+
for (const kv of values.opt ?? []) {
|
|
2216
|
+
const i = kv.indexOf("=");
|
|
2217
|
+
if (i < 1) throw new Error("--opt is key=value");
|
|
2218
|
+
const v = kv.slice(i + 1);
|
|
2219
|
+
options[kv.slice(0, i)] = v === "true" ? true : v === "false" ? false : v;
|
|
2220
|
+
}
|
|
2221
|
+
const media = [];
|
|
2222
|
+
for (const m of values.media ?? []) {
|
|
2223
|
+
if (/^https?:\/\//.test(m)) media.push({ url: m });
|
|
2224
|
+
else {
|
|
2225
|
+
const bytes = new Uint8Array(await io.local().readFile(m));
|
|
2226
|
+
const ext = m.split(".").pop()?.toLowerCase() ?? "";
|
|
2227
|
+
const contentType = {
|
|
2228
|
+
jpg: "image/jpeg",
|
|
2229
|
+
jpeg: "image/jpeg",
|
|
2230
|
+
png: "image/png",
|
|
2231
|
+
webp: "image/webp",
|
|
2232
|
+
gif: "image/gif",
|
|
2233
|
+
mp4: "video/mp4",
|
|
2234
|
+
mov: "video/quicktime",
|
|
2235
|
+
webm: "video/webm",
|
|
2236
|
+
pdf: "application/pdf"
|
|
2237
|
+
}[ext];
|
|
2238
|
+
if (!contentType) throw new Error(`${m}: jpg, png, webp, gif, mp4, mov, webm or pdf`);
|
|
2239
|
+
const up = await vibiz2.projects.social.upload(
|
|
2240
|
+
id,
|
|
2241
|
+
{ bytes, contentType, filename: m.split("/").pop() },
|
|
2242
|
+
env
|
|
2243
|
+
);
|
|
2244
|
+
media.push({ url: up.url });
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
const input = {
|
|
2248
|
+
text: arg ?? "",
|
|
2249
|
+
media,
|
|
2250
|
+
targets: values.to.split(",").filter(Boolean).map((account_id) => ({ account_id, options })),
|
|
2251
|
+
...values.at ? { schedule_at: values.at } : {},
|
|
2252
|
+
...values.draft ? { draft: true } : {}
|
|
2253
|
+
};
|
|
2254
|
+
let p;
|
|
2255
|
+
try {
|
|
2256
|
+
p = await vibiz2.projects.social.create(id, input, env);
|
|
2257
|
+
} catch (e) {
|
|
2258
|
+
if (!(e instanceof VibizApiError) || e.details?.reason !== "confirmation_required")
|
|
2259
|
+
throw e;
|
|
2260
|
+
await sure(
|
|
2261
|
+
`This ${values.at ? `schedules a post for ${values.at}` : "publishes now"} on the live accounts ${values.to}.`
|
|
2262
|
+
);
|
|
2263
|
+
p = await vibiz2.projects.social.create(id, { ...input, confirm: true }, env);
|
|
2264
|
+
}
|
|
2265
|
+
showPost(p);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
if (sub === "posts") {
|
|
2269
|
+
const list = await vibiz2.projects.social.posts(id, {
|
|
2270
|
+
env,
|
|
2271
|
+
...values.status ? { status: values.status } : {}
|
|
2272
|
+
});
|
|
2273
|
+
out(list.data, () => {
|
|
2274
|
+
if (!list.data.length) io.log("No posts.");
|
|
2275
|
+
for (const p of list.data) {
|
|
2276
|
+
io.log(
|
|
2277
|
+
`${p.id} ${p.status.padEnd(16)} ${p.targets.map((t) => t.platform).join(",").padEnd(20)} ${p.text.slice(0, 50).replace(/\n/g, " ")}`
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
});
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
if (sub === "approve" || sub === "reject" || sub === "cancel") {
|
|
2284
|
+
if (!arg) throw new Error(`usage: vibiz social ${sub} <post id>`);
|
|
2285
|
+
const p = sub === "approve" ? await vibiz2.projects.social.approve(
|
|
2286
|
+
id,
|
|
2287
|
+
arg,
|
|
2288
|
+
values.at ? { schedule_at: values.at } : {},
|
|
2289
|
+
env
|
|
2290
|
+
) : sub === "reject" ? await vibiz2.projects.social.reject(id, arg, env) : await vibiz2.projects.social.cancel(id, arg, env);
|
|
2291
|
+
showPost(p);
|
|
2292
|
+
return;
|
|
2293
|
+
}
|
|
2294
|
+
if (sub === "campaigns") {
|
|
2295
|
+
const list = await vibiz2.projects.social.campaigns(id, { env });
|
|
2296
|
+
out(list.data, () => {
|
|
2297
|
+
if (!list.data.length)
|
|
2298
|
+
io.log("No campaigns (connect ads: vibiz social connect meta --ads).");
|
|
2299
|
+
for (const c of list.data) {
|
|
2300
|
+
const money2 = (v) => v === null ? "\u2013" : `${(v / 100).toFixed(2)} ${c.currency ?? ""}`.trim();
|
|
2301
|
+
io.log(
|
|
2302
|
+
`${c.id} ${c.status.padEnd(14)} ${money2(c.daily_budget_cents)}/day spent ${money2(c.spend_cents)} ${c.name}`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
});
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
if (sub === "pause" || sub === "resume" || sub === "budget") {
|
|
2309
|
+
if (!arg) throw new Error(`usage: vibiz social ${sub} <campaign id>`);
|
|
2310
|
+
const list = await vibiz2.projects.social.campaigns(id, { env });
|
|
2311
|
+
const c = list.data.find((x) => x.id === arg);
|
|
2312
|
+
if (!c)
|
|
2313
|
+
throw new Error(`No campaign ${arg} on the first ads account (vibiz social campaigns).`);
|
|
2314
|
+
const budget = sub === "budget" ? Number(values.budget) : void 0;
|
|
2315
|
+
if (sub === "budget" && !(budget && budget >= 100))
|
|
2316
|
+
throw new Error("--budget <cents per day>, at least 100");
|
|
2317
|
+
const input = {
|
|
2318
|
+
account_id: c.account_id,
|
|
2319
|
+
ad_account_id: c.ad_account_id,
|
|
2320
|
+
kind: sub,
|
|
2321
|
+
campaign_id: c.id,
|
|
2322
|
+
...budget ? { daily_budget_cents: budget } : {}
|
|
2323
|
+
};
|
|
2324
|
+
let ch;
|
|
2325
|
+
try {
|
|
2326
|
+
ch = await vibiz2.projects.social.adChange(id, input, env);
|
|
2327
|
+
} catch (e) {
|
|
2328
|
+
if (!(e instanceof VibizApiError) || e.details?.reason !== "confirmation_required")
|
|
2329
|
+
throw e;
|
|
2330
|
+
await sure(
|
|
2331
|
+
`This ${sub === "pause" ? "pauses" : sub === "resume" ? "resumes (spends money on)" : `sets ${budget} cents a day on`} the live campaign "${c.name}".`
|
|
2332
|
+
);
|
|
2333
|
+
ch = await vibiz2.projects.social.adChange(id, { ...input, confirm: true }, env);
|
|
2334
|
+
}
|
|
2335
|
+
out(
|
|
2336
|
+
ch,
|
|
2337
|
+
() => io.log(`${ch.id} ${ch.kind} ${ch.status}${ch.next_step ? `
|
|
2338
|
+
${ch.next_step}` : ""}`)
|
|
2339
|
+
);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
throw new Error(
|
|
2343
|
+
"usage: vibiz social [connect|accounts|post|posts|approve|reject|cancel|campaigns|pause|resume|budget]"
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
case "connectors": {
|
|
2347
|
+
const vibiz2 = await io.client();
|
|
2348
|
+
const id = await io.project();
|
|
2349
|
+
const env = values.dev ? "development" : void 0;
|
|
2350
|
+
const [sub = "list", name, ...more] = positionals;
|
|
2351
|
+
const out = (x, text) => values.json ? io.log(JSON.stringify(x, null, 2)) : text();
|
|
2352
|
+
const c = vibiz2.projects.connectors;
|
|
2353
|
+
const ready = (k) => `prod ${k.ready.production ? "ready" : "no key"}, dev ${k.ready.development ? "ready" : "no key"}`;
|
|
2354
|
+
const printOps = (ops) => {
|
|
2355
|
+
for (const o of ops) {
|
|
2356
|
+
const required = o.params.required ?? [];
|
|
2357
|
+
const props = Object.keys(o.params.properties ?? {}).map(
|
|
2358
|
+
(k) => `${k}${required.includes(k) ? "" : "?"}`
|
|
2359
|
+
);
|
|
2360
|
+
io.log(
|
|
2361
|
+
` ${o.enabled ? "\u25CF" : "\u25CB"} ${o.id.padEnd(24)} ${o.method.padEnd(6)} ${o.path} [${o.risk}]${props.length ? ` ${props.join(" ")}` : ""}${o.body ? " +body" : ""}`
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
};
|
|
2365
|
+
if (sub === "list") {
|
|
2366
|
+
const list = await c.list(id);
|
|
2367
|
+
out(list, () => {
|
|
2368
|
+
if (!list.length)
|
|
2369
|
+
io.log(
|
|
2370
|
+
"No connectors yet: vibiz connectors import <openapi-url> or vibiz connectors add"
|
|
2371
|
+
);
|
|
2372
|
+
for (const k of list) {
|
|
2373
|
+
const on = k.operations.filter((o) => o.enabled).length;
|
|
2374
|
+
io.log(
|
|
2375
|
+
`${k.name.padEnd(20)} ${k.base_url} ${on}/${k.operations.length} operations ${ready(k)} billed by ${k.pricing.billed_by}`
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
});
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
if (sub === "import") {
|
|
2382
|
+
if (!name)
|
|
2383
|
+
throw new Error("usage: vibiz connectors import <spec-url|file> [--name n] [--add]");
|
|
2384
|
+
const input = {
|
|
2385
|
+
.../^https?:\/\//.test(name) ? { url: name } : { spec: await io.local().readFile(name, "utf8") },
|
|
2386
|
+
...values.name ? { name: values.name } : {},
|
|
2387
|
+
...values.enable ? { enable: values.enable.split(",") } : {}
|
|
2388
|
+
};
|
|
2389
|
+
const draft = await c.import(id, input);
|
|
2390
|
+
if (values.add) {
|
|
2391
|
+
const made = await c.create(id, draft.manifest);
|
|
2392
|
+
out(made, () => {
|
|
2393
|
+
io.log(`${dot("ready")} Added ${made.name} (${made.base_url})`);
|
|
2394
|
+
printOps(made.operations);
|
|
2395
|
+
const keys = made.secrets.map((x) => x.name);
|
|
2396
|
+
if (keys.length)
|
|
2397
|
+
io.log(` Keys: vibiz connectors secret ${made.name} ${keys[0]} [--dev]`);
|
|
2398
|
+
});
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
out(draft, () => {
|
|
2402
|
+
io.log(
|
|
2403
|
+
`${draft.manifest.label} \u2192 ${draft.manifest.name} ${draft.manifest.base_url || "(no base URL)"}`
|
|
2404
|
+
);
|
|
2405
|
+
io.log(
|
|
2406
|
+
` auth: ${draft.manifest.auth.type} ${draft.counts.kept} of ${draft.counts.seen} operations (\u25CF enabled: reads only)`
|
|
2407
|
+
);
|
|
2408
|
+
printOps(draft.manifest.operations);
|
|
2409
|
+
for (const p of draft.problems)
|
|
2410
|
+
io.log(
|
|
2411
|
+
` ! ${p.operation ? `${p.operation} (${p.method} ${p.path}): ` : ""}${p.message}`
|
|
2412
|
+
);
|
|
2413
|
+
io.log(" Save it: the same with --add (and --enable op,op for writes).");
|
|
2414
|
+
});
|
|
2415
|
+
return;
|
|
2416
|
+
}
|
|
2417
|
+
if (sub === "add") {
|
|
2418
|
+
let manifest;
|
|
2419
|
+
if (values.file) {
|
|
2420
|
+
manifest = JSON.parse(await io.local().readFile(values.file, "utf8"));
|
|
2421
|
+
} else {
|
|
2422
|
+
const [baseUrl, ...ops] = more;
|
|
2423
|
+
if (!name || !baseUrl) {
|
|
2424
|
+
throw new Error(
|
|
2425
|
+
'usage: vibiz connectors add --file manifest.json | add <name> <base_url> "GET /path" \u2026 [--auth header:X-Api-Key] [--var NAME]'
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
const envName = values.var ?? `${name.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
|
|
2429
|
+
const [kind = "none", where = ""] = (values.auth ?? "none").split(":");
|
|
2430
|
+
const auth = kind === "bearer" ? { type: "bearer", env: envName } : kind === "header" ? { type: "header", header: where || "X-Api-Key", env: envName } : kind === "query" ? { type: "query", param: where || "api_key", env: envName } : { type: "none" };
|
|
2431
|
+
manifest = { name, base_url: baseUrl, auth, operations: ops };
|
|
2432
|
+
}
|
|
2433
|
+
const made = await c.create(id, manifest);
|
|
2434
|
+
out(made, () => {
|
|
2435
|
+
io.log(`${dot("ready")} Added ${made.name} (${made.base_url})`);
|
|
2436
|
+
printOps(made.operations);
|
|
2437
|
+
});
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
if (sub === "approvals") {
|
|
2441
|
+
const list = await c.approvals(id, { status: "pending" });
|
|
2442
|
+
out(list, () => {
|
|
2443
|
+
if (!list.length) io.log("Nothing waiting for approval.");
|
|
2444
|
+
for (const a of list) {
|
|
2445
|
+
io.log(
|
|
2446
|
+
`${a.id} ${a.connector}.${a.operation} (${a.environment}, ${a.reason}) until ${a.expires_at.slice(0, 16)}
|
|
2447
|
+
${JSON.stringify(a.request)}`
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
if (list.length) io.log("Approve with an owner key: vibiz connectors approve <id>");
|
|
2451
|
+
});
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
if (sub === "approve" || sub === "deny") {
|
|
2455
|
+
if (!name) throw new Error(`usage: vibiz connectors ${sub} <approval id>`);
|
|
2456
|
+
const a = await c.approval(id, name);
|
|
2457
|
+
io.log(`${a.connector}.${a.operation} (${a.environment}): ${JSON.stringify(a.request)}`);
|
|
2458
|
+
const r = sub === "approve" ? await c.approve(id, name) : await c.deny(id, name);
|
|
2459
|
+
out(
|
|
2460
|
+
r,
|
|
2461
|
+
() => io.log(
|
|
2462
|
+
r.status === "approved" ? `${dot("ready")} Approved: the agent can make this call once in the next hour (approval_id ${r.id}).` : "Denied."
|
|
2463
|
+
)
|
|
2464
|
+
);
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (!name)
|
|
2468
|
+
throw new Error(`usage: vibiz connectors ${sub} <name> \u2026 (vibiz connectors --help)`);
|
|
2469
|
+
if (sub === "ops") {
|
|
2470
|
+
const k = await c.get(id, name);
|
|
2471
|
+
out(k.operations, () => {
|
|
2472
|
+
io.log(`${k.label} ${k.base_url} ${ready(k)} (\u25CF enabled)`);
|
|
2473
|
+
printOps(k.operations);
|
|
2474
|
+
});
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
if (sub === "enable" || sub === "disable") {
|
|
2478
|
+
if (!more.length) throw new Error(`usage: vibiz connectors ${sub} <name> <operation\u2026>`);
|
|
2479
|
+
const k = await c.update(id, name, { [sub]: more });
|
|
2480
|
+
out(k.operations, () => printOps(k.operations));
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
if (sub === "secret") {
|
|
2484
|
+
const [key] = more;
|
|
2485
|
+
if (!key) throw new Error("usage: vibiz connectors secret <name> <VAR> [--dev]");
|
|
2486
|
+
const value = io.env[key] ?? await io.promptHidden(`${key} (hidden): `);
|
|
2487
|
+
if (!value) throw new Error(`No value: set $${key} or type it at the prompt.`);
|
|
2488
|
+
const k = await c.setSecrets(id, name, { [key]: value }, env);
|
|
2489
|
+
out(
|
|
2490
|
+
k.ready,
|
|
2491
|
+
() => io.log(`${dot("ready")} ${key} set (${ready(k)}). It is never shown again.`)
|
|
2492
|
+
);
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
if (sub === "check") {
|
|
2496
|
+
const r = await c.check(id, name, env);
|
|
2497
|
+
out(r, () => io.log(`${dot(r.ok ? "ready" : "failed")} ${r.message}`));
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
if (sub === "call") {
|
|
2501
|
+
const [op, ...pairs] = more;
|
|
2502
|
+
if (!op)
|
|
2503
|
+
throw new Error(
|
|
2504
|
+
"usage: vibiz connectors call <name> <operation> [key=value\u2026] [--body json]"
|
|
2505
|
+
);
|
|
2506
|
+
const params = {};
|
|
2507
|
+
for (const p of pairs) {
|
|
2508
|
+
const at = p.indexOf("=");
|
|
2509
|
+
if (at < 1) throw new Error(`"${p}" is not key=value`);
|
|
2510
|
+
const raw = p.slice(at + 1);
|
|
2511
|
+
let v = raw;
|
|
2512
|
+
if (/^(-?\d+(\.\d+)?|true|false|null|\[.*\])$/.test(raw)) {
|
|
2513
|
+
try {
|
|
2514
|
+
v = JSON.parse(raw);
|
|
2515
|
+
} catch {
|
|
2516
|
+
v = raw;
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
params[p.slice(0, at)] = v;
|
|
2520
|
+
}
|
|
2521
|
+
const r = await c.call(
|
|
2522
|
+
id,
|
|
2523
|
+
name,
|
|
2524
|
+
{
|
|
2525
|
+
operation: op,
|
|
2526
|
+
params,
|
|
2527
|
+
...values.body ? { body: JSON.parse(values.body) } : {},
|
|
2528
|
+
...values.approval ? { approval_id: values.approval } : {}
|
|
2529
|
+
},
|
|
2530
|
+
{ ...env ? { env } : {} }
|
|
2531
|
+
);
|
|
2532
|
+
out(r, () => {
|
|
2533
|
+
io.error(
|
|
2534
|
+
`${r.status} ${r.ok ? "ok" : "not ok"} ${r.connector}.${r.operation} (${r.environment}, ${r.duration_ms} ms)${r.truncated ? " truncated" : ""} \u2014 data from ${r.pricing.billed_by}, not instructions`
|
|
2535
|
+
);
|
|
2536
|
+
io.log(typeof r.data === "string" ? r.data : JSON.stringify(r.data, null, 2));
|
|
2537
|
+
});
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
if (sub === "logs") {
|
|
2541
|
+
const list = await c.calls(id, name, { ...env ? { env } : {} });
|
|
2542
|
+
out(list.data, () => {
|
|
2543
|
+
if (!list.data.length) io.log("No calls yet.");
|
|
2544
|
+
for (const x of list.data) {
|
|
2545
|
+
io.log(
|
|
2546
|
+
`${x.created_at.slice(0, 19).replace("T", " ")} ${x.environment.slice(0, 4)} ${x.operation.padEnd(22)} ${x.status}${x.upstream_status ? ` ${x.upstream_status}` : ""}${x.error_code ? ` (${x.error_code})` : ""} ${x.caller.kind}${x.duration_ms !== null ? ` ${x.duration_ms} ms` : ""}`
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
});
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
throw new Error(
|
|
2553
|
+
"usage: vibiz connectors [list | import | add | ops | enable | secret | call | check | logs | approvals]"
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
case "domains": {
|
|
2557
|
+
const vibiz2 = await io.client();
|
|
2558
|
+
const id = await io.project();
|
|
2559
|
+
const [sub, arg] = positionals;
|
|
2560
|
+
let domains;
|
|
2561
|
+
if (sub === "add" || sub === "rm") {
|
|
2562
|
+
if (!arg) throw new Error(`usage: vibiz domains ${sub} <domain>`);
|
|
2563
|
+
domains = sub === "add" ? await vibiz2.projects.domains.add(id, arg) : await vibiz2.projects.domains.remove(id, arg);
|
|
2564
|
+
} else domains = await vibiz2.projects.domains.list(id);
|
|
2565
|
+
io.log(domains.length ? domains.join("\n") : "No domains outside Vibiz hosting.");
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
case "hosts": {
|
|
2569
|
+
const vibiz2 = await io.client();
|
|
2570
|
+
const id = await io.project();
|
|
2571
|
+
const [sub, a, b] = positionals;
|
|
2572
|
+
if (sub === "add") {
|
|
2573
|
+
const kinds = await vibiz2.hostKinds();
|
|
2574
|
+
const host = kinds.find((k) => k.kind === a);
|
|
2575
|
+
if (!host) {
|
|
2576
|
+
throw new Error(
|
|
2577
|
+
`usage: vibiz hosts add <${kinds.map((k) => k.kind).join("|")}> <${"name"}> [field=value\u2026] [--dev]
|
|
2578
|
+
Any other host: vibiz connect`
|
|
2579
|
+
);
|
|
2580
|
+
}
|
|
2581
|
+
const config = {};
|
|
2582
|
+
for (const arg of positionals.slice(2)) {
|
|
2583
|
+
const eq = arg.indexOf("=");
|
|
2584
|
+
if (eq > 0) config[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
2585
|
+
else {
|
|
2586
|
+
const first = host.fields.find((f) => f.required && !config[f.name]);
|
|
2587
|
+
if (first) config[first.name] = arg;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
if (values.team) config.team_id = values.team;
|
|
2591
|
+
const missing = host.fields.filter((f) => f.required && !config[f.name]);
|
|
2592
|
+
if (missing.length) {
|
|
2593
|
+
throw new Error(
|
|
2594
|
+
`${host.label} needs ${missing.map((f) => `${f.name}=<${f.placeholder}>`).join(" ")}`
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
const envName = TOKEN_ENV[host.kind];
|
|
2598
|
+
const token = envName && io.env[envName] || await io.promptHidden(`${host.label} token (${host.token_help}): `);
|
|
2599
|
+
if (!token) {
|
|
2600
|
+
throw new Error(
|
|
2601
|
+
`A ${host.label} token is needed${envName ? ` (${envName}, or type it)` : ""}.`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
const t = await vibiz2.projects.hosts.add(id, {
|
|
2605
|
+
kind: host.kind,
|
|
2606
|
+
token,
|
|
2607
|
+
config,
|
|
2608
|
+
environments: values.dev && host.environments.includes("development") ? ["production", "development"] : ["production"]
|
|
2609
|
+
});
|
|
2610
|
+
io.log(
|
|
2611
|
+
`Connected ${host.label} ${t.name} (${t.id}). Vibiz writes the env there now and on every change; it applies ${host.applies}.`
|
|
2612
|
+
);
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
if (sub === "sync") {
|
|
2616
|
+
await vibiz2.projects.hosts.sync(id);
|
|
2617
|
+
io.log("Syncing every connected host.");
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (sub === "rm") {
|
|
2621
|
+
if (!a) throw new Error("usage: vibiz hosts rm <id> [--keep-values]");
|
|
2622
|
+
await vibiz2.projects.hosts.remove(id, a, { keepValues: values["keep-values"] });
|
|
2623
|
+
io.log(
|
|
2624
|
+
values["keep-values"] ? `Disconnected ${a}; its variables stay but get no more updates.` : `Disconnected ${a}; removing the variables Vibiz wrote there.`
|
|
2625
|
+
);
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
const list = await vibiz2.projects.hosts.list(id);
|
|
2629
|
+
if (!list.length) io.log("No connected hosts. Add one: vibiz hosts add vercel <project>");
|
|
2630
|
+
for (const t of list) {
|
|
2631
|
+
io.log(
|
|
2632
|
+
`${t.id} ${t.kind} ${t.name.padEnd(24)} ${t.environments.join("+").padEnd(23)} ${t.status}${t.last_synced_at ? ` synced ${t.last_synced_at}` : ""}${t.error ? `
|
|
2633
|
+
${t.error}` : ""}`
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
return;
|
|
2637
|
+
}
|
|
2638
|
+
case "app-key": {
|
|
2639
|
+
const vibiz2 = await io.client();
|
|
2640
|
+
const id = await io.project();
|
|
2641
|
+
if (positionals[0] === "rotate") {
|
|
2642
|
+
const k = await vibiz2.projects.appKeys.rotate(id, {
|
|
2643
|
+
environment: values.dev ? "development" : "production",
|
|
2644
|
+
...values.grace ? { grace_seconds: Number(values.grace) } : {}
|
|
2645
|
+
});
|
|
2646
|
+
io.log(
|
|
2647
|
+
`New ${k.environment} key ${k.prefix}\u2026; the old one works until ${k.previous_valid_until ?? "now (revoked)"}.
|
|
2648
|
+
Hosting and connected hosts get the new value by themselves.`
|
|
2649
|
+
);
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
for (const k of await vibiz2.projects.appKeys.list(id)) {
|
|
2653
|
+
io.log(
|
|
2654
|
+
`${k.environment.padEnd(12)} ${k.prefix ? `${k.prefix}\u2026` : "(issued on first use)"}${k.last_used_at ? ` last used ${k.last_used_at}` : ""}`
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
case "run": {
|
|
2660
|
+
if (!passthrough.length) {
|
|
2661
|
+
const action = positionals[0];
|
|
2662
|
+
if (!action) {
|
|
2663
|
+
throw new Error(
|
|
2664
|
+
"usage: vibiz run <action> --input '<json>' or vibiz run -- <command\u2026>"
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2667
|
+
const input = values.input ? JSON.parse(values.input) : {};
|
|
2668
|
+
const out = await (await io.client()).actions.run(action, input);
|
|
2669
|
+
io.log(JSON.stringify(out.output, null, 2));
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
const command2 = passthrough;
|
|
2673
|
+
const local = io.local();
|
|
2674
|
+
let env;
|
|
2675
|
+
let where;
|
|
2676
|
+
if (io.env.VIBIZ_APP_KEY) {
|
|
2677
|
+
const app = createVibiz();
|
|
2678
|
+
env = await app.loadEnv({ override: false });
|
|
2679
|
+
where = (await app.whoami()).environment;
|
|
2680
|
+
} else {
|
|
2681
|
+
env = await (await io.client()).projects.dev.ensure(await io.project());
|
|
2682
|
+
where = "development";
|
|
2683
|
+
}
|
|
2684
|
+
io.err(`vibiz: ${Object.keys(env).length} variables (${where})
|
|
2685
|
+
`);
|
|
2686
|
+
const [cmd, ...args] = command2;
|
|
2687
|
+
return local.exit(
|
|
2688
|
+
await local.exec(cmd, args, { ...env, ...io.env }, { forwardSignals: true })
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
case "connect": {
|
|
2692
|
+
const found = await detectHosts(io.local().cwd());
|
|
2693
|
+
io.log("Recommended: Vibiz hosting. Every key is already there and stays in sync:");
|
|
2694
|
+
io.log(" vibiz deploy\n");
|
|
2695
|
+
if (!found.length) {
|
|
2696
|
+
io.log("No other host found in this folder. Hosting somewhere else?");
|
|
2697
|
+
io.log(ONE_KEY_PATHS);
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
for (const h of found) {
|
|
2701
|
+
io.log(`Found ${h.label} (${h.file}).`);
|
|
2702
|
+
if (h.push) {
|
|
2703
|
+
io.log(` Vibiz can write the env into it and keep it in sync (an owner, once):`);
|
|
2704
|
+
io.log(` vibiz hosts add ${h.kind} <its project or service>
|
|
2705
|
+
`);
|
|
2706
|
+
} else {
|
|
2707
|
+
io.log(`${ONE_KEY_PATHS}
|
|
2708
|
+
`);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
io.log(
|
|
2712
|
+
"Running it elsewhere? Add its domain too, so sign-in accepts it: vibiz domains add <domain>"
|
|
2713
|
+
);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
case "env": {
|
|
2717
|
+
if (positionals[0] === "pull") {
|
|
2718
|
+
io.local();
|
|
2719
|
+
const format2 = values.format ?? "dotenv";
|
|
2720
|
+
if (!["dotenv", "shell", "docker", "json"].includes(format2)) {
|
|
2721
|
+
throw new Error("--format is dotenv, shell, docker or json");
|
|
2722
|
+
}
|
|
2723
|
+
const key = io.env.VIBIZ_APP_KEY;
|
|
2724
|
+
if (!key) throw new Error("vibiz env pull needs VIBIZ_APP_KEY (the app\u2019s own key).");
|
|
2725
|
+
const base = (io.env.VIBIZ_API_URL ?? "https://app.vibiz.app").replace(/\/$/, "");
|
|
2726
|
+
const res = await fetch(`${base}/v1/app/env?format=${format2}`, {
|
|
2727
|
+
headers: { authorization: `Bearer ${key}` }
|
|
2728
|
+
});
|
|
2729
|
+
const text = await res.text();
|
|
2730
|
+
if (!res.ok) throw new Error(`Vibiz answered ${res.status}: ${text.slice(0, 200)}`);
|
|
2731
|
+
if (values.output) {
|
|
2732
|
+
await io.local().writeFile(values.output, text, { mode: 384 });
|
|
2733
|
+
await io.local().chmod(values.output, 384);
|
|
2734
|
+
io.err(`Wrote the app's env to ${values.output}
|
|
2735
|
+
`);
|
|
2736
|
+
} else io.out(text);
|
|
2737
|
+
return;
|
|
2738
|
+
}
|
|
2739
|
+
const env = await (await io.client()).projects.env(await io.project());
|
|
2740
|
+
if (values.write) {
|
|
2741
|
+
const existing = await io.local().readFile(values.write, "utf8").catch(() => "");
|
|
2742
|
+
await io.local().writeFile(values.write, mergeEnvFile(existing, env), { mode: 384 });
|
|
2743
|
+
await io.local().chmod(values.write, 384);
|
|
2744
|
+
io.log(`Wrote ${Object.keys(env).length} variables to ${values.write}`);
|
|
2745
|
+
} else io.log(mergeEnvFile("", env).trimEnd());
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
case "add": {
|
|
2749
|
+
const type = positionals[0];
|
|
2750
|
+
if (!type) throw new Error("usage: vibiz add <hosting|database|auth|email|analytics|replay>");
|
|
2751
|
+
const vibiz2 = await io.client();
|
|
2752
|
+
const id = await io.project();
|
|
2753
|
+
await vibiz2.projects.addResource(id, type);
|
|
2754
|
+
printProject(io, await vibiz2.projects.waitUntilReady(id));
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
case "analytics": {
|
|
2758
|
+
const period = values.period ?? "7d";
|
|
2759
|
+
if (!["24h", "7d", "30d"].includes(period)) throw new Error("--period is 24h, 7d or 30d");
|
|
2760
|
+
const report = await (await io.client()).analytics.get(await io.project(), { period });
|
|
2761
|
+
if (values.json) io.log(JSON.stringify(report, null, 2));
|
|
2762
|
+
else printAnalytics(io, report);
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
case "replays": {
|
|
2766
|
+
const period = values.period ?? "7d";
|
|
2767
|
+
if (!["24h", "7d", "30d"].includes(period)) throw new Error("--period is 24h, 7d or 30d");
|
|
2768
|
+
const vibiz2 = await io.client();
|
|
2769
|
+
const id = await io.project();
|
|
2770
|
+
const filtered = values.errors || values.rage || values.dead || values.path;
|
|
2771
|
+
const [insights, list] = await Promise.all([
|
|
2772
|
+
filtered ? null : vibiz2.replays.insights(id, { period }),
|
|
2773
|
+
vibiz2.replays.list(id, {
|
|
2774
|
+
period,
|
|
2775
|
+
has_error: values.errors,
|
|
2776
|
+
rage: values.rage,
|
|
2777
|
+
dead: values.dead,
|
|
2778
|
+
...values.path ? { path: values.path } : {}
|
|
2779
|
+
})
|
|
2780
|
+
]);
|
|
2781
|
+
if (values.json) {
|
|
2782
|
+
io.log(JSON.stringify({ insights, replays: list.data }, null, 2));
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
if (insights) printInsights(io, insights);
|
|
2786
|
+
io.log(`
|
|
2787
|
+
sessions (most worth watching first)`);
|
|
2788
|
+
if (!list.data.length) io.log(" none");
|
|
2789
|
+
for (const r of list.data) {
|
|
2790
|
+
const flags = [
|
|
2791
|
+
r.counts.errors ? `${r.counts.errors} err` : "",
|
|
2792
|
+
r.counts.rage_clicks ? `${r.counts.rage_clicks} rage` : "",
|
|
2793
|
+
r.counts.dead_clicks ? `${r.counts.dead_clicks} dead` : "",
|
|
2794
|
+
r.incomplete ? "partial" : ""
|
|
2795
|
+
].filter(Boolean);
|
|
2796
|
+
io.log(
|
|
2797
|
+
` ${r.id} ${new Date(r.started_at).toLocaleString()} ${clock(r.duration_ms).padStart(6)} ${r.trigger.padEnd(6)} ${flags.join(", ").padEnd(22)} ${r.pages.join(" \u2192 ")}`
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
if (list.data[0]) io.log(`
|
|
2801
|
+
Details and a link to watch: vibiz replay ${list.data[0].id}`);
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
case "replay": {
|
|
2805
|
+
const id = positionals[0];
|
|
2806
|
+
if (!id) throw new Error("usage: vibiz replay <id>");
|
|
2807
|
+
const vibiz2 = await io.client();
|
|
2808
|
+
const [detail, share] = await Promise.all([vibiz2.replays.get(id), vibiz2.replays.share(id)]);
|
|
2809
|
+
if (values.json) io.log(JSON.stringify({ ...detail, share }, null, 2));
|
|
2810
|
+
else printReplay(io, detail, share.url, share.expires_at);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
case "projects": {
|
|
2814
|
+
for (const p of await (await io.client()).projects.list()) {
|
|
2815
|
+
io.log(`${p.id} ${p.name.padEnd(24)} ${p.url ?? ""}`);
|
|
2816
|
+
}
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2819
|
+
case "send": {
|
|
2820
|
+
if (!values.to || !values.subject || !values.text) {
|
|
2821
|
+
throw new Error("usage: vibiz send --to <addr> --subject <s> --text <body> [--from info]");
|
|
2822
|
+
}
|
|
2823
|
+
const out = await (await io.client()).email.send(await io.project(), {
|
|
2824
|
+
to: values.to,
|
|
2825
|
+
subject: values.subject,
|
|
2826
|
+
text: values.text,
|
|
2827
|
+
...values.from ? { from: values.from } : {}
|
|
2828
|
+
});
|
|
2829
|
+
const usage = out.usage ? ` (${out.usage.used}/${out.usage.limit} today)` : "";
|
|
2830
|
+
io.log(`${dot("ready")} Sent ${out.output.from} \u2192 ${out.output.to}${usage}`);
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
case "ai": {
|
|
2834
|
+
if (positionals[0] === "keys") {
|
|
2835
|
+
await aiKeysCommand(io, positionals.slice(1), values);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
if (positionals[0] === "models") {
|
|
2839
|
+
const q = positionals.slice(1).join(" ");
|
|
2840
|
+
const models = await (await io.client()).ai.models({ all: values.all || !!q, q });
|
|
2841
|
+
if (values.json) {
|
|
2842
|
+
io.log(JSON.stringify(models, null, 2));
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
if (!models.length) {
|
|
2846
|
+
io.log("No model matches. Try fewer words, or `vibiz ai models --all`.");
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
for (const m of models) io.log(modelLine(m));
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
const prompt = positionals.join(" ");
|
|
2853
|
+
if (!prompt)
|
|
2854
|
+
throw new Error('usage: vibiz ai "<prompt>" [--model vibiz/smart] [--system "\u2026"]');
|
|
2855
|
+
const vibiz2 = await io.aiClient(values.project);
|
|
2856
|
+
let cost;
|
|
2857
|
+
for await (const chunk of vibiz2.ai.chatStream({
|
|
2858
|
+
...values.model ? { model: values.model } : {},
|
|
2859
|
+
messages: [
|
|
2860
|
+
...values.system ? [{ role: "system", content: values.system }] : [],
|
|
2861
|
+
{ role: "user", content: prompt }
|
|
2862
|
+
],
|
|
2863
|
+
stream_options: { include_usage: true }
|
|
2864
|
+
})) {
|
|
2865
|
+
io.out(chunk.choices[0]?.delta.content ?? "");
|
|
2866
|
+
if (chunk.cost_micro_usd !== void 0) cost = chunk.cost_micro_usd;
|
|
2867
|
+
}
|
|
2868
|
+
io.out("\n");
|
|
2869
|
+
if (cost !== void 0) io.error(` ${usd(cost)}`);
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
case "image": {
|
|
2873
|
+
io.local();
|
|
2874
|
+
const prompt = positionals.join(" ");
|
|
2875
|
+
if (!prompt || !values.output) throw new Error('usage: vibiz image "<prompt>" -o out.png');
|
|
2876
|
+
const vibiz2 = await io.aiClient(values.project);
|
|
2877
|
+
const out = await vibiz2.ai.image({
|
|
2878
|
+
prompt,
|
|
2879
|
+
...values.size ? { size: values.size } : {},
|
|
2880
|
+
...values.model ? { model: values.model } : {}
|
|
2881
|
+
});
|
|
2882
|
+
const b64 = out.data[0]?.b64_json;
|
|
2883
|
+
if (!b64) throw new Error("No image came back.");
|
|
2884
|
+
await io.local().writeFile(values.output, Buffer.from(b64, "base64"));
|
|
2885
|
+
io.log(`${dot("ready")} ${values.output} (${out.model}, ${usd(out.cost_micro_usd)})`);
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
case "say": {
|
|
2889
|
+
io.local();
|
|
2890
|
+
const text = positionals.join(" ");
|
|
2891
|
+
if (!text || !values.output)
|
|
2892
|
+
throw new Error('usage: vibiz say "<text>" -o out.mp3 [--voice alloy]');
|
|
2893
|
+
const vibiz2 = await io.aiClient(values.project);
|
|
2894
|
+
const audio = await vibiz2.ai.speech({
|
|
2895
|
+
input: text,
|
|
2896
|
+
...values.voice ? { voice: values.voice } : {},
|
|
2897
|
+
...values.model ? { model: values.model } : {}
|
|
2898
|
+
});
|
|
2899
|
+
await io.local().writeFile(values.output, audio);
|
|
2900
|
+
io.log(`${dot("ready")} ${values.output} (${audio.byteLength} bytes)`);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
case "transcribe": {
|
|
2904
|
+
io.local();
|
|
2905
|
+
const file = positionals[0];
|
|
2906
|
+
if (!file) throw new Error("usage: vibiz transcribe <audio-file> [--language en]");
|
|
2907
|
+
const vibiz2 = await io.aiClient(values.project);
|
|
2908
|
+
const out = await vibiz2.ai.transcribe({
|
|
2909
|
+
file: await io.local().readFile(file),
|
|
2910
|
+
filename: file.split("/").pop() ?? "audio",
|
|
2911
|
+
...values.language ? { language: values.language } : {},
|
|
2912
|
+
...values.model ? { model: values.model } : {}
|
|
2913
|
+
});
|
|
2914
|
+
io.log(out.text);
|
|
2915
|
+
io.error(` ${usd(out.cost_micro_usd)}`);
|
|
2916
|
+
return;
|
|
2917
|
+
}
|
|
2918
|
+
case "usage": {
|
|
2919
|
+
const vibiz2 = await io.client();
|
|
2920
|
+
const period = usagePeriod("30d");
|
|
2921
|
+
if (values.open) {
|
|
2922
|
+
const link = await vibiz2.dashboard.link({ period });
|
|
2923
|
+
io.openUrl(link.url);
|
|
2924
|
+
io.log(`Dashboard (until ${new Date(link.expires_at).toLocaleTimeString()}): ${link.url}`);
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
const report = await vibiz2.usage.get({
|
|
2928
|
+
period,
|
|
2929
|
+
group_by: (values.by ?? "tool").split(","),
|
|
2930
|
+
...values.project ? { project: values.project } : {},
|
|
2931
|
+
...values.model ? { model: values.model } : {},
|
|
2932
|
+
...values.tool ? { tool: values.tool } : {},
|
|
2933
|
+
...values["end-user"] ? { end_user: values["end-user"] } : {},
|
|
2934
|
+
...values["api-key"] ? { api_key: values["api-key"] } : {}
|
|
2935
|
+
});
|
|
2936
|
+
io.log(values.json ? JSON.stringify(report, null, 2) : formatUsage(report));
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
case "billing": {
|
|
2940
|
+
const vibiz2 = await io.client();
|
|
2941
|
+
const [sub, arg] = positionals;
|
|
2942
|
+
if (!sub) {
|
|
2943
|
+
const b = await vibiz2.billing.get();
|
|
2944
|
+
if (values.json) {
|
|
2945
|
+
io.log(JSON.stringify(b, null, 2));
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
const history = b.mode === "credits" ? await vibiz2.billing.ledger({ limit: 5 }) : void 0;
|
|
2949
|
+
io.log(formatBilling(b, history));
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (sub === "top-up" || sub === "topup") {
|
|
2953
|
+
const c = await vibiz2.billing.checkout({ amount_cents: parseDollars(arg) });
|
|
2954
|
+
if (values.json) {
|
|
2955
|
+
io.log(JSON.stringify(c, null, 2));
|
|
2956
|
+
return;
|
|
2957
|
+
}
|
|
2958
|
+
if (values.open && c.url) io.openUrl(c.url);
|
|
2959
|
+
io.log(
|
|
2960
|
+
`Pay $${(c.amount_cents / 100).toFixed(2)} here (open it in a browser; the link works for an hour):
|
|
2961
|
+
${c.url}
|
|
2962
|
+
|
|
2963
|
+
The credits show in \`vibiz billing\` a few seconds after paying.`
|
|
2964
|
+
);
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
if (sub === "history") {
|
|
2968
|
+
const kinds = values.kind ? values.kind.split(",") : void 0;
|
|
2969
|
+
const list = await vibiz2.billing.ledger({
|
|
2970
|
+
...kinds ? { kind: kinds } : {},
|
|
2971
|
+
limit: 50
|
|
2972
|
+
});
|
|
2973
|
+
io.log(
|
|
2974
|
+
values.json ? JSON.stringify(list, null, 2) : list.data.length ? list.data.map(formatEntry).join("\n") : "Nothing yet."
|
|
2975
|
+
);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
if (sub === "auto-recharge") {
|
|
2979
|
+
if (arg !== "on" && arg !== "off") {
|
|
2980
|
+
throw new Error(
|
|
2981
|
+
"usage: vibiz billing auto-recharge on|off [--threshold 5] [--amount 25]"
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
const b = await vibiz2.billing.update({
|
|
2985
|
+
auto_recharge: {
|
|
2986
|
+
enabled: arg === "on",
|
|
2987
|
+
...values.threshold ? { threshold_cents: parseDollars(values.threshold, "--threshold") } : {},
|
|
2988
|
+
...values.amount ? { amount_cents: parseDollars(values.amount, "--amount") } : {}
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
io.log(values.json ? JSON.stringify(b, null, 2) : formatBilling(b));
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
throw new Error(
|
|
2995
|
+
"usage: vibiz billing [top-up <dollars> | history [--kind k] | auto-recharge on|off]"
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
case "metrics": {
|
|
2999
|
+
const report = await (await io.client()).metrics.get({ period: usagePeriod("7d") });
|
|
3000
|
+
io.log(values.json ? JSON.stringify(report, null, 2) : formatMetrics(report));
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
case "keys": {
|
|
3004
|
+
const vibiz2 = await io.client();
|
|
3005
|
+
const [sub, arg] = positionals;
|
|
3006
|
+
if (sub === "create") {
|
|
3007
|
+
if (!arg) throw new Error("usage: vibiz keys create <name> [--owner]");
|
|
3008
|
+
const key = await vibiz2.apiKeys.create(arg, { access: values.owner ? "owner" : "agent" });
|
|
3009
|
+
io.log(
|
|
3010
|
+
`${key.name} ${key.id} ${key.access}
|
|
3011
|
+
${key.secret}
|
|
3012
|
+
|
|
3013
|
+
Store the key now: it is not shown again.`
|
|
3014
|
+
);
|
|
3015
|
+
return;
|
|
3016
|
+
}
|
|
3017
|
+
if (sub === "revoke") {
|
|
3018
|
+
if (!arg) throw new Error("usage: vibiz keys revoke <key id>");
|
|
3019
|
+
await vibiz2.apiKeys.revoke(arg);
|
|
3020
|
+
io.log(`Revoked ${arg}.`);
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
for (const k of await vibiz2.apiKeys.list()) {
|
|
3024
|
+
io.log(
|
|
3025
|
+
`${k.id} ${k.name.padEnd(20)} ${k.prefix}\u2026 ${(k.access ?? "owner").padEnd(5)} ${k.revoked_at ? "revoked" : "active"}`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
return;
|
|
3029
|
+
}
|
|
3030
|
+
case "admin": {
|
|
3031
|
+
if (positionals[0] !== "metrics") throw new Error("usage: vibiz admin metrics [--period 7d]");
|
|
3032
|
+
const vibiz2 = await io.client();
|
|
3033
|
+
const period = usagePeriod("7d");
|
|
3034
|
+
if (values.open) {
|
|
3035
|
+
const link = await vibiz2.dashboard.link({ scope: "admin", period });
|
|
3036
|
+
io.openUrl(link.url);
|
|
3037
|
+
io.log(`Platform dashboard: ${link.url}`);
|
|
3038
|
+
return;
|
|
3039
|
+
}
|
|
3040
|
+
const m = await vibiz2.admin.metrics({ period });
|
|
3041
|
+
io.log(values.json ? JSON.stringify(m, null, 2) : formatAdminMetrics(m));
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
case "docs": {
|
|
3045
|
+
const { DOC_TOPICS, DOCS } = await import("./src-BOTTM5XC.js");
|
|
3046
|
+
const topic = positionals[0];
|
|
3047
|
+
if (!topic || !DOC_TOPICS.includes(topic)) {
|
|
3048
|
+
for (const t of DOC_TOPICS) io.log(` vibiz docs ${t.padEnd(13)} ${DOCS[t].title}`);
|
|
3049
|
+
if (topic) io.setExitCode(1);
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
io.log(`# ${DOCS[topic].title}
|
|
3053
|
+
|
|
3054
|
+
${DOCS[topic].body}`);
|
|
3055
|
+
return;
|
|
3056
|
+
}
|
|
3057
|
+
case "skills": {
|
|
3058
|
+
const [sub = "list", ...names] = positionals;
|
|
3059
|
+
if (sub === "list") return skillsList(io, await apiBase(), values);
|
|
3060
|
+
if (sub === "add") return skillsAdd(io, await apiBase(), names, values);
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
"usage: vibiz skills [list | add [name\u2026] [--global] [--agent claude|codex|cursor]]"
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
case "mcp": {
|
|
3066
|
+
if (positionals[0] === "add") return mcpAdd(io, await apiBase(), values.clients);
|
|
3067
|
+
if (positionals[0])
|
|
3068
|
+
throw new Error('usage: vibiz mcp (serve over stdio) | vibiz mcp add --clients "<client>"');
|
|
3069
|
+
io.local();
|
|
3070
|
+
const { serveStdio } = await import("./src-BOTTM5XC.js");
|
|
3071
|
+
await serveStdio({ version: cliVersion() });
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
3074
|
+
case void 0:
|
|
3075
|
+
case "help":
|
|
3076
|
+
case "--help":
|
|
3077
|
+
io.log(HELP);
|
|
3078
|
+
return;
|
|
3079
|
+
default:
|
|
3080
|
+
throw new Error(`Unknown command "${command}". Run: vibiz help`);
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
function cliErrorLines(err) {
|
|
3084
|
+
if (err instanceof VibizApiError && err.code === "forbidden" && /production env/.test(err.message)) {
|
|
3085
|
+
return [
|
|
3086
|
+
`\u2715 ${err.message}`,
|
|
3087
|
+
" For local work: vibiz dev (a development database, sign-in and AI key)."
|
|
3088
|
+
];
|
|
3089
|
+
}
|
|
3090
|
+
if (err instanceof VibizApiError) {
|
|
3091
|
+
const out = [`\u2715 ${err.message} (${err.code})`];
|
|
3092
|
+
const d = err.details;
|
|
3093
|
+
if (d?.approval_id) out.push(` approval_id: ${d.approval_id}`);
|
|
3094
|
+
if (d?.next_step) out.push(` ${d.next_step}`);
|
|
3095
|
+
return out;
|
|
3096
|
+
}
|
|
3097
|
+
return [`\u2715 ${err instanceof Error ? err.message : String(err)}`];
|
|
3098
|
+
}
|
|
3099
|
+
function modelLine(m) {
|
|
3100
|
+
const runs = m.model ? `${m.model.name} (${m.model.maker})` : `${m.name} (${m.maker ?? ""})`;
|
|
3101
|
+
const p = m.pricing ?? {};
|
|
3102
|
+
const price = p.unit === "tokens" ? `$${p.input_per_million_usd} / $${p.output_per_million_usd} per M tokens` : p.unit === "characters" ? `$${p.per_1000_usd} per 1k chars` : typeof p.usd === "number" ? `$${p.usd} per ${p.unit}` : "";
|
|
3103
|
+
return `${m.id.padEnd(34)} ${runs.padEnd(40)} ${price}`;
|
|
3104
|
+
}
|
|
3105
|
+
async function aiKeysCommand(io, args, values) {
|
|
3106
|
+
const vibiz2 = await io.client();
|
|
3107
|
+
const [sub = "list", provider = "google"] = args;
|
|
3108
|
+
if (provider !== "google") throw new Error("Supported provider: google");
|
|
3109
|
+
const scope = values.project ? { projectId: values.project } : {};
|
|
3110
|
+
const show = (k) => {
|
|
3111
|
+
if (values.json) return io.log(JSON.stringify(k, null, 2));
|
|
3112
|
+
const where = k.scope === "project" ? `project ${k.project_id}` : "account";
|
|
3113
|
+
if (!k.has_key) {
|
|
3114
|
+
io.log(
|
|
3115
|
+
`${k.provider} (${where}): no key${k.inherits_account_key ? ", uses the account key" : ""}`
|
|
3116
|
+
);
|
|
3117
|
+
return;
|
|
3118
|
+
}
|
|
3119
|
+
const kind = k.type === "vertex" ? `Vertex AI ${k.vertex?.project_id} ${k.vertex?.region}` : "API key";
|
|
3120
|
+
io.log(
|
|
3121
|
+
`${dot(k.last_error ? "failed" : "ready")} ${k.provider} (${where}): ${kind} \u2026${k.last4}`
|
|
3122
|
+
);
|
|
3123
|
+
io.log(
|
|
3124
|
+
` last used: ${k.last_used_at ?? "never"}${k.last_error ? ` last error: ${k.last_error.message}` : ""}`
|
|
3125
|
+
);
|
|
3126
|
+
const mine = (k.models ?? []).filter((m) => m.paid_by === "your_key").map((m) => m.model);
|
|
3127
|
+
const ours = (k.models ?? []).filter((m) => m.paid_by === "vibiz").map((m) => m.model);
|
|
3128
|
+
io.log(` on your key (0 here): ${mine.join(", ") || "none"}`);
|
|
3129
|
+
io.log(` on Vibiz (billed): ${ours.join(", ") || "none"}`);
|
|
3130
|
+
};
|
|
3131
|
+
const useFor = values["use-for"] ? values["use-for"].split(",").map((x) => x.trim()) : void 0;
|
|
3132
|
+
switch (sub) {
|
|
3133
|
+
case "list": {
|
|
3134
|
+
const out = await vibiz2.aiKeys.list(scope);
|
|
3135
|
+
for (const k of out.data) show(k);
|
|
3136
|
+
if (!values.json && out.usage_30d.calls) {
|
|
3137
|
+
io.log(
|
|
3138
|
+
` last 30 days: ${out.usage_30d.calls} calls on your keys, would have cost $${out.usage_30d.would_have_cost_usd.toFixed(2)}`
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
return;
|
|
3142
|
+
}
|
|
3143
|
+
case "set": {
|
|
3144
|
+
if (values.vertex) {
|
|
3145
|
+
const file = (await io.local().readFile(values.vertex)).toString();
|
|
3146
|
+
show(
|
|
3147
|
+
await vibiz2.aiKeys.set(
|
|
3148
|
+
provider,
|
|
3149
|
+
{
|
|
3150
|
+
type: "vertex",
|
|
3151
|
+
service_account: file,
|
|
3152
|
+
...values["gcp-project"] ? { project_id: values["gcp-project"] } : {},
|
|
3153
|
+
...values.region ? { region: values.region } : {},
|
|
3154
|
+
...useFor ? { use_for: useFor } : {}
|
|
3155
|
+
},
|
|
3156
|
+
scope
|
|
3157
|
+
)
|
|
3158
|
+
);
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
const key = io.env.GEMINI_API_KEY || io.env.GOOGLE_API_KEY || await io.promptHidden("API key (hidden): ");
|
|
3162
|
+
if (!key) throw new Error("The key is needed: GEMINI_API_KEY, or type it.");
|
|
3163
|
+
show(
|
|
3164
|
+
await vibiz2.aiKeys.set(
|
|
3165
|
+
provider,
|
|
3166
|
+
{ type: "gemini_api_key", api_key: key.trim(), ...useFor ? { use_for: useFor } : {} },
|
|
3167
|
+
scope
|
|
3168
|
+
)
|
|
3169
|
+
);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
case "test": {
|
|
3173
|
+
const out = await vibiz2.aiKeys.test(provider, scope);
|
|
3174
|
+
show(out);
|
|
3175
|
+
if (out.test && !out.test.ok)
|
|
3176
|
+
throw new Error(out.last_error?.message ?? "The key failed its check.");
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
case "remove": {
|
|
3180
|
+
show(await vibiz2.aiKeys.remove(provider, scope));
|
|
3181
|
+
return;
|
|
3182
|
+
}
|
|
3183
|
+
default:
|
|
3184
|
+
throw new Error("usage: vibiz ai keys [list | set | test | remove] [google] [--project id]");
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
// packages/cli/src/main.ts
|
|
3189
|
+
async function readJson2(path) {
|
|
3190
|
+
try {
|
|
3191
|
+
return JSON.parse(await readFile2(path, "utf8"));
|
|
3192
|
+
} catch {
|
|
3193
|
+
return null;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
async function login() {
|
|
3197
|
+
const saved = await readJson2(CONFIG_FILE) ?? {};
|
|
3198
|
+
const apiKey = process.env.VIBIZ_API_KEY || saved.apiKey;
|
|
3199
|
+
if (!apiKey) throw new Error("Not logged in. Run: vibiz login (it prints a link to approve)");
|
|
3200
|
+
const baseUrl = process.env.VIBIZ_API_URL || saved.apiUrl || DEFAULT_API_URL;
|
|
3201
|
+
return { apiKey, baseUrl };
|
|
3202
|
+
}
|
|
3203
|
+
async function promptHidden(question) {
|
|
3204
|
+
const { stdin, stderr } = process;
|
|
3205
|
+
if (!stdin.isTTY) return "";
|
|
3206
|
+
stderr.write(question);
|
|
3207
|
+
stdin.setRawMode(true);
|
|
3208
|
+
stdin.resume();
|
|
3209
|
+
stdin.setEncoding("utf8");
|
|
3210
|
+
return new Promise((done) => {
|
|
3211
|
+
let value = "";
|
|
3212
|
+
const onData = (ch) => {
|
|
3213
|
+
if (ch === "\r" || ch === "\n" || ch === "") {
|
|
3214
|
+
stdin.setRawMode(false);
|
|
3215
|
+
stdin.pause();
|
|
3216
|
+
stdin.off("data", onData);
|
|
3217
|
+
stderr.write("\n");
|
|
3218
|
+
done(value.trim());
|
|
3219
|
+
} else if (ch === "") {
|
|
3220
|
+
process.exit(130);
|
|
3221
|
+
} else if (ch === "\x7F") value = value.slice(0, -1);
|
|
3222
|
+
else value += ch;
|
|
3223
|
+
};
|
|
3224
|
+
stdin.on("data", onData);
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
async function promptLine(question) {
|
|
3228
|
+
if (!process.stdin.isTTY) return "";
|
|
3229
|
+
const { createInterface } = await import("node:readline/promises");
|
|
3230
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
3231
|
+
try {
|
|
3232
|
+
return (await rl.question(question)).trim();
|
|
3233
|
+
} finally {
|
|
3234
|
+
rl.close();
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
function openUrl(url) {
|
|
3238
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
3239
|
+
try {
|
|
3240
|
+
spawn(cmd, [url], { stdio: "ignore", detached: true }).unref();
|
|
3241
|
+
} catch {
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
var terminal = {
|
|
3245
|
+
log: (...parts) => console.log(format(...parts)),
|
|
3246
|
+
error: (...parts) => console.error(format(...parts)),
|
|
3247
|
+
out: (text) => void process.stdout.write(text),
|
|
3248
|
+
err: (text) => void process.stderr.write(text),
|
|
3249
|
+
env: process.env,
|
|
3250
|
+
client: async () => new Vibiz(await login()),
|
|
3251
|
+
aiClient: async (project) => {
|
|
3252
|
+
const { apiKey, baseUrl } = await login();
|
|
3253
|
+
const headers = project ? { "x-vibiz-project": project } : void 0;
|
|
3254
|
+
return new Vibiz({
|
|
3255
|
+
apiKey,
|
|
3256
|
+
baseUrl,
|
|
3257
|
+
...headers ? {
|
|
3258
|
+
fetch: ((url, init) => fetch(url, {
|
|
3259
|
+
...init,
|
|
3260
|
+
headers: { ...init?.headers, ...headers }
|
|
3261
|
+
}))
|
|
3262
|
+
} : {}
|
|
3263
|
+
});
|
|
3264
|
+
},
|
|
3265
|
+
project: async () => {
|
|
3266
|
+
const linked = await readJson2(PROJECT_FILE);
|
|
3267
|
+
if (!linked?.project_id) throw new Error(`No ${PROJECT_FILE} here. Run: vibiz init <name>`);
|
|
3268
|
+
return linked.project_id;
|
|
3269
|
+
},
|
|
3270
|
+
promptHidden,
|
|
3271
|
+
promptLine,
|
|
3272
|
+
openUrl,
|
|
3273
|
+
local: () => ({
|
|
3274
|
+
cwd: () => process.cwd(),
|
|
3275
|
+
readFile: readFile2,
|
|
3276
|
+
writeFile: writeFile2,
|
|
3277
|
+
chmod,
|
|
3278
|
+
mkdir,
|
|
3279
|
+
exec: (cmd, args, env, opts) => {
|
|
3280
|
+
const child = spawn(cmd, args, { stdio: "inherit", env });
|
|
3281
|
+
if (opts?.forwardSignals) {
|
|
3282
|
+
for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
3283
|
+
process.on(sig, () => child.kill(sig));
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
return new Promise((done) => child.on("exit", (c, s) => done(c ?? (s ? 128 : 1))));
|
|
3287
|
+
},
|
|
3288
|
+
exit: (code) => process.exit(code)
|
|
3289
|
+
}),
|
|
3290
|
+
setExitCode: (code) => {
|
|
3291
|
+
process.exitCode = code;
|
|
3292
|
+
}
|
|
3293
|
+
};
|
|
3294
|
+
var argv = process.argv.slice(2);
|
|
3295
|
+
if (argv[0] === "--version" || argv[0] === "-v") {
|
|
3296
|
+
console.log(cliVersion());
|
|
3297
|
+
} else {
|
|
3298
|
+
runCli(argv, terminal).catch((err) => {
|
|
3299
|
+
for (const line of cliErrorLines(err)) console.error(line);
|
|
3300
|
+
process.exit(1);
|
|
3301
|
+
});
|
|
3302
|
+
}
|