wawesome 0.0.1

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/index.mjs";
@@ -0,0 +1 @@
1
+ export {}
package/dist/index.mjs ADDED
@@ -0,0 +1,863 @@
1
+ import cac from "cac";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { build } from "esbuild";
5
+ import os from "node:os";
6
+ import http from "node:http";
7
+ import readline from "node:readline";
8
+ import { select } from "@inquirer/prompts";
9
+ //#region src/config.ts
10
+ /**
11
+ * Hardcoded Supabase project config for the wawesome.io platform.
12
+ * These are public values — the anon key is designed to be embedded in clients.
13
+ */
14
+ const SUPABASE_URL = "https://vclasavxxoufwymrutai.supabase.co";
15
+ /** Path to the user-level settings file (~/.wawesome/settings.json) */
16
+ function getSettingsPath() {
17
+ return path.join(os.homedir(), ".wawesome", "settings.json");
18
+ }
19
+ /**
20
+ * Read stored user settings from ~/.wawesome/settings.json.
21
+ */
22
+ function readSettings() {
23
+ const settingsPath = getSettingsPath();
24
+ if (!fs.existsSync(settingsPath)) return {};
25
+ try {
26
+ const raw = fs.readFileSync(settingsPath, "utf-8");
27
+ return JSON.parse(raw);
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+ /**
33
+ * Resolve gateway URL with resolution hierarchy:
34
+ * 1. Explicit override argument (CLI flag --gateway / --api)
35
+ * 2. Environment variables GATEWAY_URL or WAWESOME_GATEWAY_URL
36
+ * 3. User settings file ~/.wawesome/settings.json (gateway_url)
37
+ * 4. Stored login credentials (~/.wawesome/credentials.json)
38
+ * 5. Default fallback: "https://api.wawesome.io"
39
+ */
40
+ function getGatewayUrl(overrideUrl) {
41
+ if (overrideUrl) return overrideUrl;
42
+ if (process.env.GATEWAY_URL) return process.env.GATEWAY_URL;
43
+ if (process.env.WAWESOME_GATEWAY_URL) return process.env.WAWESOME_GATEWAY_URL;
44
+ const settings = readSettings();
45
+ if (settings.gateway_url && typeof settings.gateway_url === "string") return settings.gateway_url;
46
+ const creds = readCredentials();
47
+ if (creds?.gateway_url) return creds.gateway_url;
48
+ return "https://api.wawesome.io";
49
+ }
50
+ getGatewayUrl();
51
+ /** Localhost port used during OAuth callback */
52
+ const OAUTH_CALLBACK_PORT = 9999;
53
+ /** Path to the user-level credentials file */
54
+ function getCredentialsPath() {
55
+ return path.join(os.homedir(), ".wawesome", "credentials.json");
56
+ }
57
+ /**
58
+ * Read stored credentials. Returns null if not logged in.
59
+ */
60
+ function readCredentials() {
61
+ const credPath = getCredentialsPath();
62
+ if (!fs.existsSync(credPath)) return null;
63
+ try {
64
+ const raw = fs.readFileSync(credPath, "utf-8");
65
+ return JSON.parse(raw);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+ /**
71
+ * Write credentials to disk, creating the directory if needed.
72
+ */
73
+ function writeCredentials(creds) {
74
+ const credPath = getCredentialsPath();
75
+ const dir = path.dirname(credPath);
76
+ fs.mkdirSync(dir, { recursive: true });
77
+ fs.writeFileSync(credPath, JSON.stringify(creds, null, 2), "utf-8");
78
+ }
79
+ /**
80
+ * Delete stored credentials.
81
+ */
82
+ function deleteCredentials() {
83
+ const credPath = getCredentialsPath();
84
+ if (fs.existsSync(credPath)) {
85
+ fs.rmSync(credPath, { force: true });
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ /**
91
+ * Read wawesome-function.json from the project directory.
92
+ */
93
+ function readFunctionConfig(projectDir) {
94
+ const dir = projectDir || process.cwd();
95
+ const configPath = path.join(dir, "wawesome-function.json");
96
+ if (!fs.existsSync(configPath)) return null;
97
+ try {
98
+ const raw = fs.readFileSync(configPath, "utf-8");
99
+ return JSON.parse(raw);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ //#endregion
105
+ //#region src/build.ts
106
+ /**
107
+ * Bundles user TS/JS entry point into a single optimized ESM JavaScript file using esbuild.
108
+ */
109
+ async function buildJs(entryInput, options) {
110
+ const startTime = Date.now();
111
+ const config = readFunctionConfig();
112
+ const entry = entryInput || config?.entry || "src/index.ts";
113
+ const outPath = path.resolve(options.out);
114
+ const isVerbose = Boolean(options.verbose);
115
+ if (!fs.existsSync(entry)) {
116
+ console.error(`[wawesome] Error: Entry file '${entry}' not found.`);
117
+ process.exit(1);
118
+ }
119
+ console.log(`[wawesome] Building ${entry} -> ${options.out}...`);
120
+ if (isVerbose) {
121
+ console.log(`[wawesome:verbose] Entry point resolved: ${path.resolve(entry)}`);
122
+ console.log(`[wawesome:verbose] Target output path: ${outPath}`);
123
+ }
124
+ try {
125
+ await build({
126
+ entryPoints: [entry],
127
+ bundle: true,
128
+ format: "esm",
129
+ outfile: outPath,
130
+ platform: "neutral",
131
+ target: "es2022",
132
+ treeShaking: true,
133
+ minify: true,
134
+ keepNames: true,
135
+ legalComments: "none",
136
+ logLevel: isVerbose ? "info" : "silent"
137
+ });
138
+ const sizeKb = (fs.statSync(outPath).size / 1024).toFixed(2);
139
+ const elapsed = Date.now() - startTime;
140
+ console.log(`[wawesome] Successfully built ${options.out} (${sizeKb} KB) in ${elapsed}ms!`);
141
+ return outPath;
142
+ } catch (err) {
143
+ console.error("[wawesome] Build failed!");
144
+ if (isVerbose && err instanceof Error) console.error(err.stack || err.message);
145
+ else if (err instanceof Error) console.error(`[wawesome] ${err.message} (run with --verbose for full stack trace)`);
146
+ else console.error(err);
147
+ process.exit(1);
148
+ }
149
+ }
150
+ //#endregion
151
+ //#region src/auth.ts
152
+ /**
153
+ * OAuth login flow:
154
+ * 1. Build Supabase OAuth URL
155
+ * 2. Open browser
156
+ * 3. Listen on localhost for callback with access_token
157
+ * 4. Exchange Supabase JWT for tenant-scoped JWT via gateway
158
+ * 5. Store credentials in ~/.wawesome/credentials.json
159
+ */
160
+ async function login(options) {
161
+ const gatewayUrl = getGatewayUrl(options.gateway || options.api);
162
+ const provider = options.provider || "github";
163
+ const isVerbose = Boolean(options.verbose);
164
+ const authUrl = `${SUPABASE_URL}/auth/v1/authorize?provider=${provider}&redirect_to=${encodeURIComponent(`http://localhost:${OAUTH_CALLBACK_PORT}/callback`)}`;
165
+ console.log(`[wawesome] Starting ${provider} OAuth login...`);
166
+ console.log(`[wawesome] Gateway: ${gatewayUrl}`);
167
+ if (isVerbose) {
168
+ console.log(`[wawesome:verbose] Supabase URL: ${SUPABASE_URL}`);
169
+ console.log(`[wawesome:verbose] Auth URL: ${authUrl}`);
170
+ }
171
+ return new Promise((resolve, reject) => {
172
+ const server = http.createServer(async (req, res) => {
173
+ const reqUrl = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`);
174
+ if (reqUrl.pathname === "/callback") {
175
+ res.writeHead(200, { "Content-Type": "text/html" });
176
+ res.end(`
177
+ <!DOCTYPE html>
178
+ <html>
179
+ <head><title>wawesome - Authentication</title></head>
180
+ <body style="font-family: system-ui; text-align: center; padding: 50px; background: #0f172a; color: #f8fafc;">
181
+ <h2>šŸ” Authenticating with ${provider}...</h2>
182
+ <p>Transferring auth tokens back to CLI terminal...</p>
183
+ <script>
184
+ const hash = window.location.hash.substring(1);
185
+ const params = new URLSearchParams(hash);
186
+ const accessToken = params.get('access_token');
187
+ const refreshToken = params.get('refresh_token');
188
+
189
+ if (accessToken) {
190
+ fetch('/token?access_token=' + encodeURIComponent(accessToken) + '&refresh_token=' + encodeURIComponent(refreshToken || ''))
191
+ .then(() => {
192
+ document.body.innerHTML = '<h1>āœ… Success!</h1><p>You can close this tab and return to your terminal.</p>';
193
+ });
194
+ } else {
195
+ document.body.innerHTML = '<h1>āŒ Error</h1><p>No access token found in URL hash.</p>';
196
+ }
197
+ <\/script>
198
+ </body>
199
+ </html>
200
+ `);
201
+ return;
202
+ }
203
+ if (reqUrl.pathname === "/token") {
204
+ const accessToken = reqUrl.searchParams.get("access_token");
205
+ res.writeHead(200, { "Content-Type": "text/plain" });
206
+ res.end("Token received by CLI.");
207
+ if (!accessToken) {
208
+ console.error("[wawesome] Error: No access token received.");
209
+ server.close();
210
+ reject(/* @__PURE__ */ new Error("No access token received"));
211
+ return;
212
+ }
213
+ if (isVerbose) console.log("[wawesome:verbose] Supabase access token received.");
214
+ try {
215
+ console.log("[wawesome] Fetching your workspaces...");
216
+ const tenantsRes = await fetch(`${gatewayUrl}/api/v1/me/tenants`, { headers: { Authorization: `Bearer ${accessToken}` } });
217
+ if (!tenantsRes.ok) throw new Error(`Failed to fetch tenants (HTTP ${tenantsRes.status}). Have you completed onboarding?`);
218
+ const tenants = await tenantsRes.json();
219
+ let primaryTenantId;
220
+ if (tenants.length === 0) {
221
+ console.log("[wawesome] No workspaces found. Initializing default workspace...");
222
+ const initRes = await fetch(`${gatewayUrl}/api/v1/onboarding/init`, {
223
+ method: "POST",
224
+ headers: {
225
+ Authorization: `Bearer ${accessToken}`,
226
+ "Content-Type": "application/json"
227
+ },
228
+ body: JSON.stringify({ tenant_name: "Personal Workspace" })
229
+ });
230
+ if (!initRes.ok) throw new Error(`Failed to initialize workspace (HTTP ${initRes.status}).`);
231
+ primaryTenantId = (await initRes.json()).tenant_id;
232
+ } else primaryTenantId = tenants[0].tenant_id;
233
+ if (isVerbose) console.log(`[wawesome:verbose] Using tenant: ${primaryTenantId}`);
234
+ console.log("[wawesome] Exchanging for tenant-scoped credentials...");
235
+ const exchangeRes = await fetch(`${gatewayUrl}/api/v1/auth/token-exchange`, {
236
+ method: "POST",
237
+ headers: {
238
+ Authorization: `Bearer ${accessToken}`,
239
+ "Content-Type": "application/json"
240
+ },
241
+ body: JSON.stringify({ tenant_id: primaryTenantId })
242
+ });
243
+ if (!exchangeRes.ok) throw new Error(`Token exchange failed (HTTP ${exchangeRes.status}).`);
244
+ const exchangeData = await exchangeRes.json();
245
+ let userEmail = exchangeData.email || "unknown";
246
+ try {
247
+ const payloadPart = exchangeData.tenant_jwt.split(".")[1];
248
+ const decoded = JSON.parse(Buffer.from(payloadPart, "base64url").toString("utf-8"));
249
+ if (decoded.email) userEmail = decoded.email;
250
+ } catch {}
251
+ writeCredentials({
252
+ gateway_url: gatewayUrl,
253
+ tenant_jwt: exchangeData.tenant_jwt,
254
+ tenant_id: primaryTenantId,
255
+ user_email: userEmail
256
+ });
257
+ console.log("\n======================================================");
258
+ console.log("šŸŽ‰ \x1B[32mLOGIN SUCCESSFUL!\x1B[0m");
259
+ console.log("======================================================");
260
+ console.log(`\n Tenant: ${primaryTenantId}`);
261
+ console.log(` Gateway: ${gatewayUrl}`);
262
+ console.log(` Email: ${userEmail}`);
263
+ console.log("\n Credentials saved to ~/.wawesome/credentials.json");
264
+ console.log("======================================================\n");
265
+ server.close();
266
+ resolve();
267
+ } catch (err) {
268
+ console.error(`[wawesome] Login failed: ${err instanceof Error ? err.message : err}`);
269
+ server.close();
270
+ reject(err);
271
+ }
272
+ } else {
273
+ res.writeHead(404, { "Content-Type": "text/plain" });
274
+ res.end("Not Found");
275
+ }
276
+ });
277
+ server.listen(OAUTH_CALLBACK_PORT, async () => {
278
+ console.log(`\n🌐 Listening for OAuth callback on http://localhost:${OAUTH_CALLBACK_PORT}/callback ...`);
279
+ console.log(`šŸš€ Opening browser for ${provider} authentication...`);
280
+ console.log(`šŸ‘‰ ${authUrl}\n`);
281
+ try {
282
+ const open = (await import("open")).default;
283
+ await open(authUrl);
284
+ } catch {
285
+ console.log("[wawesome] Could not open browser automatically. Please visit the URL above manually.");
286
+ }
287
+ });
288
+ });
289
+ }
290
+ /**
291
+ * Clear stored credentials.
292
+ */
293
+ function logout() {
294
+ if (deleteCredentials()) console.log("[wawesome] āœ… Logged out. Credentials removed.");
295
+ else console.log("[wawesome] Not logged in (no credentials found).");
296
+ }
297
+ /**
298
+ * Show current login status.
299
+ */
300
+ function whoami() {
301
+ const creds = readCredentials();
302
+ if (!creds) {
303
+ console.log("[wawesome] Not logged in. Run 'wawesome login' to authenticate.");
304
+ return;
305
+ }
306
+ console.log("\n[wawesome] Current session:");
307
+ console.log(` Email: ${creds.user_email}`);
308
+ console.log(` Tenant: ${creds.tenant_id}`);
309
+ console.log(` Gateway: ${creds.gateway_url}\n`);
310
+ }
311
+ //#endregion
312
+ //#region src/deploy.ts
313
+ /**
314
+ * Deploy a function: build → upload JS to gateway → promote.
315
+ */
316
+ async function deploy(entryInput, options) {
317
+ const isVerbose = Boolean(options.verbose);
318
+ const creds = readCredentials();
319
+ if (!creds) {
320
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
321
+ process.exit(1);
322
+ }
323
+ const config = readFunctionConfig();
324
+ if (!config) {
325
+ console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
326
+ console.error("[wawesome] Run 'wawesome init' to create one, or create it manually.");
327
+ process.exit(1);
328
+ }
329
+ const { app, function: funcName } = config;
330
+ if (isVerbose) {
331
+ console.log(`[wawesome:verbose] Deploying to app=${app}, function=${funcName}`);
332
+ console.log(`[wawesome:verbose] Gateway: ${creds.gateway_url}`);
333
+ }
334
+ let bundlePath;
335
+ if (options.skipBuild) {
336
+ bundlePath = path.resolve(options.out);
337
+ if (!fs.existsSync(bundlePath)) {
338
+ console.error(`[wawesome] Error: Built bundle not found at '${bundlePath}'. Run 'wawesome build' first or remove --skip-build.`);
339
+ process.exit(1);
340
+ }
341
+ console.log(`[wawesome] Skipping build, using existing bundle: ${bundlePath}`);
342
+ } else {
343
+ const buildOpts = {
344
+ out: options.out,
345
+ verbose: options.verbose
346
+ };
347
+ bundlePath = await buildJs(entryInput || config.entry, buildOpts);
348
+ }
349
+ const jsCode = fs.readFileSync(bundlePath, "utf-8");
350
+ if (!jsCode.trim()) {
351
+ console.error("[wawesome] Error: Built bundle is empty.");
352
+ process.exit(1);
353
+ }
354
+ if (isVerbose) console.log(`[wawesome:verbose] Bundle size: ${(Buffer.byteLength(jsCode) / 1024).toFixed(2)} KB`);
355
+ console.log(`[wawesome] Uploading code for ${app}/${funcName}...`);
356
+ const uploadUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/code`;
357
+ if (isVerbose) console.log(`[wawesome:verbose] POST ${uploadUrl}`);
358
+ const uploadRes = await fetch(uploadUrl, {
359
+ method: "POST",
360
+ headers: {
361
+ Authorization: `Bearer ${creds.tenant_jwt}`,
362
+ "Content-Type": "application/javascript"
363
+ },
364
+ body: jsCode
365
+ });
366
+ if (!uploadRes.ok) {
367
+ const errorBody = await uploadRes.text();
368
+ if (uploadRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
369
+ else if (uploadRes.status === 409) {
370
+ let msg = "Version with this code bundle already exists.";
371
+ try {
372
+ const parsed = JSON.parse(errorBody);
373
+ if (parsed.error) msg = parsed.error;
374
+ } catch {}
375
+ console.error(`\n[wawesome] \x1b[31mError: ${msg}\x1b[0m`);
376
+ console.error("[wawesome] Code versions are immutable and cannot be overwritten.");
377
+ console.error("[wawesome] To switch active version, run: \x1B[36mwawesome version switch\x1B[0m\n");
378
+ } else {
379
+ console.error(`[wawesome] Error: Code upload failed (HTTP ${uploadRes.status}).`);
380
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
381
+ }
382
+ process.exit(1);
383
+ }
384
+ const version = (await uploadRes.json()).version_number;
385
+ console.log(`[wawesome] āœ… Code uploaded (version ${version ?? "unknown"}).`);
386
+ console.log(`[wawesome] Promoting to production...`);
387
+ const deployUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/deploy`;
388
+ if (isVerbose) console.log(`[wawesome:verbose] POST ${deployUrl}`);
389
+ const deployBody = {};
390
+ if (version !== void 0) deployBody.version_number = version;
391
+ const deployRes = await fetch(deployUrl, {
392
+ method: "POST",
393
+ headers: {
394
+ Authorization: `Bearer ${creds.tenant_jwt}`,
395
+ "Content-Type": "application/json"
396
+ },
397
+ body: JSON.stringify(deployBody)
398
+ });
399
+ if (!deployRes.ok) {
400
+ const errorBody = await deployRes.text();
401
+ if (deployRes.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
402
+ else {
403
+ console.error(`[wawesome] Error: Deployment failed (HTTP ${deployRes.status}).`);
404
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
405
+ }
406
+ process.exit(1);
407
+ }
408
+ console.log("\n======================================================");
409
+ console.log("šŸš€ \x1B[32mDEPLOYED SUCCESSFULLY!\x1B[0m");
410
+ console.log("======================================================");
411
+ console.log(`\n App: ${app}`);
412
+ console.log(` Function: ${funcName}`);
413
+ if (version !== void 0) console.log(` Version: ${version}`);
414
+ console.log("======================================================\n");
415
+ }
416
+ //#endregion
417
+ //#region src/init.ts
418
+ function prompt(question, defaultVal) {
419
+ const rl = readline.createInterface({
420
+ input: process.stdin,
421
+ output: process.stdout
422
+ });
423
+ return new Promise((resolve) => {
424
+ rl.question(`${question} (${defaultVal}): `, (answer) => {
425
+ rl.close();
426
+ resolve(answer.trim() || defaultVal);
427
+ });
428
+ });
429
+ }
430
+ /**
431
+ * Scaffold a new wawesome function project in the current directory.
432
+ */
433
+ async function init(options) {
434
+ const isVerbose = Boolean(options.verbose);
435
+ const cwd = process.cwd();
436
+ const dirName = path.basename(cwd);
437
+ console.log("[wawesome] Initializing a new function project...\n");
438
+ const functionName = await prompt(" Function name?", dirName);
439
+ const appSlug = await prompt(" App slug?", "default-app");
440
+ const entry = "src/index.ts";
441
+ if (isVerbose) console.log(`[wawesome:verbose] function=${functionName}, app=${appSlug}, entry=${entry}`);
442
+ const configContent = {
443
+ app: appSlug,
444
+ function: functionName,
445
+ entry
446
+ };
447
+ const configPath = path.join(cwd, "wawesome-function.json");
448
+ if (fs.existsSync(configPath)) console.log("[wawesome] wawesome-function.json already exists, skipping.");
449
+ else {
450
+ fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2) + "\n", "utf-8");
451
+ console.log("[wawesome] āœ… Created wawesome-function.json");
452
+ }
453
+ const srcDir = path.join(cwd, "src");
454
+ const indexPath = path.join(srcDir, "index.ts");
455
+ if (fs.existsSync(indexPath)) console.log("[wawesome] src/index.ts already exists, skipping.");
456
+ else {
457
+ fs.mkdirSync(srcDir, { recursive: true });
458
+ fs.writeFileSync(indexPath, `/**
459
+ * Serverless Function Handler
460
+ * Standard HTTP fetch request handler
461
+ */
462
+ export default {
463
+ async fetch(request: Request): Promise<Response> {
464
+ return Response.json({
465
+ message: "Hello, World!",
466
+ });
467
+ },
468
+ };
469
+ `, "utf-8");
470
+ console.log("[wawesome] āœ… Created src/index.ts");
471
+ }
472
+ const pkgPath = path.join(cwd, "package.json");
473
+ if (fs.existsSync(pkgPath)) console.log("[wawesome] package.json already exists, skipping.");
474
+ else {
475
+ const pkgContent = {
476
+ name: functionName,
477
+ type: "module",
478
+ version: "1.0.0",
479
+ description: `Serverless function: ${functionName}`,
480
+ scripts: {
481
+ build: "wawesome build",
482
+ deploy: "wawesome deploy",
483
+ typecheck: "tsc --noEmit"
484
+ },
485
+ devDependencies: {
486
+ "wawesome": "^1.0.0",
487
+ typescript: "^7.0.0"
488
+ }
489
+ };
490
+ fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2) + "\n", "utf-8");
491
+ console.log("[wawesome] āœ… Created package.json");
492
+ }
493
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
494
+ if (fs.existsSync(tsconfigPath)) console.log("[wawesome] tsconfig.json already exists, skipping.");
495
+ else {
496
+ fs.writeFileSync(tsconfigPath, JSON.stringify({
497
+ compilerOptions: {
498
+ target: "ES2022",
499
+ module: "ESNext",
500
+ moduleResolution: "bundler",
501
+ strict: true,
502
+ noEmit: true,
503
+ lib: [
504
+ "ES2022",
505
+ "DOM",
506
+ "DOM.Iterable"
507
+ ]
508
+ },
509
+ include: ["src"]
510
+ }, null, 2) + "\n", "utf-8");
511
+ console.log("[wawesome] āœ… Created tsconfig.json");
512
+ }
513
+ const gitignorePath = path.join(cwd, ".gitignore");
514
+ if (fs.existsSync(gitignorePath)) console.log("[wawesome] .gitignore already exists, skipping.");
515
+ else {
516
+ fs.writeFileSync(gitignorePath, "node_modules\ndist\n", "utf-8");
517
+ console.log("[wawesome] āœ… Created .gitignore");
518
+ }
519
+ console.log("\n[wawesome] šŸŽ‰ Project initialized! Next steps:");
520
+ console.log(" 1. npm install");
521
+ console.log(" 2. wawesome login");
522
+ console.log(" 3. wawesome deploy\n");
523
+ }
524
+ //#endregion
525
+ //#region src/version.ts
526
+ async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
527
+ const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
528
+ if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
529
+ const res = await fetch(url, {
530
+ method: "GET",
531
+ headers: { Authorization: `Bearer ${tenantJwt}` }
532
+ });
533
+ if (!res.ok) {
534
+ const errorBody = await res.text();
535
+ if (res.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
536
+ else {
537
+ console.error(`[wawesome] Error: Failed to list versions (HTTP ${res.status}).`);
538
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
539
+ }
540
+ process.exit(1);
541
+ }
542
+ return await res.json();
543
+ }
544
+ /**
545
+ * List existing function versions.
546
+ */
547
+ async function listVersions(options) {
548
+ const isVerbose = Boolean(options.verbose);
549
+ const env = options.env || "production";
550
+ const creds = readCredentials();
551
+ if (!creds) {
552
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
553
+ process.exit(1);
554
+ }
555
+ const config = readFunctionConfig();
556
+ if (!config) {
557
+ console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
558
+ process.exit(1);
559
+ }
560
+ const { app, function: funcName } = config;
561
+ const data = await fetchVersions(creds.gateway_url, creds.tenant_jwt, app, funcName, env, isVerbose);
562
+ if (!data.versions || data.versions.length === 0) {
563
+ console.log(`[wawesome] No versions found for '${app}/${funcName}'. Deploy code first using 'wawesome deploy'.`);
564
+ return;
565
+ }
566
+ console.log(`\nšŸ“¦ \x1b[1mVersions for '${app}/${funcName}' (environment: ${env})\x1b[0m\n`);
567
+ console.log("VERSION | HASH | CREATED AT | STATUS | ACTIVE");
568
+ console.log("--------|----------|----------------------|-----------|-------");
569
+ for (const v of data.versions) {
570
+ const isActive = v.version_number === data.active_version_number;
571
+ const verStr = `#${v.version_number}`.padEnd(7);
572
+ const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
573
+ const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
574
+ const statusStr = v.status.padEnd(9);
575
+ console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${isActive ? "\x1B[32mā˜… [active]\x1B[0m" : ""}`);
576
+ }
577
+ console.log("");
578
+ }
579
+ /**
580
+ * Switch active deployment version.
581
+ */
582
+ async function switchVersion(targetInput, options) {
583
+ const isVerbose = Boolean(options.verbose);
584
+ const env = options.env || "production";
585
+ const creds = readCredentials();
586
+ if (!creds) {
587
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
588
+ process.exit(1);
589
+ }
590
+ const config = readFunctionConfig();
591
+ if (!config) {
592
+ console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
593
+ process.exit(1);
594
+ }
595
+ const { app, function: funcName } = config;
596
+ const data = await fetchVersions(creds.gateway_url, creds.tenant_jwt, app, funcName, env, isVerbose);
597
+ if (!data.versions || data.versions.length === 0) {
598
+ console.error(`[wawesome] Error: No versions found for '${app}/${funcName}'. Deploy code first.`);
599
+ process.exit(1);
600
+ }
601
+ let selectedVersionNum;
602
+ if (targetInput !== void 0 && targetInput.trim() !== "") {
603
+ const cleaned = targetInput.trim().replace(/^v/, "");
604
+ const matched = data.versions.find((v) => v.version_number.toString() === cleaned || v.cwasm_hash.startsWith(cleaned) || v.cwasm_hash.slice(0, 8) === cleaned);
605
+ if (!matched) {
606
+ console.error(`\n[wawesome] \x1b[31mError: Version '${targetInput}' not found for '${app}/${funcName}'.\x1b[0m`);
607
+ console.error("\nAvailable versions:");
608
+ for (const v of data.versions) console.error(` - v${v.version_number} (${v.cwasm_hash.slice(0, 8)}) created at ${v.created_at}`);
609
+ console.error("");
610
+ process.exit(1);
611
+ }
612
+ selectedVersionNum = matched.version_number;
613
+ } else {
614
+ if (!process.stdout.isTTY) {
615
+ console.error("[wawesome] Error: Missing version argument in non-interactive environment.");
616
+ console.error("[wawesome] Usage: wawesome version switch <version_number_or_hash>");
617
+ process.exit(1);
618
+ }
619
+ const choices = data.versions.map((v) => {
620
+ const isActive = v.version_number === data.active_version_number;
621
+ const hashShort = v.cwasm_hash.slice(0, 8);
622
+ const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
623
+ const badge = isActive ? " ā˜… [active]" : "";
624
+ return {
625
+ name: `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`,
626
+ value: v.version_number
627
+ };
628
+ });
629
+ try {
630
+ selectedVersionNum = await select({
631
+ message: `Select active version for '${app}/${funcName}' (${env}):`,
632
+ choices,
633
+ pageSize: 10
634
+ });
635
+ } catch {
636
+ console.log("\n[wawesome] Version selection cancelled.");
637
+ process.exit(0);
638
+ }
639
+ }
640
+ console.log(`[wawesome] Promoting v${selectedVersionNum} to ${env}...`);
641
+ const deployUrl = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/deploy`;
642
+ const deployRes = await fetch(deployUrl, {
643
+ method: "POST",
644
+ headers: {
645
+ Authorization: `Bearer ${creds.tenant_jwt}`,
646
+ "Content-Type": "application/json"
647
+ },
648
+ body: JSON.stringify({
649
+ version_number: selectedVersionNum,
650
+ environment: env
651
+ })
652
+ });
653
+ if (!deployRes.ok) {
654
+ const errorBody = await deployRes.text();
655
+ console.error(`[wawesome] Error: Failed to set active version (HTTP ${deployRes.status}).`);
656
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
657
+ process.exit(1);
658
+ }
659
+ console.log(`\n======================================================`);
660
+ console.log(`šŸš€ \x1b[32mACTIVE VERSION UPDATED!\x1b[0m`);
661
+ console.log(`======================================================`);
662
+ console.log(` App: ${app}`);
663
+ console.log(` Function: ${funcName}`);
664
+ console.log(` Environment: ${env}`);
665
+ console.log(` Active Ver: v${selectedVersionNum}`);
666
+ console.log(`======================================================\n`);
667
+ }
668
+ //#endregion
669
+ //#region src/env.ts
670
+ const STANDARD_SECRET_MESSAGES = [
671
+ "Encrypted at rest using AES-256",
672
+ "This value can't be viewed again after you save it — only updated or deleted",
673
+ "Only decrypted at the moment your function runs — never returned by the CLI, dashboard, or API after creation."
674
+ ];
675
+ /**
676
+ * Load credentials and validate project config.
677
+ */
678
+ function loadClientContext() {
679
+ const creds = readCredentials();
680
+ if (!creds) {
681
+ console.error("[wawesome] Error: Not logged in. Run 'wawesome login' first.");
682
+ process.exit(1);
683
+ }
684
+ const config = readFunctionConfig();
685
+ if (!config || !config.app || !config.app.trim()) {
686
+ console.error("[wawesome] Error: No wawesome-function.json found in the current directory.");
687
+ console.error("[wawesome] Run 'wawesome init' to create one, or create it manually.");
688
+ process.exit(1);
689
+ }
690
+ return {
691
+ creds,
692
+ app: config.app.trim()
693
+ };
694
+ }
695
+ /**
696
+ * Set or overwrite an environment variable on the current app.
697
+ */
698
+ async function setEnvVar(key, value, options) {
699
+ const isVerbose = Boolean(options.verbose);
700
+ if (!key || value === void 0) {
701
+ console.error("[wawesome] Error: Missing key or value. Usage: wawesome env set <KEY> <VALUE> [--secret]");
702
+ process.exit(1);
703
+ }
704
+ const { creds, app } = loadClientContext();
705
+ const isSecret = Boolean(options.secret);
706
+ const url = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env`;
707
+ if (isVerbose) {
708
+ console.log(`[wawesome:verbose] POST ${url}`);
709
+ console.log(`[wawesome:verbose] Payload: key=${key}, is_secret=${isSecret}`);
710
+ }
711
+ const res = await fetch(url, {
712
+ method: "POST",
713
+ headers: {
714
+ Authorization: `Bearer ${creds.tenant_jwt}`,
715
+ "Content-Type": "application/json"
716
+ },
717
+ body: JSON.stringify({
718
+ key,
719
+ value,
720
+ is_secret: isSecret
721
+ })
722
+ });
723
+ if (!res.ok) {
724
+ const errorBody = await res.text();
725
+ if (res.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
726
+ else {
727
+ let msg = `HTTP ${res.status}`;
728
+ try {
729
+ const parsed = JSON.parse(errorBody);
730
+ if (parsed.error) msg = parsed.error;
731
+ } catch {}
732
+ console.error(`[wawesome] Error: Failed to set variable '${key}' (${msg}).`);
733
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
734
+ }
735
+ process.exit(1);
736
+ }
737
+ const data = await res.json();
738
+ if (data.variable.is_secret || isSecret) {
739
+ console.log(`\n[wawesome] āœ… Set secret variable '${key}' on app '${app}'.`);
740
+ const messages = data.notice?.messages?.length ? data.notice.messages : STANDARD_SECRET_MESSAGES;
741
+ console.log("\nšŸ”’ \x1B[1mSecret Notice:\x1B[0m");
742
+ for (const msg of messages) console.log(` • ${msg}`);
743
+ console.log("");
744
+ } else console.log(`[wawesome] āœ… Set variable '${key}' on app '${app}'.`);
745
+ }
746
+ function truncate(str, maxLen) {
747
+ if (str.length <= maxLen) return str;
748
+ if (maxLen <= 3) return str.slice(0, maxLen);
749
+ return str.slice(0, maxLen - 1) + "…";
750
+ }
751
+ /**
752
+ * List environment variables on the current app.
753
+ */
754
+ async function listEnvVars(options) {
755
+ const isVerbose = Boolean(options.verbose);
756
+ const { creds, app } = loadClientContext();
757
+ const url = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env`;
758
+ if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
759
+ const res = await fetch(url, {
760
+ method: "GET",
761
+ headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
762
+ });
763
+ if (!res.ok) {
764
+ const errorBody = await res.text();
765
+ if (res.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
766
+ else {
767
+ console.error(`[wawesome] Error: Failed to list variables (HTTP ${res.status}).`);
768
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
769
+ }
770
+ process.exit(1);
771
+ }
772
+ const data = await res.json();
773
+ if (!data.variables || data.variables.length === 0) {
774
+ console.log(`[wawesome] No environment variables set for app '${app}'.`);
775
+ return;
776
+ }
777
+ const availableWidth = (process.stdout.columns && process.stdout.columns > 40 ? process.stdout.columns : 80) - 12;
778
+ let rawMaxKeyLen = 15;
779
+ let rawMaxValLen = 15;
780
+ for (const item of data.variables) {
781
+ if (item.key.length > rawMaxKeyLen) rawMaxKeyLen = item.key.length;
782
+ if (item.value.length > rawMaxValLen) rawMaxValLen = item.value.length;
783
+ }
784
+ const maxKeyLen = Math.min(rawMaxKeyLen, Math.max(15, Math.floor(availableWidth * .4)));
785
+ const maxValLen = Math.min(rawMaxValLen, Math.max(15, availableWidth - maxKeyLen));
786
+ console.log(`\nšŸ“‹ \x1b[1mEnvironment variables for '${app}'\x1b[0m\n`);
787
+ const headerKey = "KEY".padEnd(maxKeyLen);
788
+ const headerVal = "VALUE".padEnd(maxValLen);
789
+ console.log(`${headerKey} | ${headerVal} | SECRET`);
790
+ console.log(`${"-".repeat(maxKeyLen)}-|- ${"-".repeat(maxValLen)} |-------`);
791
+ for (const item of data.variables) {
792
+ const truncatedKey = truncate(item.key, maxKeyLen);
793
+ const truncatedVal = truncate(item.value, maxValLen);
794
+ const keyStr = truncatedKey.padEnd(maxKeyLen);
795
+ const valStr = truncatedVal.padEnd(maxValLen);
796
+ const secretStr = item.is_secret ? "\x1B[33mtrue\x1B[0m" : "false";
797
+ console.log(`${keyStr} | ${valStr} | ${secretStr}`);
798
+ }
799
+ console.log("");
800
+ }
801
+ /**
802
+ * Delete an environment variable on the current app.
803
+ */
804
+ async function removeEnvVar(key, options) {
805
+ const isVerbose = Boolean(options.verbose);
806
+ if (!key) {
807
+ console.error("[wawesome] Error: Missing variable key. Usage: wawesome env rm <KEY>");
808
+ process.exit(1);
809
+ }
810
+ const { creds, app } = loadClientContext();
811
+ const url = `${creds.gateway_url}/v1/apps/${encodeURIComponent(app)}/env/${encodeURIComponent(key)}`;
812
+ if (isVerbose) console.log(`[wawesome:verbose] DELETE ${url}`);
813
+ const res = await fetch(url, {
814
+ method: "DELETE",
815
+ headers: { Authorization: `Bearer ${creds.tenant_jwt}` }
816
+ });
817
+ if (!res.ok) {
818
+ const errorBody = await res.text();
819
+ if (res.status === 401) console.error("[wawesome] Error: Authentication expired. Run 'wawesome login' again.");
820
+ else if (res.status === 404) console.error(`[wawesome] Error: Variable '${key}' not found on app '${app}'.`);
821
+ else {
822
+ console.error(`[wawesome] Error: Failed to delete variable '${key}' (HTTP ${res.status}).`);
823
+ if (isVerbose) console.error(`[wawesome:verbose] Response: ${errorBody}`);
824
+ }
825
+ process.exit(1);
826
+ }
827
+ console.log(`[wawesome] āœ… Deleted variable '${key}' from app '${app}'.`);
828
+ }
829
+ /**
830
+ * Dispatcher function for `wawesome env` CLI command.
831
+ */
832
+ async function envCommand(action, key, value, options) {
833
+ if (!action || action === "list" || action === "ls") return listEnvVars(options);
834
+ if (action === "set") return setEnvVar(key, value, options);
835
+ if (action === "rm" || action === "remove" || action === "delete" || action === "unset") return removeEnvVar(key, options);
836
+ console.error(`[wawesome] Error: Unknown env action '${action}'. Available actions: list, set, rm.`);
837
+ process.exit(1);
838
+ }
839
+ //#endregion
840
+ //#region src/index.ts
841
+ const cli = cac("wawesome");
842
+ cli.command("build [entry]", "Bundle a serverless function to an optimized JS file").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").action((entry, options) => buildJs(entry, options));
843
+ cli.command("deploy [entry]", "Build, upload, and promote a serverless function").option("-o, --out <path>", "Output JS bundle path", { default: "dist/index.js" }).option("-v, --verbose", "Enable verbose debug output").option("--skip-build", "Skip the build step, deploy an already-built bundle").action((entry, options) => deploy(entry, options));
844
+ cli.command("version [action] [target]", "Manage versions (e.g. 'version list', 'version switch [version]')").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((action, target, options) => {
845
+ if (!action || action === "list" || action === "ls") return listVersions(options);
846
+ if (action === "switch" || action === "use" || action === "select") return switchVersion(target, options);
847
+ return switchVersion(action, options);
848
+ });
849
+ cli.command("versions", "Alias for 'version list'").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((options) => listVersions(options));
850
+ cli.command("switch [target]", "Alias for 'version switch'").option("-e, --env <environment>", "Target environment (default: production)").option("-v, --verbose", "Enable verbose debug output").action((target, options) => switchVersion(target, options));
851
+ cli.command("env [action] [key] [value]", "Manage environment variables (set, list, rm)").usage("env <action> [key] [value]\n\nActions:\n set <key> <value> [--secret] Set or overwrite an environment variable\n list List environment variables for the current app\n rm <key> Delete an environment variable").example("wawesome env list").example("wawesome env set API_KEY my-secret-val --secret").example("wawesome env rm API_KEY").option("-s, --secret", "Flag variable as secret (write-only)").option("-v, --verbose", "Enable verbose debug output").action((action, key, value, options) => envCommand(action, key, value, options));
852
+ cli.command("env set <key> <value>", "Set or overwrite an environment variable on the current app").option("-s, --secret", "Flag variable as secret (write-only)").option("-v, --verbose", "Enable verbose debug output").action((key, value, options) => setEnvVar(key, value, options));
853
+ cli.command("env list", "List environment variables for the current app").alias("env ls").option("-v, --verbose", "Enable verbose debug output").action((options) => listEnvVars(options));
854
+ cli.command("env rm <key>", "Delete an environment variable from the current app").alias("env remove").alias("env delete").alias("env unset").option("-v, --verbose", "Enable verbose debug output").action((key, options) => removeEnvVar(key, options));
855
+ cli.command("login", "Authenticate with the wawesome.io platform").option("--api <url>", "API URL (default: https://api.wawesome.io)").option("--gateway <url>", "Alias for --api <url>").option("--provider <name>", "OAuth provider (default: github)").option("-v, --verbose", "Enable verbose debug output").action((options) => login(options));
856
+ cli.command("logout", "Clear stored authentication credentials").action(() => logout());
857
+ cli.command("whoami", "Show current login session info").action(() => whoami());
858
+ cli.command("init", "Scaffold a new function project in the current directory").option("-v, --verbose", "Enable verbose debug output").action((options) => init(options));
859
+ cli.help();
860
+ cli.version("1.0.0");
861
+ cli.parse();
862
+ //#endregion
863
+ export {};
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "wawesome",
3
+ "version": "0.0.1",
4
+ "description": "CLI tool for building and deploying serverless functions on wawesome.io platform",
5
+ "type": "module",
6
+ "bin": {
7
+ "wawesome": "./bin/wawesome.js"
8
+ },
9
+ "main": "./dist/index.mjs",
10
+ "types": "./dist/index.d.mts",
11
+ "files": [
12
+ "bin",
13
+ "dist"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsdown",
17
+ "dev": "tsdown --watch",
18
+ "test": "vitest run",
19
+ "check": "publint",
20
+ "changeset": "changeset"
21
+ },
22
+ "dependencies": {
23
+ "@inquirer/prompts": "^8.5.2",
24
+ "cac": "^6.7.14",
25
+ "esbuild": "^0.28.0",
26
+ "open": "^10.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "@changesets/cli": "^2.31.1",
30
+ "@types/node": "^26.1.1",
31
+ "publint": "^0.3.22",
32
+ "tsdown": "^0.22.5",
33
+ "typescript": "^7.0.2",
34
+ "vitest": "^4.1.10"
35
+ }
36
+ }