vargai 0.4.0-alpha85 → 0.4.0-alpha87

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/hello.tsx ADDED
@@ -0,0 +1,35 @@
1
+ /** @jsxImportSource vargai */
2
+ import { Render, Clip, Image, Video, assets } from "vargai/react";
3
+ import { fal } from "vargai/ai";
4
+
5
+ const girl = Image({
6
+ prompt: {
7
+ text: `Using the attached reference images, generate a photorealistic three-quarter editorial portrait of the exact same character — maintain identical face, hairstyle, and proportions from Image 1.
8
+
9
+ Framing: Head and shoulders, cropped at upper chest. Direct eye contact with camera.
10
+
11
+ Natural confident expression, relaxed shoulders.
12
+ Preserve the outfit neckline and visible clothing details from reference.
13
+
14
+ Background: Deep black with two contrasting orange gradient accents matching Reference 2. Soft gradient bleed, no hard edges.
15
+
16
+ Shot on 85mm f/1.4 lens, shallow depth of field. Clean studio lighting — soft key light on face, subtle rim light on hair and shoulders for separation. High-end fashion editorial aesthetic.`,
17
+ images: [assets.characters.orangeGirl, assets.backgrounds.orangeGradient],
18
+ },
19
+ model: fal.imageModel("nano-banana-pro/edit"),
20
+ aspectRatio: "9:16",
21
+ });
22
+
23
+ export default (
24
+ <Render width={1080} height={1920}>
25
+ <Clip duration={5}>
26
+ <Video
27
+ prompt={{
28
+ text: "She waves hello warmly, natural smile, friendly expression. Studio lighting, authentic confident slightly playful atmosphere. Camera static. Intense orange lighting.",
29
+ images: [girl],
30
+ }}
31
+ model={fal.videoModel("kling-v2.5")}
32
+ />
33
+ </Clip>
34
+ </Render>
35
+ );
package/package.json CHANGED
@@ -71,7 +71,7 @@
71
71
  "zod": "^4.2.1"
72
72
  },
73
73
  "sideEffects": false,
