wawesome 0.1.0 → 0.3.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/dist/index.mjs +101 -19
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -720,7 +720,7 @@ async function buildJs(entryInput, options) {
|
|
|
720
720
|
* that has to name this version — `--version`, the dependency a scaffolded
|
|
721
721
|
* project pins — reads it here, so a release bumps one file.
|
|
722
722
|
*/
|
|
723
|
-
const CLI_VERSION = "0.
|
|
723
|
+
const CLI_VERSION = "0.3.0";
|
|
724
724
|
//#endregion
|
|
725
725
|
//#region src/prompt.ts
|
|
726
726
|
/**
|
|
@@ -1326,18 +1326,85 @@ function manifestOf(assets) {
|
|
|
1326
1326
|
//#endregion
|
|
1327
1327
|
//#region ../shared/public-address.ts
|
|
1328
1328
|
const INVOCATION_PREFIX = "/x";
|
|
1329
|
+
const SLUG_SEPARATOR = "--";
|
|
1330
|
+
const SURFACE_ROUTE = "/v1/invocation-surface";
|
|
1329
1331
|
const SUBTREE_NOTE = "Every path beneath this address reaches the Function.";
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
+
/**
|
|
1333
|
+
* A gateway that serves no form of the address — and the value a client uses for
|
|
1334
|
+
* one that did not answer at all, deliberately the same: both name no address,
|
|
1335
|
+
* and there is nothing else either could honestly do.
|
|
1336
|
+
*/
|
|
1337
|
+
const NO_INVOCATION_SURFACE = {
|
|
1338
|
+
contentOrigin: null,
|
|
1339
|
+
pathFormOrigin: null
|
|
1340
|
+
};
|
|
1341
|
+
async function fetchInvocationSurface(gatewayUrl) {
|
|
1342
|
+
const origin = trimTrailingSlashes(gatewayUrl);
|
|
1343
|
+
const res = await fetch(`${origin}${SURFACE_ROUTE}`);
|
|
1344
|
+
if (!res.ok) throw new Error(`Failed to read the gateway's address forms (HTTP ${res.status}).`);
|
|
1345
|
+
const body = await res.json();
|
|
1346
|
+
return {
|
|
1347
|
+
contentOrigin: body.content_origin ?? null,
|
|
1348
|
+
pathFormOrigin: body.path_form ? origin : null
|
|
1349
|
+
};
|
|
1332
1350
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1351
|
+
/** The one address the Function answers at, `null` where it has none. */
|
|
1352
|
+
function publicAddress(surface, tenantSlug, appSlug, functionSlug) {
|
|
1353
|
+
const base = appBase(surface, tenantSlug, appSlug);
|
|
1354
|
+
if (base === null) return null;
|
|
1335
1355
|
if (functionSlug === "root") return base;
|
|
1336
1356
|
return `${base}/${encodeURIComponent(functionSlug)}`;
|
|
1337
1357
|
}
|
|
1338
1358
|
/** The same address with the app and function still to be chosen. */
|
|
1339
|
-
function publicAddressTemplate(
|
|
1340
|
-
|
|
1359
|
+
function publicAddressTemplate(surface, tenantSlug) {
|
|
1360
|
+
if (surface.contentOrigin) {
|
|
1361
|
+
if (!isLegalDnsLabel(tenantSlug)) return null;
|
|
1362
|
+
const hostname = appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}<app>`);
|
|
1363
|
+
return hostname === null ? null : `${hostname}/<function>`;
|
|
1364
|
+
}
|
|
1365
|
+
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/<app>/<function>`;
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
function appBase(surface, tenantSlug, appSlug) {
|
|
1369
|
+
if (surface.contentOrigin) {
|
|
1370
|
+
if (!isLegalDnsLabel(tenantSlug) || !isLegalDnsLabel(appSlug)) return null;
|
|
1371
|
+
return appHostname(surface.contentOrigin, `${tenantSlug}${SLUG_SEPARATOR}${appSlug}`);
|
|
1372
|
+
}
|
|
1373
|
+
if (surface.pathFormOrigin) return `${pathFormBase(surface.pathFormOrigin, tenantSlug)}/${encodeURIComponent(appSlug)}`;
|
|
1374
|
+
return null;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* The App's own hostname, built the way the gateway rebuilds it from its own
|
|
1378
|
+
* configuration: the label one level beneath the content domain. Callers check
|
|
1379
|
+
* the slugs the label is made of, since a slug the gateway would refuse to read
|
|
1380
|
+
* a `Host` for names no App there.
|
|
1381
|
+
*/
|
|
1382
|
+
function appHostname(contentOrigin, label) {
|
|
1383
|
+
let origin;
|
|
1384
|
+
try {
|
|
1385
|
+
origin = new URL(contentOrigin);
|
|
1386
|
+
} catch {
|
|
1387
|
+
return null;
|
|
1388
|
+
}
|
|
1389
|
+
return `${origin.protocol}//${label}.${origin.host}`;
|
|
1390
|
+
}
|
|
1391
|
+
/** The surface, or none where the gateway did not answer. */
|
|
1392
|
+
async function invocationSurfaceOrNone(gatewayUrl) {
|
|
1393
|
+
try {
|
|
1394
|
+
return await fetchInvocationSurface(gatewayUrl);
|
|
1395
|
+
} catch {
|
|
1396
|
+
return NO_INVOCATION_SURFACE;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function pathFormBase(origin, tenantSlug) {
|
|
1400
|
+
return `${trimTrailingSlashes(origin)}${INVOCATION_PREFIX}/${encodeURIComponent(tenantSlug)}`;
|
|
1401
|
+
}
|
|
1402
|
+
/** Must match `dns_label::is_legal` in `server/crates/core/src/value_objects.rs`. */
|
|
1403
|
+
function isLegalDnsLabel(candidate) {
|
|
1404
|
+
return candidate.length > 0 && candidate.length <= 63 && !candidate.startsWith("-") && !candidate.endsWith("-") && !candidate.includes("--") && /^[a-z0-9-]+$/.test(candidate);
|
|
1405
|
+
}
|
|
1406
|
+
function trimTrailingSlashes(origin) {
|
|
1407
|
+
return origin.replace(/\/+$/, "");
|
|
1341
1408
|
}
|
|
1342
1409
|
//#endregion
|
|
1343
1410
|
//#region src/deploy.ts
|
|
@@ -1451,9 +1518,11 @@ async function deploy(entryInput, options) {
|
|
|
1451
1518
|
process.exit(1);
|
|
1452
1519
|
}
|
|
1453
1520
|
let address = null;
|
|
1521
|
+
let surface = null;
|
|
1454
1522
|
try {
|
|
1455
1523
|
const { slug } = await resolveWorkspace(creds);
|
|
1456
|
-
|
|
1524
|
+
surface = await fetchInvocationSurface(creds.gateway_url);
|
|
1525
|
+
address = publicAddress(surface, slug, app, funcName);
|
|
1457
1526
|
} catch (err) {
|
|
1458
1527
|
if (isVerbose) console.log(`[wawesome:verbose] Could not resolve the workspace address: ${err instanceof Error ? err.message : err}`);
|
|
1459
1528
|
}
|
|
@@ -1473,6 +1542,10 @@ async function deploy(entryInput, options) {
|
|
|
1473
1542
|
if (address) {
|
|
1474
1543
|
console.log(`\n URL: \x1b[36m${address}\x1b[0m`);
|
|
1475
1544
|
console.log(` ${SUBTREE_NOTE}`);
|
|
1545
|
+
} else if (surface) {
|
|
1546
|
+
console.log("\n URL: none — this gateway serves no public address form.");
|
|
1547
|
+
console.log(" Set CONTENT_ORIGIN on it, or ALLOW_PATH_INVOCATION_FORM");
|
|
1548
|
+
console.log(" for local development.");
|
|
1476
1549
|
}
|
|
1477
1550
|
if (headroom) {
|
|
1478
1551
|
console.log("");
|
|
@@ -2413,9 +2486,11 @@ async function ensureSession(options) {
|
|
|
2413
2486
|
/** Bound on the rename loop, so a name the gateway keeps refusing ends the offer. */
|
|
2414
2487
|
const MAX_RENAME_ATTEMPTS = 3;
|
|
2415
2488
|
/** Show the address the Function will answer on, once it is deployed. */
|
|
2416
|
-
function announceUrl(
|
|
2489
|
+
function announceUrl(surface, slug, appSlug, functionName) {
|
|
2490
|
+
const address = publicAddress(surface, slug, appSlug, functionName);
|
|
2491
|
+
if (!address) return;
|
|
2417
2492
|
console.log("\n[wawesome] Your Function will answer on:\n");
|
|
2418
|
-
console.log(` \x1b[36m${
|
|
2493
|
+
console.log(` \x1b[36m${address}\x1b[0m`);
|
|
2419
2494
|
console.log(` ${SUBTREE_NOTE}\n`);
|
|
2420
2495
|
}
|
|
2421
2496
|
/**
|
|
@@ -2435,7 +2510,8 @@ function announceUrl(creds, slug, appSlug, functionName) {
|
|
|
2435
2510
|
async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionName) {
|
|
2436
2511
|
const current = tenant?.tenant_slug ?? creds.tenant_slug;
|
|
2437
2512
|
if (!current) return;
|
|
2438
|
-
|
|
2513
|
+
const surface = await invocationSurfaceOrNone(creds.gateway_url);
|
|
2514
|
+
announceUrl(surface, current, appSlug, functionName);
|
|
2439
2515
|
if (!tenant) {
|
|
2440
2516
|
console.log(` '${current}' is your workspace address. Whether it can still be changed`);
|
|
2441
2517
|
console.log(" is a question for the gateway, which did not answer.\n");
|
|
@@ -2459,7 +2535,7 @@ async function offerWorkspaceAddress(session, creds, tenant, appSlug, functionNa
|
|
|
2459
2535
|
try {
|
|
2460
2536
|
const renamed = await renameTenantSlug(creds, answer);
|
|
2461
2537
|
console.log(`\n[wawesome] ✅ Workspace renamed to '${renamed.tenant_slug}'.`);
|
|
2462
|
-
announceUrl(
|
|
2538
|
+
announceUrl(surface, renamed.tenant_slug, appSlug, functionName);
|
|
2463
2539
|
return;
|
|
2464
2540
|
} catch (err) {
|
|
2465
2541
|
console.log(`[wawesome] ${errorText(err)}`);
|
|
@@ -2739,6 +2815,8 @@ export default {
|
|
|
2739
2815
|
}
|
|
2740
2816
|
//#endregion
|
|
2741
2817
|
//#region src/version.ts
|
|
2818
|
+
/** How both surfaces name a version nobody can switch to. */
|
|
2819
|
+
const RECLAIMED = "⚠ [assets reclaimed]";
|
|
2742
2820
|
async function fetchVersions(gatewayUrl, tenantJwt, app, funcName, env, isVerbose) {
|
|
2743
2821
|
const url = `${gatewayUrl}/v1/apps/${encodeURIComponent(app)}/functions/${encodeURIComponent(funcName)}/versions?environment=${encodeURIComponent(env)}`;
|
|
2744
2822
|
if (isVerbose) console.log(`[wawesome:verbose] GET ${url}`);
|
|
@@ -2788,7 +2866,8 @@ async function listVersions(options) {
|
|
|
2788
2866
|
const hashStr = v.cwasm_hash.slice(0, 8).padEnd(8);
|
|
2789
2867
|
const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19).padEnd(20);
|
|
2790
2868
|
const statusStr = v.status.padEnd(9);
|
|
2791
|
-
|
|
2869
|
+
const badge = isActive ? "\x1B[32m★ [active]\x1B[0m" : v.rollbackable === false ? `\x1b[33m${RECLAIMED}\x1b[0m` : "";
|
|
2870
|
+
console.log(`${verStr} | ${hashStr} | ${dateStr} | ${statusStr} | ${badge}`);
|
|
2792
2871
|
}
|
|
2793
2872
|
console.log("");
|
|
2794
2873
|
}
|
|
@@ -2836,7 +2915,7 @@ async function switchVersion(targetInput, options) {
|
|
|
2836
2915
|
const isActive = v.version_number === data.active_version_number;
|
|
2837
2916
|
const hashShort = v.cwasm_hash.slice(0, 8);
|
|
2838
2917
|
const dateStr = new Date(v.created_at).toISOString().replace("T", " ").slice(0, 19);
|
|
2839
|
-
const badge = isActive ? " ★ [active]" : "";
|
|
2918
|
+
const badge = isActive ? " ★ [active]" : v.rollbackable === false ? ` ${RECLAIMED}` : "";
|
|
2840
2919
|
return {
|
|
2841
2920
|
name: `v${v.version_number} (${hashShort}) • ${dateStr}${badge}`,
|
|
2842
2921
|
value: v.version_number
|
|
@@ -2867,9 +2946,9 @@ async function switchVersion(targetInput, options) {
|
|
|
2867
2946
|
})
|
|
2868
2947
|
});
|
|
2869
2948
|
if (!deployRes.ok) {
|
|
2870
|
-
const
|
|
2871
|
-
console.error(`[wawesome] Error:
|
|
2872
|
-
if (isVerbose) console.error(`[wawesome:verbose] Response: ${
|
|
2949
|
+
const rejection = await asGatewayError(deployRes, `Failed to set active version (HTTP ${deployRes.status}).`);
|
|
2950
|
+
console.error(`[wawesome] Error: ${rejection.message}`);
|
|
2951
|
+
if (isVerbose) console.error(`[wawesome:verbose] Response: ${rejection.body}`);
|
|
2873
2952
|
process.exit(1);
|
|
2874
2953
|
}
|
|
2875
2954
|
console.log(`\n======================================================`);
|
|
@@ -3395,8 +3474,11 @@ async function showWorkspace(options) {
|
|
|
3395
3474
|
console.log(` Address: ${tenant.tenant_slug}`);
|
|
3396
3475
|
console.log(` Tenant: ${tenant.id}`);
|
|
3397
3476
|
if (options.verbose) {
|
|
3398
|
-
|
|
3399
|
-
|
|
3477
|
+
const template = publicAddressTemplate(await invocationSurfaceOrNone(creds.gateway_url), tenant.tenant_slug);
|
|
3478
|
+
if (template) {
|
|
3479
|
+
console.log(` URLs: ${template}`);
|
|
3480
|
+
console.log(` ${SUBTREE_NOTE}`);
|
|
3481
|
+
}
|
|
3400
3482
|
}
|
|
3401
3483
|
if (tenant.slug_locked) console.log("\n 🔒 The address is fixed — a Function version has been promoted and live URLs carry it.");
|
|
3402
3484
|
else {
|