74
- "version": "0.4.0-alpha85",
74
+ "version": "0.4.0-alpha87",
75
75
  "exports": {
76
76
  ".": "./src/index.ts",
77
77
  "./ai": "./src/ai-sdk/index.ts",
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Garry Tan Talking Head Video
3
+ * Generate a video of Garry Tan saying "varg.ai is cool!"
4
+ */
5
+
6
+ import {
7
+ generateImage,
8
+ experimental_generateSpeech as generateSpeech,
9
+ } from "ai";
10
+ import { elevenlabs, File, fal, generateVideo } from "../index";
11
+
12
+ async function main() {
13
+ const script = `varg.ai is cool!`;
14
+
15
+ console.log("generating Garry Tan image and voice in parallel...");
16
+ const [imageResult, speechResult] = await Promise.all([
17
+ generateImage({
18
+ model: fal.imageModel("flux-schnell"),
19
+ prompt:
20
+ "Garry Tan, Y Combinator CEO, Asian American man, short dark hair, glasses, friendly smile, professional headshot, studio lighting, clean background, looking at camera",
21
+ n: 1,
22
+ }),
23
+ generateSpeech({
24
+ model: elevenlabs.speechModel("turbo"),
25
+ text: script,
26
+ voice: "adam",
27
+ }),
28
+ ]);
29
+
30
+ const firstImage = imageResult.images[0];
31
+ if (!firstImage) throw new Error("No image generated");
32
+ const image = File.from(firstImage);
33
+ const audio = File.from(speechResult.audio);
34
+
35
+ console.log(`image: ${(await image.data()).byteLength} bytes`);
36
+ console.log(`audio: ${(await audio.data()).byteLength} bytes`);
37
+
38
+ await Bun.write("output/garry-tan-image.png", await image.data());
39
+ await Bun.write("output/garry-tan-voice.mp3", await audio.data());
40
+
41
+ console.log("\nanimating Garry Tan (5 seconds)...");
42
+ const { video } = await generateVideo({
43
+ model: fal.videoModel("wan-2.5"),
44
+ prompt: {
45
+ text: "man talking naturally, moving mouth while speaking, subtle head movements, professional demeanor, blinking naturally",
46
+ images: [await image.data()],
47
+ },
48
+ duration: 5,
49
+ });
50
+
51
+ const output = File.from(video);
52
+ console.log(`video: ${(await output.data()).byteLength} bytes`);
53
+ await Bun.write("output/garry-tan-varg.mp4", await output.data());
54
+
55
+ console.log("\ndone! files saved to output/");
56
+ console.log("- output/garry-tan-image.png");
57
+ console.log("- output/garry-tan-voice.mp3");
58
+ console.log("- output/garry-tan-varg.mp4");
59
+ }
60
+
61
+ main().catch(console.error);
@@ -43,7 +43,20 @@ class VargAPIError extends Error {
43
43
  }
44
44
 
45
45
  function resolveConfig(settings: VargProviderSettings = {}) {
46
- const apiKey = settings.apiKey ?? process.env.VARG_API_KEY ?? "";
46
+ let apiKey = settings.apiKey ?? process.env.VARG_API_KEY ?? "";
47
+
48
+ // Fallback to global credentials (~/.varg/credentials) if no key from settings or env
49
+ if (!apiKey) {
50
+ try {
51
+ const { getGlobalApiKey } = require("../../cli/credentials") as {
52
+ getGlobalApiKey: () => string | null;
53
+ };
54
+ apiKey = getGlobalApiKey() ?? "";
55
+ } catch {
56
+ // credentials module may not be available in all contexts (e.g., browser)
57
+ }
58
+ }
59
+
47
60
  const baseUrl = settings.baseUrl ?? "https://api.varg.ai/v1";
48
61
  return { apiKey, baseUrl };
49
62
  }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * vargai balance — check your credit balance
3
+ *
4
+ * Fetches the current balance from the Gateway API using the saved API key.
5
+ */
6
+
7
+ import { defineCommand } from "citty";
8
+ import { getCredentials, getGlobalApiKey } from "../credentials";
9
+
10
+ const GATEWAY_URL = process.env.VARG_GATEWAY_URL ?? "https://api.varg.ai";
11
+
12
+ const COLORS = {
13
+ reset: "\x1b[0m",
14
+ bold: "\x1b[1m",
15
+ dim: "\x1b[2m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ red: "\x1b[31m",
19
+ cyan: "\x1b[36m",
20
+ };
21
+
22
+ function formatCents(cents: number): string {
23
+ return `$${(cents / 100).toLocaleString("en-US", { minimumFractionDigits: 2 })}`;
24
+ }
25
+
26
+ export const balanceCmd = defineCommand({
27
+ meta: {
28
+ name: "balance",
29
+ description: "check your credit balance",
30
+ },
31
+ async run() {
32
+ const apiKey = process.env.VARG_API_KEY ?? getGlobalApiKey();
33
+ const creds = getCredentials();
34
+
35
+ if (!apiKey) {
36
+ console.log();
37
+ console.log(
38
+ `${COLORS.yellow} !${COLORS.reset} Not logged in. Run ${COLORS.cyan}vargai login${COLORS.reset} first.`,
39
+ );
40
+ console.log();
41
+ return;
42
+ }
43
+
44
+ process.stdout.write(
45
+ `\n${COLORS.dim} ● Fetching balance...${COLORS.reset}`,
46
+ );
47
+
48
+ try {
49
+ const res = await fetch(`${GATEWAY_URL}/v1/balance`, {
50
+ headers: {
51
+ Authorization: `Bearer ${apiKey}`,
52
+ },
53
+ });
54
+
55
+ if (!res.ok) {
56
+ process.stdout.write("\r\x1b[K");
57
+ const body = (await res.json().catch(() => ({}))) as {
58
+ error?: string | { message?: string };
59
+ };
60
+ const errMsg =
61
+ typeof body.error === "string"
62
+ ? body.error
63
+ : (body.error?.message ??
64
+ `Failed to fetch balance (${res.status})`);
65
+ console.log(`${COLORS.red} ✗${COLORS.reset} ${errMsg}`);
66
+ console.log();
67
+ return;
68
+ }
69
+
70
+ const data = (await res.json()) as { balance_cents: number };
71
+
72
+ process.stdout.write("\r\x1b[K");
73
+
74
+ console.log(
75
+ `${COLORS.bold}${COLORS.cyan}varg${COLORS.reset}${COLORS.dim} — account balance${COLORS.reset}`,
76
+ );
77
+ console.log();
78
+
79
+ if (creds?.email) {
80
+ console.log(` ${COLORS.dim}Account:${COLORS.reset} ${creds.email}`);
81
+ }
82
+
83
+ console.log(
84
+ ` ${COLORS.dim}Balance:${COLORS.reset} ${COLORS.bold}${data.balance_cents.toLocaleString()} credits${COLORS.reset} (${formatCents(data.balance_cents)})`,
85
+ );
86
+ console.log();
87
+
88
+ if (data.balance_cents <= 0) {
89
+ console.log(
90
+ ` ${COLORS.yellow}No credits remaining.${COLORS.reset} Run ${COLORS.cyan}vargai topup${COLORS.reset} to add more.`,
91
+ );
92
+ console.log();
93
+ }
94
+ } catch (error) {
95
+ process.stdout.write("\r\x1b[K");
96
+ console.log(
97
+ `${COLORS.red} ✗${COLORS.reset} Failed to connect to gateway. Check your connection.`,
98
+ );
99
+ console.log();
100
+ }
101
+ },
102
+ });
@@ -1,9 +1,12 @@
1
+ export { balanceCmd } from "./balance.ts";
1
2
  export { findCmd, showFindHelp } from "./find.tsx";
2
3
  export { frameCmd, showFrameHelp } from "./frame.tsx";
3
4
  export { helloCmd } from "./hello.ts";
4
5
  export { helpCmd, showHelp } from "./help.tsx";
5
6
  export { initCmd, showInitHelp } from "./init.tsx";
6
7
  export { listCmd, showListHelp } from "./list.tsx";
8
+ export { loginCmd } from "./login.tsx";
9
+ export { logoutCmd } from "./logout.ts";
7
10
  export {
8
11
  previewCmd,
9
12
  renderCmd,
@@ -13,4 +16,5 @@ export {
13
16
  export { runCmd, showRunHelp, showTargetHelp } from "./run.tsx";
14
17
  export { showStoryboardHelp, storyboardCmd } from "./storyboard.tsx";
15
18
  export { studioCmd } from "./studio.ts";
19
+ export { topupCmd } from "./topup.ts";
16
20
  export { showWhichHelp, whichCmd } from "./which.tsx";
@@ -0,0 +1,563 @@
1
+ /**
2
+ * vargai login — agent-first authentication
3
+ *
4
+ * Two modes:
5
+ * 1. Email OTP — sign in via email magic code (creates account + API key)
6
+ * 2. API key — paste an existing API key directly
7
+ *
8
+ * The `runLogin()` function is exported so `init` can embed the login flow.
9
+ */
10
+
11
+ import { defineCommand } from "citty";
12
+ import {
13
+ getCredentials,
14
+ getCredentialsPath,
15
+ saveCredentials,
16
+ } from "../credentials";
17
+
18
+ const APP_URL = process.env.VARG_APP_URL ?? "https://app.varg.ai";
19
+ const GATEWAY_URL = process.env.VARG_GATEWAY_URL ?? "https://api.varg.ai";
20
+
21
+ export const COLORS = {
22
+ reset: "\x1b[0m",
23
+ bold: "\x1b[1m",
24
+ dim: "\x1b[2m",
25
+ green: "\x1b[32m",
26
+ yellow: "\x1b[33m",
27
+ blue: "\x1b[34m",
28
+ red: "\x1b[31m",
29
+ cyan: "\x1b[36m",
30
+ gray: "\x1b[90m",
31
+ };
32
+
33
+ export const log = {
34
+ info: (msg: string) =>
35
+ console.log(`${COLORS.blue}info${COLORS.reset} ${msg}`),
36
+ success: (msg: string) =>
37
+ console.log(`${COLORS.green} ✓${COLORS.reset} ${msg}`),
38
+ error: (msg: string) => console.log(`${COLORS.red} ✗${COLORS.reset} ${msg}`),
39
+ warn: (msg: string) =>
40
+ console.log(`${COLORS.yellow} !${COLORS.reset} ${msg}`),
41
+ step: (msg: string) =>
42
+ console.log(
43
+ `\n${COLORS.bold}${COLORS.cyan}==>${COLORS.reset} ${COLORS.bold}${msg}${COLORS.reset}`,
44
+ ),
45
+ };
46
+
47
+ // Credit packages (mirrored from app/src/config/credit-packages.ts)
48
+ const CREDIT_PACKAGES = [
49
+ {
50
+ id: "credits-2000",
51
+ credits: 2000,
52
+ amountCents: 2000,
53
+ label: "2,000 credits",
54
+ },
55
+ {
56
+ id: "credits-5000",
57
+ credits: 5000,
58
+ amountCents: 5000,
59
+ label: "5,000 credits",
60
+ },
61
+ {
62
+ id: "credits-10000",
63
+ credits: 10000,
64
+ amountCents: 10000,
65
+ label: "10,000 credits",
66
+ popular: true,
67
+ },
68
+ {
69
+ id: "credits-20000",
70
+ credits: 20000,
71
+ amountCents: 20000,
72
+ label: "20,000 credits",
73
+ },
74
+ {
75
+ id: "credits-50000",
76
+ credits: 50000,
77
+ amountCents: 50000,
78
+ label: "50,000 credits",
79
+ },
80
+ {
81
+ id: "credits-100000",
82
+ credits: 100000,
83
+ amountCents: 100000,
84
+ label: "100,000 credits",
85
+ },
86
+ ];
87
+
88
+ function formatCents(cents: number): string {
89
+ return `$${(cents / 100).toLocaleString("en-US", { minimumFractionDigits: 0 })}`;
90
+ }
91
+
92
+ function maskApiKey(key: string): string {
93
+ if (key.length <= 16) return key;
94
+ return `${key.slice(0, 12)}...${key.slice(-4)}`;
95
+ }
96
+
97
+ export async function readLine(prompt: string): Promise<string> {
98
+ process.stdout.write(prompt);
99
+ return new Promise<string>((resolve) => {
100
+ process.stdin.setEncoding("utf8");
101
+ process.stdin.ref();
102
+ process.stdin.resume();
103
+ process.stdin.once("data", (data) => {
104
+ process.stdin.pause();
105
+ resolve(data.toString().trim());
106
+ });
107
+ });
108
+ }
109
+
110
+ export async function openBrowser(url: string): Promise<void> {
111
+ const platform = process.platform;
112
+ try {
113
+ if (platform === "darwin") {
114
+ Bun.spawn(["open", url]);
115
+ } else if (platform === "linux") {
116
+ Bun.spawn(["xdg-open", url]);
117
+ } else if (platform === "win32") {
118
+ Bun.spawn(["cmd", "/c", "start", url]);
119
+ } else {
120
+ log.warn(`Could not open browser. Visit this URL manually:\n ${url}`);
121
+ }
122
+ } catch {
123
+ log.warn(`Could not open browser. Visit this URL manually:\n ${url}`);
124
+ }
125
+ }
126
+
127
+ // ──── Login Result ────
128
+
129
+ export interface LoginResult {
130
+ apiKey: string;
131
+ email: string;
132
+ balanceCents: number;
133
+ /** Only available after email OTP login, not API key login */
134
+ accessToken: string;
135
+ }
136
+
137
+ // ──── API Key Login ────
138
+
139
+ async function loginWithApiKey(): Promise<LoginResult | null> {
140
+ console.log();
141
+ console.log(
142
+ `${COLORS.dim} Paste your API key from ${COLORS.reset}${COLORS.cyan}https://app.varg.ai${COLORS.reset}`,
143
+ );
144
+ console.log();
145
+
146
+ for (let attempt = 0; attempt < 3; attempt++) {
147
+ const key = await readLine(` API key: `);
148
+
149
+ if (!key) {
150
+ log.error("No key entered.");
151
+ if (attempt < 2) {
152
+ console.log(
153
+ `${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
154
+ );
155
+ continue;
156
+ }
157
+ return null;
158
+ }
159
+
160
+ // Validate by calling the gateway balance endpoint
161
+ process.stdout.write(
162
+ `${COLORS.dim} ● Validating API key...${COLORS.reset}`,
163
+ );
164
+
165
+ try {
166
+ const res = await fetch(`${GATEWAY_URL}/v1/balance`, {
167
+ headers: { Authorization: `Bearer ${key}` },
168
+ });
169
+
170
+ if (!res.ok) {
171
+ process.stdout.write("\r\x1b[K");
172
+ if (res.status === 401 || res.status === 403) {
173
+ log.error("Invalid API key.");
174
+ } else {
175
+ const body = (await res.json().catch(() => ({}))) as {
176
+ error?: string | { message?: string };
177
+ };
178
+ const errMsg =
179
+ typeof body.error === "string"
180
+ ? body.error
181
+ : (body.error?.message ?? `Validation failed (${res.status})`);
182
+ log.error(errMsg);
183
+ }
184
+
185
+ if (attempt < 2) {
186
+ console.log(
187
+ `${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
188
+ );
189
+ continue;
190
+ }
191
+ return null;
192
+ }
193
+
194
+ const data = (await res.json()) as { balance_cents: number };
195
+ process.stdout.write("\r\x1b[K");
196
+
197
+ return {
198
+ apiKey: key,
199
+ email: "", // unknown for direct API key login
200
+ balanceCents: data.balance_cents,
201
+ accessToken: "", // not available for API key login
202
+ };
203
+ } catch {
204
+ process.stdout.write("\r\x1b[K");
205
+ log.error("Failed to connect to gateway. Check your connection.");
206
+ if (attempt < 2) {
207
+ console.log(
208
+ `${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
209
+ );
210
+ continue;
211
+ }
212
+ return null;
213
+ }
214
+ }
215
+
216
+ return null;
217
+ }
218
+
219
+ // ──── Email OTP Login ────
220
+
221
+ async function loginWithEmail(): Promise<LoginResult | null> {
222
+ console.log();
223
+
224
+ const email = await readLine(` Enter your email: `);
225
+
226
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
227
+ log.error("Invalid email address.");
228
+ return null;
229
+ }
230
+
231
+ // Send OTP
232
+ console.log();
233
+ process.stdout.write(
234
+ `${COLORS.dim} ● Sending verification code...${COLORS.reset}`,
235
+ );
236
+
237
+ const sendRes = await fetch(`${APP_URL}/api/auth/cli/send-otp`, {
238
+ method: "POST",
239
+ headers: { "Content-Type": "application/json" },
240
+ body: JSON.stringify({ email }),
241
+ });
242
+
243
+ if (!sendRes.ok) {
244
+ const err = (await sendRes.json().catch(() => ({}))) as { error?: string };
245
+ process.stdout.write("\r\x1b[K");
246
+ log.error(err.error ?? "Failed to send verification code.");
247
+ return null;
248
+ }
249
+
250
+ process.stdout.write("\r\x1b[K");
251
+ log.success("Code sent! Check your inbox.");
252
+ console.log();
253
+
254
+ // Get OTP code (up to 3 attempts)
255
+ for (let attempt = 0; attempt < 3; attempt++) {
256
+ const code = await readLine(` Enter the 6-digit code: `);
257
+
258
+ if (!code || !/^\d{6}$/.test(code)) {
259
+ log.error("Code must be 6 digits.");
260
+ if (attempt < 2) {
261
+ console.log(
262
+ `${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
263
+ );
264
+ continue;
265
+ }
266
+ return null;
267
+ }
268
+
269
+ process.stdout.write(`${COLORS.dim} ● Verifying...${COLORS.reset}`);
270
+
271
+ const verifyRes = await fetch(`${APP_URL}/api/auth/cli/verify-otp`, {
272
+ method: "POST",
273
+ headers: { "Content-Type": "application/json" },
274
+ body: JSON.stringify({ email, code }),
275
+ });
276
+
277
+ if (!verifyRes.ok) {
278
+ const err = (await verifyRes.json().catch(() => ({}))) as {
279
+ error?: string;
280
+ };
281
+ process.stdout.write("\r\x1b[K");
282
+
283
+ if (verifyRes.status === 401 && attempt < 2) {
284
+ log.error(err.error ?? "Invalid code.");
285
+ console.log(
286
+ `${COLORS.dim} Try again (${2 - attempt} attempts left)${COLORS.reset}`,
287
+ );
288
+ continue;
289
+ }
290
+
291
+ log.error(err.error ?? "Verification failed.");
292
+ return null;
293
+ }
294
+
295
+ const result = (await verifyRes.json()) as {
296
+ api_key: string;
297
+ email: string;
298
+ balance_cents: number;
299
+ access_token: string;
300
+ };
301
+
302
+ process.stdout.write("\r\x1b[K");
303
+
304
+ return {
305
+ apiKey: result.api_key,
306
+ email: result.email,
307
+ balanceCents: result.balance_cents,
308
+ accessToken: result.access_token,
309
+ };
310
+ }
311
+
312
+ return null;
313
+ }
314
+
315
+ // ──── Credit Package Selector ────
316
+
317
+ export async function showCreditPackages(accessToken: string): Promise<void> {
318
+ // Need an access token for Stripe checkout — only available after email login
319
+ if (!accessToken) {
320
+ console.log();
321
+ console.log(
322
+ `${COLORS.dim} Add credits anytime with ${COLORS.cyan}vargai topup${COLORS.reset}${COLORS.dim} or at ${COLORS.cyan}https://app.varg.ai${COLORS.reset}`,
323
+ );
324
+ return;
325
+ }
326
+
327
+ console.log();
328
+ console.log(
329
+ `${COLORS.dim}───${COLORS.reset} ${COLORS.bold}Add credits${COLORS.reset} ${COLORS.dim}${"─".repeat(40)}${COLORS.reset}`,
330
+ );
331
+ console.log();
332
+
333
+ for (let i = 0; i < CREDIT_PACKAGES.length; i++) {
334
+ const pkg = CREDIT_PACKAGES[i]!;
335
+ const num = `[${i + 1}]`;
336
+ const popular = pkg.popular
337
+ ? ` ${COLORS.yellow}★ popular${COLORS.reset}`
338
+ : "";
339
+ const price = formatCents(pkg.amountCents).padStart(7);
340
+ console.log(
341
+ ` ${COLORS.cyan}${num}${COLORS.reset} ${pkg.label.padEnd(18)} ${COLORS.dim}-${COLORS.reset} ${COLORS.bold}${price}${COLORS.reset}${popular}`,
342
+ );
343
+ }
344
+
345
+ console.log();
346
+ console.log(` ${COLORS.dim}[s] Skip for now${COLORS.reset}`);
347
+ console.log();
348
+
349
+ const selection = await readLine(
350
+ ` Select a package (1-${CREDIT_PACKAGES.length}) or [s] to skip: `,
351
+ );
352
+
353
+ if (selection.toLowerCase() === "s" || selection === "") {
354
+ return;
355
+ }
356
+
357
+ const pkgIndex = parseInt(selection, 10) - 1;
358
+ if (isNaN(pkgIndex) || pkgIndex < 0 || pkgIndex >= CREDIT_PACKAGES.length) {
359
+ log.warn("Invalid selection. Skipping.");
360
+ return;
361
+ }
362
+
363
+ const selectedPkg = CREDIT_PACKAGES[pkgIndex]!;
364
+
365
+ // Create Stripe checkout session
366
+ process.stdout.write(
367
+ `\n${COLORS.dim} ● Creating checkout session...${COLORS.reset}`,
368
+ );
369
+
370
+ const checkoutRes = await fetch(`${APP_URL}/api/billing/checkout`, {
371
+ method: "POST",
372
+ headers: {
373
+ "Content-Type": "application/json",
374
+ Authorization: `Bearer ${accessToken}`,
375
+ Origin: APP_URL,
376
+ },
377
+ body: JSON.stringify({ packageId: selectedPkg.id }),
378
+ });
379
+
380
+ if (!checkoutRes.ok) {
381
+ process.stdout.write("\r\x1b[K");
382
+ const err = (await checkoutRes.json().catch(() => ({}))) as {
383
+ error?: string;
384
+ };
385
+ log.error(err.error ?? "Failed to create checkout session.");
386
+ console.log();
387
+ log.info(
388
+ `You can add credits anytime with ${COLORS.cyan}vargai topup${COLORS.reset} or at ${COLORS.cyan}https://app.varg.ai${COLORS.reset}`,
389
+ );
390
+ return;
391
+ }
392
+
393
+ const { url } = (await checkoutRes.json()) as { url: string };
394
+
395
+ process.stdout.write("\r\x1b[K");
396
+
397
+ log.success("Opening Stripe checkout in your browser...");
398
+ console.log();
399
+
400
+ await openBrowser(url);
401
+
402
+ console.log(
403
+ `${COLORS.dim} If the browser didn't open, visit:${COLORS.reset}`,
404
+ );
405
+ console.log(` ${COLORS.cyan}${url}${COLORS.reset}`);
406
+ console.log();
407
+ log.info("Credits will be added to your account after payment.");
408
+ }
409
+
410
+ // ──── Main Login Flow (exported for use by init) ────
411
+
412
+ export interface RunLoginOptions {
413
+ /** Show credit package selector after login. Default: true */
414
+ showPackages?: boolean;
415
+ /** Show the header banner. Default: true */
416
+ showHeader?: boolean;
417
+ /** Skip the "already logged in" check. Default: false */
418
+ forceLogin?: boolean;
419
+ }
420
+
421
+ /**
422
+ * Run the interactive login flow. Returns the login result, or null if
423
+ * the user cancelled / was already logged in and chose to keep credentials.
424
+ *
425
+ * Can be called from `vargai login` or embedded in `vargai init`.
426
+ */
427
+ export async function runLogin(
428
+ options: RunLoginOptions = {},
429
+ ): Promise<LoginResult | null> {
430
+ const {
431
+ showPackages = true,
432
+ showHeader = true,
433
+ forceLogin = false,
434
+ } = options;
435
+
436
+ if (showHeader) {
437
+ console.log();
438
+ console.log(
439
+ `${COLORS.bold}${COLORS.cyan}varg${COLORS.reset}${COLORS.dim} — ai video infrastructure${COLORS.reset}`,
440
+ );
441
+ console.log();
442
+ }
443
+
444
+ // Check if already logged in
445
+ if (!forceLogin) {
446
+ const existing = getCredentials();
447
+ if (existing) {
448
+ const emailLabel = existing.email
449
+ ? existing.email
450
+ : maskApiKey(existing.api_key);
451
+ console.log(
452
+ `${COLORS.dim}Already logged in as ${COLORS.reset}${COLORS.bold}${emailLabel}${COLORS.reset}`,
453
+ );
454
+ if (existing.email) {
455
+ console.log(
456
+ `${COLORS.dim}API key: ${maskApiKey(existing.api_key)}${COLORS.reset}`,
457
+ );
458
+ }
459
+ console.log();
460
+
461
+ const answer = await readLine(
462
+ `${COLORS.yellow}Log in as a different account?${COLORS.reset} (y/N): `,
463
+ );
464
+
465
+ if (answer.toLowerCase() !== "y") {
466
+ log.info("Keeping existing credentials.");
467
+ return null;
468
+ }
469
+ console.log();
470
+ }
471
+ }
472
+
473
+ // Mode selector
474
+ log.step("Sign in to varg.ai");
475
+ console.log();
476
+ console.log(
477
+ ` ${COLORS.cyan}[1]${COLORS.reset} Email ${COLORS.dim}— sign in with your email (creates account if needed)${COLORS.reset}`,
478
+ );
479
+ console.log(
480
+ ` ${COLORS.cyan}[2]${COLORS.reset} API key ${COLORS.dim}— paste an existing API key${COLORS.reset}`,
481
+ );
482
+ console.log();
483
+
484
+ const mode = await readLine(` Select login method (1-2): `);
485
+
486
+ let result: LoginResult | null = null;
487
+
488
+ if (mode === "2") {
489
+ result = await loginWithApiKey();
490
+ } else {
491
+ // Default to email login (mode "1" or anything else)
492
+ result = await loginWithEmail();
493
+ }
494
+
495
+ if (!result) {
496
+ log.error("Login failed.");
497
+ return null;
498
+ }
499
+
500
+ // Save credentials
501
+ saveCredentials({
502
+ api_key: result.apiKey,
503
+ email: result.email,
504
+ created_at: new Date().toISOString(),
505
+ });
506
+
507
+ console.log();
508
+ if (result.email) {
509
+ log.success(`Logged in as ${COLORS.bold}${result.email}${COLORS.reset}`);
510
+ } else {
511
+ log.success("API key validated and saved.");
512
+ }
513
+ log.success(
514
+ `API key saved to ${COLORS.dim}${getCredentialsPath()}${COLORS.reset}`,
515
+ );
516
+ log.success(
517
+ `Balance: ${COLORS.bold}${result.balanceCents.toLocaleString()} credits${COLORS.reset} (${formatCents(result.balanceCents)})`,
518
+ );
519
+
520
+ // Credit packages
521
+ if (showPackages) {
522
+ await showCreditPackages(result.accessToken);
523
+ }
524
+
525
+ return result;
526
+ }
527
+
528
+ // ──── Get Started Message ────
529
+
530
+ function printGetStarted(): void {
531
+ console.log();
532
+ console.log(`${COLORS.bold}Get started:${COLORS.reset}`);
533
+ console.log(
534
+ ` ${COLORS.cyan}vargai init${COLORS.reset} ${COLORS.dim}Set up a new project${COLORS.reset}`,
535
+ );
536
+ console.log(
537
+ ` ${COLORS.cyan}vargai render${COLORS.reset} ${COLORS.dim}Render a video${COLORS.reset}`,
538
+ );
539
+ console.log(
540
+ ` ${COLORS.cyan}vargai topup${COLORS.reset} ${COLORS.dim}Add credits to your account${COLORS.reset}`,
541
+ );
542
+ console.log();
543
+ }
544
+
545
+ // ──── Command Definition ────
546
+
547
+ export const loginCmd = defineCommand({
548
+ meta: {
549
+ name: "login",
550
+ description: "sign in to varg.ai and get your API key",
551
+ },
552
+ async run() {
553
+ try {
554
+ const result = await runLogin({ showPackages: true, showHeader: true });
555
+ if (result) {
556
+ printGetStarted();
557
+ }
558
+ } finally {
559
+ // Allow process to exit cleanly after interactive prompts
560
+ process.stdin.unref();
561
+ }
562
+ },
563
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * vargai logout — clear saved credentials
3
+ */
4
+
5
+ import { defineCommand } from "citty";
6
+ import {
7
+ clearCredentials,
8
+ getCredentials,
9
+ getCredentialsPath,
10
+ } from "../credentials";
11
+
12
+ const COLORS = {
13
+ reset: "\x1b[0m",
14
+ bold: "\x1b[1m",
15
+ dim: "\x1b[2m",
16
+ green: "\x1b[32m",
17
+ yellow: "\x1b[33m",
18
+ cyan: "\x1b[36m",
19
+ };
20
+
21
+ export const logoutCmd = defineCommand({
22
+ meta: {
23
+ name: "logout",
24
+ description: "sign out and remove saved API key",
25
+ },
26
+ async run() {
27
+ const creds = getCredentials();
28
+
29
+ if (!creds) {
30
+ console.log(
31
+ `\n${COLORS.dim}Not logged in. No credentials to remove.${COLORS.reset}\n`,
32
+ );
33
+ return;
34
+ }
35
+
36
+ const removed = clearCredentials();
37
+
38
+ if (removed) {
39
+ console.log();
40
+ console.log(
41
+ `${COLORS.green} ✓${COLORS.reset} Logged out. Credentials removed from ${COLORS.dim}${getCredentialsPath()}${COLORS.reset}`,
42
+ );
43
+ console.log(
44
+ `${COLORS.dim} Previously logged in as ${creds.email}${COLORS.reset}`,
45
+ );
46
+ console.log();
47
+ console.log(
48
+ `${COLORS.dim}To log in again: ${COLORS.reset}${COLORS.cyan}vargai login${COLORS.reset}`,
49
+ );
50
+ console.log();
51
+ } else {
52
+ console.log(
53
+ `\n${COLORS.yellow} !${COLORS.reset} No credentials file found.\n`,
54
+ );
55
+ }
56
+ },
57
+ });
@@ -18,7 +18,18 @@ async function detectDefaultModels(): Promise<DefaultModels | undefined> {
18
18
  const defaults: DefaultModels = {};
19
19
 
20
20
  // Gateway provider — single key for all models (recommended)
21
- if (process.env.VARG_API_KEY) {
21
+ // Check env var first, then global credentials (~/.varg/credentials)
22
+ let hasVargKey = !!process.env.VARG_API_KEY;
23
+ if (!hasVargKey) {
24
+ try {
25
+ const { getGlobalApiKey } = await import("../credentials");
26
+ hasVargKey = !!getGlobalApiKey();
27
+ } catch {
28
+ // credentials module may not be available
29
+ }
30
+ }
31
+
32
+ if (hasVargKey) {
22
33
  const { varg } = await import("../../ai-sdk/providers/varg");
23
34
  defaults.image = varg.imageModel("nano-banana-pro");
24
35
  defaults.video = varg.videoModel("kling-v3");
@@ -83,7 +94,7 @@ async function loadComponent(filePath: string): Promise<VargElement> {
83
94
 
84
95
  if (hasVargaiImport) {
85
96
  const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
86
- // Resolve JSX pragma to absolute path so it works from the cache dir
97
+ // Resolve @jsxImportSource to absolute path so it works from the cache dir
87
98
  const runtimeDir = resolve(pkgDir, "src/react/runtime");
88
99
  const resolvedSource = source.replace(
89
100
  /@jsxImportSource\s+vargai/,
@@ -0,0 +1,83 @@
1
+ /**
2
+ * vargai topup — add credits to your account
3
+ *
4
+ * Opens the app billing page in the browser where the user can purchase credits.
5
+ * If the user isn't logged in yet, directs them to `vargai login` first.
6
+ */
7
+
8
+ import { defineCommand } from "citty";
9
+ import { getCredentials } from "../credentials";
10
+
11
+ const APP_URL = process.env.VARG_APP_URL ?? "https://app.varg.ai";
12
+
13
+ const COLORS = {
14
+ reset: "\x1b[0m",
15
+ bold: "\x1b[1m",
16
+ dim: "\x1b[2m",
17
+ green: "\x1b[32m",
18
+ yellow: "\x1b[33m",
19
+ cyan: "\x1b[36m",
20
+ };
21
+
22
+ async function openBrowser(url: string): Promise<void> {
23
+ const platform = process.platform;
24
+ try {
25
+ if (platform === "darwin") {
26
+ Bun.spawn(["open", url]);
27
+ } else if (platform === "linux") {
28
+ Bun.spawn(["xdg-open", url]);
29
+ } else if (platform === "win32") {
30
+ Bun.spawn(["cmd", "/c", "start", url]);
31
+ }
32
+ } catch {
33
+ // silently fail — URL is printed below
34
+ }
35
+ }
36
+
37
+ export const topupCmd = defineCommand({
38
+ meta: {
39
+ name: "topup",
40
+ description: "add credits to your account",
41
+ },
42
+ async run() {
43
+ const creds = getCredentials();
44
+
45
+ if (!creds) {
46
+ console.log();
47
+ console.log(
48
+ `${COLORS.yellow} !${COLORS.reset} Not logged in. Run ${COLORS.cyan}vargai login${COLORS.reset} first.`,
49
+ );
50
+ console.log();
51
+ return;
52
+ }
53
+
54
+ console.log();
55
+ console.log(
56
+ `${COLORS.bold}${COLORS.cyan}varg${COLORS.reset}${COLORS.dim} — add credits${COLORS.reset}`,
57
+ );
58
+ console.log();
59
+ console.log(
60
+ `${COLORS.dim} Logged in as ${COLORS.reset}${COLORS.bold}${creds.email}${COLORS.reset}`,
61
+ );
62
+ console.log();
63
+
64
+ const billingUrl = `${APP_URL}/dashboard?tab=billing`;
65
+
66
+ console.log(
67
+ `${COLORS.green} ✓${COLORS.reset} Opening billing page in your browser...`,
68
+ );
69
+ console.log();
70
+
71
+ await openBrowser(billingUrl);
72
+
73
+ console.log(
74
+ `${COLORS.dim} If the browser didn't open, visit:${COLORS.reset}`,
75
+ );
76
+ console.log(` ${COLORS.cyan}${billingUrl}${COLORS.reset}`);
77
+ console.log();
78
+ console.log(
79
+ `${COLORS.dim} Log in with ${COLORS.reset}${creds.email}${COLORS.dim} to manage credits.${COLORS.reset}`,
80
+ );
81
+ console.log();
82
+ },
83
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Global credential management for vargai CLI.
3
+ *
4
+ * Stores and retrieves the user's API key from ~/.varg/credentials.
5
+ * File is created with 0600 permissions (owner read/write only).
6
+ */
7
+
8
+ import {
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { dirname, join } from "node:path";
17
+
18
+ export interface VargCredentials {
19
+ api_key: string;
20
+ email: string;
21
+ created_at: string;
22
+ }
23
+
24
+ const CREDENTIALS_DIR = join(homedir(), ".varg");
25
+ const CREDENTIALS_PATH = join(CREDENTIALS_DIR, "credentials");
26
+
27
+ // Module-level cache to avoid repeated file reads
28
+ let _cached: VargCredentials | null | undefined;
29
+
30
+ /**
31
+ * Get the full credentials object from ~/.varg/credentials.
32
+ * Returns null if the file doesn't exist or is malformed.
33
+ * Result is cached in memory after first read.
34
+ */
35
+ export function getCredentials(): VargCredentials | null {
36
+ if (_cached !== undefined) return _cached;
37
+
38
+ try {
39
+ if (!existsSync(CREDENTIALS_PATH)) {
40
+ _cached = null;
41
+ return null;
42
+ }
43
+
44
+ const raw = readFileSync(CREDENTIALS_PATH, "utf-8");
45
+ const parsed = JSON.parse(raw) as Partial<VargCredentials>;
46
+
47
+ if (!parsed.api_key || typeof parsed.api_key !== "string") {
48
+ _cached = null;
49
+ return null;
50
+ }
51
+
52
+ _cached = {
53
+ api_key: parsed.api_key,
54
+ email: parsed.email ?? "",
55
+ created_at: parsed.created_at ?? new Date().toISOString(),
56
+ };
57
+
58
+ return _cached;
59
+ } catch {
60
+ _cached = null;
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Get the global API key from ~/.varg/credentials.
67
+ * Returns null if not logged in.
68
+ */
69
+ export function getGlobalApiKey(): string | null {
70
+ const creds = getCredentials();
71
+ return creds?.api_key ?? null;
72
+ }
73
+
74
+ /**
75
+ * Save credentials to ~/.varg/credentials.
76
+ * Creates the ~/.varg/ directory if it doesn't exist.
77
+ * File is written with 0600 permissions (owner read/write only).
78
+ */
79
+ export function saveCredentials(creds: VargCredentials): void {
80
+ if (!existsSync(CREDENTIALS_DIR)) {
81
+ mkdirSync(CREDENTIALS_DIR, { recursive: true });
82
+ }
83
+
84
+ writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2) + "\n", {
85
+ mode: 0o600,
86
+ });
87
+
88
+ // Invalidate cache
89
+ _cached = creds;
90
+ }
91
+
92
+ /**
93
+ * Delete ~/.varg/credentials and clear cache.
94
+ * Returns true if the file was deleted, false if it didn't exist.
95
+ */
96
+ export function clearCredentials(): boolean {
97
+ _cached = null;
98
+
99
+ if (!existsSync(CREDENTIALS_PATH)) {
100
+ return false;
101
+ }
102
+
103
+ unlinkSync(CREDENTIALS_PATH);
104
+ return true;
105
+ }
106
+
107
+ /**
108
+ * Get the path to the credentials file (for display purposes).
109
+ */
110
+ export function getCredentialsPath(): string {
111
+ return CREDENTIALS_PATH;
112
+ }
package/src/cli/index.ts CHANGED
@@ -12,12 +12,15 @@ import { defineCommand, runMain } from "citty";
12
12
  import { registry } from "../core/registry";
13
13
  import { allDefinitions } from "../definitions";
14
14
  import {
15
+ balanceCmd,
15
16
  findCmd,
16
17
  frameCmd,
17
18
  helloCmd,
18
19
  helpCmd,
19
20
  initCmd,
20
21
  listCmd,
22
+ loginCmd,
23
+ logoutCmd,
21
24
  previewCmd,
22
25
  renderCmd,
23
26
  runCmd,
@@ -34,6 +37,7 @@ import {
34
37
  showWhichHelp,
35
38
  storyboardCmd,
36
39
  studioCmd,
40
+ topupCmd,
37
41
  whichCmd,
38
42
  } from "./commands";
39
43
 
@@ -116,6 +120,10 @@ const main = defineCommand({
116
120
  description: "ai video generation sdk",
117
121
  },
118
122
  subCommands: {
123
+ login: loginCmd,
124
+ logout: logoutCmd,
125
+ balance: balanceCmd,
126
+ topup: topupCmd,
119
127
  hello: helloCmd,
120
128
  init: initCmd,
121
129
  render: renderCmd,
@@ -0,0 +1,58 @@
1
+ /**
2
+ * OmniHuman v1.5 React syntax test
3
+ *
4
+ * Uses a local image + local audio file to generate a talking video.
5
+ *
6
+ * Run: bun run src/react/examples/omnihuman15-react-test.tsx
7
+ * Output: output/omnihuman15-react-test.mp4
8
+ */
9
+
10
+ import { fal } from "../../ai-sdk/providers/fal";
11
+ import { Clip, Render, render, Video } from "..";
12
+
13
+ const IMAGE_PATH = "output/garry-tan-image.png";
14
+ const AUDIO_PATH = "output/garry-tan-voice.mp3";
15
+
16
+ const video = (
17
+ <Render width={720} height={1280}>
18
+ <Clip duration={5}>
19
+ <Video
20
+ model={fal.videoModel("omnihuman-v1.5")}
21
+ prompt={{
22
+ text: "friendly professional talking head, natural blinking, subtle head movement",
23
+ images: [IMAGE_PATH],
24
+ audio: AUDIO_PATH,
25
+ }}
26
+ providerOptions={{
27
+ fal: {
28
+ resolution: "720p",
29
+ turbo_mode: true,
30
+ },
31
+ }}
32
+ />
33
+ </Clip>
34
+ </Render>
35
+ );
36
+
37
+ async function main() {
38
+ if (!process.env.FAL_API_KEY && !process.env.FAL_KEY) {
39
+ console.error("ERROR: FAL_API_KEY/FAL_KEY not found in environment");
40
+ process.exit(1);
41
+ }
42
+
43
+ const result = await render(video, {
44
+ output: "output/omnihuman15-react-test.mp4",
45
+ cache: ".cache/ai",
46
+ });
47
+
48
+ console.log(
49
+ `ok: output/omnihuman15-react-test.mp4 (${(result.video.byteLength / 1024 / 1024).toFixed(2)} MB)`,
50
+ );
51
+ }
52
+
53
+ if (import.meta.main) {
54
+ main().catch((err) => {
55
+ console.error(err);
56
+ process.exit(1);
57
+ });
58
+ }
@@ -1,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": ["Bash(bun run:*)", "WebFetch(domain:fal.ai)"],
4
- "deny": [],
5
- "ask": []
6
- }
7
- }
package/.env.example DELETED
@@ -1,33 +0,0 @@
1
- # fal.ai api key
2
- FAL_API_KEY=fal_xxx
3
-
4
- # higgsfield credentials
5
- HIGGSFIELD_API_KEY=hf_xxx
6
- HIGGSFIELD_SECRET=secret_xxx
7
-
8
- # elevenlabs api key
9
- ELEVENLABS_API_KEY=el_xxx
10
-
11
- # groq api key (ultra-fast whisper transcription)
12
- GROQ_API_KEY=gsk_xxx
13
-
14
- # fireworks api key (word-level transcription with timestamps)
15
- FIREWORKS_API_KEY=fw_xxx
16
-
17
- # cloudflare r2 / s3 storage
18
- CLOUDFLARE_R2_API_URL=https://xxx.r2.cloudflarestorage.com
19
- CLOUDFLARE_ACCESS_KEY_ID=xxx
20
- CLOUDFLARE_ACCESS_SECRET=xxx
21
- CLOUDFLARE_R2_BUCKET=m
22
-
23
- # replicate (optional)
24
- REPLICATE_API_TOKEN=r8_xxx
25
-
26
- # apify (web scraping actors)
27
- APIFY_TOKEN=apify_api_xxx
28
-
29
- # decart ai (real-time & batch video/image)
30
- DECART_API_KEY=decart_xxx
31
-
32
- # together ai (fast flux-schnell, no queue)
33
- TOGETHER_API_KEY=together_xxx
@@ -1,23 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- jobs:
10
- lint-and-format:
11
- runs-on: ubuntu-latest
12
- steps:
13
- - uses: actions/checkout@v4
14
-
15
- - uses: oven-sh/setup-bun@v2
16
- with:
17
- bun-version: latest
18
-
19
- - name: Install dependencies
20
- run: bun install
21
-
22
- - name: Check
23
- run: bun run check
package/.husky/README.md DELETED
@@ -1,102 +0,0 @@
1
- # Git Hooks Configuration
2
-
3
- This project uses [Husky](https://typicode.github.io/husky/) to manage Git hooks for maintaining code quality and security.
4
-
5
- ## Installed Hooks
6
-
7
- ### `pre-commit`
8
- Runs before each commit:
9
- - **Gitleaks** - Scans staged files for secrets and credentials
10
- - **Lint-staged** - Runs Biome linter/formatter on staged files
11
-
12
- ### `commit-msg`
13
- Validates commit messages:
14
- - **Commitlint** - Enforces [Conventional Commits](https://www.conventionalcommits.org/) format
15
-
16
- ### `pre-push`
17
- Runs before pushing to remote:
18
- - **TypeScript type checking** - Ensures no type errors before push
19
-
20
- ## Commit Message Format
21
-
22
- Follow the Conventional Commits specification:
23
-
24
- ```
25
- <type>(<scope>): <subject>
26
-
27
- <body>
28
-
29
- <footer>
30
- ```
31
-
32
- ### Types
33
- - `feat`: New feature
34
- - `fix`: Bug fix
35
- - `docs`: Documentation changes
36
- - `style`: Code style changes (formatting, etc)
37
- - `refactor`: Code refactoring
38
- - `perf`: Performance improvements
39
- - `test`: Test changes
40
- - `build`: Build system changes
41
- - `ci`: CI/CD changes
42
- - `chore`: Other changes
43
- - `revert`: Revert previous commit
44
-
45
- ### Examples
46
- ```bash
47
- feat: add video generation API
48
- fix(transcribe): handle empty audio files
49
- docs: update installation guide
50
- refactor: simplify audio processing pipeline
51
- ```
52
-
53
- ## Available Scripts
54
-
55
- ```bash
56
- # Run linter
57
- bun run lint
58
-
59
- # Format code
60
- bun run format
61
-
62
- # Type check
63
- bun run type-check
64
-
65
- # Check bundle size
66
- bun run size
67
- ```
68
-
69
- ## Bypassing Hooks
70
-
71
- ⚠️ **Not recommended** - Only use when absolutely necessary:
72
-
73
- ```bash
74
- # Skip all hooks
75
- git commit --no-verify -m "emergency fix"
76
-
77
- # Skip specific checks by setting env vars
78
- HUSKY=0 git commit -m "skip all hooks"
79
- ```
80
-
81
- ## Troubleshooting
82
-
83
- If hooks aren't running:
84
-
85
- ```bash
86
- # Reinstall hooks
87
- rm -rf .husky/_
88
- bun run prepare
89
- chmod +x .husky/pre-commit .husky/commit-msg .husky/pre-push
90
- ```
91
-
92
- ## Size Limits
93
-
94
- Bundle size limits are defined in `.size-limit.json`. Check size before publishing:
95
-
96
- ```bash
97
- bun run size
98
- ```
99
-
100
-
101
-
102
-
package/.husky/commit-msg DELETED
@@ -1,6 +0,0 @@
1
- # Check commit message format
2
- bunx --no -- commitlint --edit ${1}
3
-
4
-
5
-
6
-
package/.husky/pre-commit DELETED
@@ -1,9 +0,0 @@
1
- # Check for secrets with gitleaks
2
- gitleaks protect --staged
3
-
4
- # Run linters on staged files
5
- bunx lint-staged
6
-
7
-
8
-
9
-
package/.husky/pre-push DELETED
@@ -1,6 +0,0 @@
1
- # Run type checking before push
2
- bun run type-check
3
-
4
-
5
-
6
-