@freecodecamp/universe-cli 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -83
- package/dist/index.cjs +862 -119
- package/dist/index.js +198 -31
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -440,6 +440,13 @@ var ProxyError = class extends CliError {
|
|
|
440
440
|
this.exitCode = mapExitCode(status);
|
|
441
441
|
}
|
|
442
442
|
};
|
|
443
|
+
var AliasDriftError = class extends ProxyError {
|
|
444
|
+
current;
|
|
445
|
+
constructor(message, current) {
|
|
446
|
+
super(409, "alias_drift", message);
|
|
447
|
+
this.current = current;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
443
450
|
function mapExitCode(status) {
|
|
444
451
|
if (status === 401 || status === 403) return EXIT_CREDENTIALS;
|
|
445
452
|
if (status === 422 || status === 0 || status >= 500) return EXIT_STORAGE;
|
|
@@ -475,9 +482,11 @@ async function readErrorEnvelope(response) {
|
|
|
475
482
|
};
|
|
476
483
|
}
|
|
477
484
|
if (isProxyErrorEnvelope(raw) && raw.error) {
|
|
485
|
+
const current = typeof raw.current === "string" ? raw.current : void 0;
|
|
478
486
|
return {
|
|
479
487
|
code: raw.error.code ?? `http_${status}`,
|
|
480
|
-
message: raw.error.message ?? response.statusText ?? "request failed"
|
|
488
|
+
message: raw.error.message ?? response.statusText ?? "request failed",
|
|
489
|
+
...current === void 0 ? {} : { current }
|
|
481
490
|
};
|
|
482
491
|
}
|
|
483
492
|
return {
|
|
@@ -485,6 +494,12 @@ async function readErrorEnvelope(response) {
|
|
|
485
494
|
message: response.statusText || "request failed"
|
|
486
495
|
};
|
|
487
496
|
}
|
|
497
|
+
function throwProxyError(status, env) {
|
|
498
|
+
if (status === 409 && env.code === "alias_drift") {
|
|
499
|
+
throw new AliasDriftError(env.message, env.current ?? "");
|
|
500
|
+
}
|
|
501
|
+
throw new ProxyError(status, env.code, env.message);
|
|
502
|
+
}
|
|
488
503
|
function stripTrailingSlash(url) {
|
|
489
504
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
490
505
|
}
|
|
@@ -505,7 +520,7 @@ function createProxyClient(cfg) {
|
|
|
505
520
|
}
|
|
506
521
|
if (!response.ok) {
|
|
507
522
|
const env = await readErrorEnvelope(response);
|
|
508
|
-
|
|
523
|
+
throwProxyError(response.status, env);
|
|
509
524
|
}
|
|
510
525
|
if (response.status === 204) {
|
|
511
526
|
return void 0;
|
|
@@ -569,18 +584,55 @@ function createProxyClient(cfg) {
|
|
|
569
584
|
}
|
|
570
585
|
});
|
|
571
586
|
},
|
|
587
|
+
async getAlias(req) {
|
|
588
|
+
const url = `${base}/api/site/${encodeURIComponent(req.site)}/alias/${encodeURIComponent(req.mode)}`;
|
|
589
|
+
let response;
|
|
590
|
+
try {
|
|
591
|
+
response = await fetchImpl(url, {
|
|
592
|
+
method: "GET",
|
|
593
|
+
headers: {
|
|
594
|
+
Authorization: await userBearer(),
|
|
595
|
+
Accept: "application/json"
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
} catch (err) {
|
|
599
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
600
|
+
throw new ProxyError(
|
|
601
|
+
0,
|
|
602
|
+
"network_error",
|
|
603
|
+
`proxy unreachable: ${message}`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
if (response.status === 404) return null;
|
|
607
|
+
if (!response.ok) {
|
|
608
|
+
const env = await readErrorEnvelope(response);
|
|
609
|
+
throwProxyError(response.status, env);
|
|
610
|
+
}
|
|
611
|
+
return await response.json();
|
|
612
|
+
},
|
|
572
613
|
async sitePromote(req) {
|
|
573
614
|
const url = `${base}/api/site/${encodeURIComponent(req.site)}/promote`;
|
|
615
|
+
const headers = {
|
|
616
|
+
Authorization: await userBearer(),
|
|
617
|
+
Accept: "application/json"
|
|
618
|
+
};
|
|
619
|
+
const body = {};
|
|
620
|
+
if (req.deployId !== void 0) body.deployId = req.deployId;
|
|
621
|
+
if (req.expectedCurrent !== void 0)
|
|
622
|
+
body.expectedCurrent = req.expectedCurrent;
|
|
623
|
+
const hasBody = Object.keys(body).length > 0;
|
|
624
|
+
if (hasBody) headers["Content-Type"] = "application/json";
|
|
574
625
|
return call(url, {
|
|
575
626
|
method: "POST",
|
|
576
|
-
headers
|
|
577
|
-
|
|
578
|
-
Accept: "application/json"
|
|
579
|
-
}
|
|
627
|
+
headers,
|
|
628
|
+
...hasBody ? { body: JSON.stringify(body) } : {}
|
|
580
629
|
});
|
|
581
630
|
},
|
|
582
631
|
async siteRollback(req) {
|
|
583
632
|
const url = `${base}/api/site/${encodeURIComponent(req.site)}/rollback`;
|
|
633
|
+
const body = { to: req.to };
|
|
634
|
+
if (req.expectedCurrent !== void 0)
|
|
635
|
+
body.expectedCurrent = req.expectedCurrent;
|
|
584
636
|
return call(url, {
|
|
585
637
|
method: "POST",
|
|
586
638
|
headers: {
|
|
@@ -588,7 +640,7 @@ function createProxyClient(cfg) {
|
|
|
588
640
|
Accept: "application/json",
|
|
589
641
|
"Content-Type": "application/json"
|
|
590
642
|
},
|
|
591
|
-
body: JSON.stringify(
|
|
643
|
+
body: JSON.stringify(body)
|
|
592
644
|
});
|
|
593
645
|
},
|
|
594
646
|
async registerSite(req) {
|
|
@@ -920,7 +972,7 @@ async function deploy(options, deps = {}) {
|
|
|
920
972
|
);
|
|
921
973
|
}
|
|
922
974
|
const git = gitState();
|
|
923
|
-
if (git.dirty) {
|
|
975
|
+
if (git.dirty && !options.json) {
|
|
924
976
|
warn(
|
|
925
977
|
"git working tree is dirty \u2014 uncommitted changes will not be reflected."
|
|
926
978
|
);
|
|
@@ -932,7 +984,7 @@ async function deploy(options, deps = {}) {
|
|
|
932
984
|
cwd,
|
|
933
985
|
outputDir
|
|
934
986
|
});
|
|
935
|
-
if (buildResult.skipped) {
|
|
987
|
+
if (buildResult.skipped && !options.json) {
|
|
936
988
|
info("build.command not set \u2014 using pre-built output.");
|
|
937
989
|
}
|
|
938
990
|
const resolvedOutputDir = buildResult.outputDir;
|
|
@@ -991,7 +1043,7 @@ async function deploy(options, deps = {}) {
|
|
|
991
1043
|
);
|
|
992
1044
|
} else {
|
|
993
1045
|
const sizeKB = (uploadResult.totalSize / 1024).toFixed(1);
|
|
994
|
-
const nextLine = mode === "preview" ?
|
|
1046
|
+
const nextLine = mode === "preview" ? `Next: universe static promote --from ${finalizeResult.deployId}` : "Promoted to production.\nPreview alias unchanged.";
|
|
995
1047
|
success(
|
|
996
1048
|
[
|
|
997
1049
|
`Deployed ${finalizeResult.deployId}`,
|
|
@@ -1225,7 +1277,7 @@ var defaultReadPlatformYaml2 = async (cwd) => {
|
|
|
1225
1277
|
function emitJson4(envelope) {
|
|
1226
1278
|
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
1227
1279
|
}
|
|
1228
|
-
var DEPLOY_ID_RE = /^(\d{8})-(\d{6})-(
|
|
1280
|
+
var DEPLOY_ID_RE = /^(\d{8})-(\d{6})-(\S+)$/;
|
|
1229
1281
|
function parseDeployId(deployId) {
|
|
1230
1282
|
const m = DEPLOY_ID_RE.exec(deployId);
|
|
1231
1283
|
if (!m) return { deployId, timestamp: null, sha: null };
|
|
@@ -1294,7 +1346,10 @@ async function ls(options, deps = {}) {
|
|
|
1294
1346
|
getAuthToken: () => identity.token
|
|
1295
1347
|
});
|
|
1296
1348
|
const raw = await client.siteDeploys({ site });
|
|
1297
|
-
const
|
|
1349
|
+
const sorted = [...raw].sort(
|
|
1350
|
+
(a, b) => b.deployId.localeCompare(a.deployId)
|
|
1351
|
+
);
|
|
1352
|
+
const deploys = sorted.map((d) => parseDeployId(d.deployId));
|
|
1298
1353
|
if (options.json) {
|
|
1299
1354
|
emitJson4(
|
|
1300
1355
|
buildEnvelope("ls", true, {
|
|
@@ -1324,7 +1379,12 @@ async function ls(options, deps = {}) {
|
|
|
1324
1379
|
// src/commands/promote.ts
|
|
1325
1380
|
import { readFile as readFile4 } from "fs/promises";
|
|
1326
1381
|
import { resolve as resolve4 } from "path";
|
|
1327
|
-
import { log as log5 } from "@clack/prompts";
|
|
1382
|
+
import { confirm, isCancel, log as log5 } from "@clack/prompts";
|
|
1383
|
+
var defaultPromptConfirm = async (msg) => {
|
|
1384
|
+
const r = await confirm({ message: msg, initialValue: false });
|
|
1385
|
+
if (isCancel(r)) return false;
|
|
1386
|
+
return r === true;
|
|
1387
|
+
};
|
|
1328
1388
|
var defaultReadPlatformYaml3 = async (cwd) => {
|
|
1329
1389
|
return readFile4(resolve4(cwd, "platform.yaml"), "utf-8");
|
|
1330
1390
|
};
|
|
@@ -1356,6 +1416,7 @@ async function promote(options, deps = {}) {
|
|
|
1356
1416
|
const success = deps.logSuccess ?? ((s) => log5.success(s));
|
|
1357
1417
|
const error = deps.logError ?? ((s) => log5.error(s));
|
|
1358
1418
|
const exit = deps.exit ?? exitWithCode;
|
|
1419
|
+
const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm;
|
|
1359
1420
|
try {
|
|
1360
1421
|
const identity = await resolveId({ env });
|
|
1361
1422
|
if (!identity) {
|
|
@@ -1371,12 +1432,75 @@ async function promote(options, deps = {}) {
|
|
|
1371
1432
|
});
|
|
1372
1433
|
let result;
|
|
1373
1434
|
if (options.from) {
|
|
1374
|
-
|
|
1435
|
+
const prod = await client.getAlias({
|
|
1375
1436
|
site: config.site,
|
|
1376
|
-
|
|
1437
|
+
mode: "production"
|
|
1377
1438
|
});
|
|
1439
|
+
const initialExpected = prod?.deployId ?? "";
|
|
1440
|
+
try {
|
|
1441
|
+
result = await client.siteRollback({
|
|
1442
|
+
site: config.site,
|
|
1443
|
+
to: options.from,
|
|
1444
|
+
expectedCurrent: initialExpected
|
|
1445
|
+
});
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
if (!(err instanceof AliasDriftError)) throw err;
|
|
1448
|
+
if (options.json) throw err;
|
|
1449
|
+
error(
|
|
1450
|
+
`drift: production moved to ${err.current}, expected ${initialExpected}`
|
|
1451
|
+
);
|
|
1452
|
+
const ok = await promptConfirm(
|
|
1453
|
+
`Retry promote --from with expectedCurrent='${err.current}'?`
|
|
1454
|
+
);
|
|
1455
|
+
if (!ok) throw err;
|
|
1456
|
+
result = await client.siteRollback({
|
|
1457
|
+
site: config.site,
|
|
1458
|
+
to: options.from,
|
|
1459
|
+
expectedCurrent: err.current
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1378
1462
|
} else {
|
|
1379
|
-
|
|
1463
|
+
const preview = await client.getAlias({
|
|
1464
|
+
site: config.site,
|
|
1465
|
+
mode: "preview"
|
|
1466
|
+
});
|
|
1467
|
+
if (preview === null) {
|
|
1468
|
+
throw new ConfigError(
|
|
1469
|
+
"no preview alias to promote \u2014 run `universe static deploy` first"
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
const prod = await client.getAlias({
|
|
1473
|
+
site: config.site,
|
|
1474
|
+
mode: "production"
|
|
1475
|
+
});
|
|
1476
|
+
if (!options.json) {
|
|
1477
|
+
success(
|
|
1478
|
+
`Promoting ${preview.deployId} \u2192 ${prod?.deployId ?? "<none>"}`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
const initialExpected = prod?.deployId ?? "";
|
|
1482
|
+
try {
|
|
1483
|
+
result = await client.sitePromote({
|
|
1484
|
+
site: config.site,
|
|
1485
|
+
deployId: preview.deployId,
|
|
1486
|
+
expectedCurrent: initialExpected
|
|
1487
|
+
});
|
|
1488
|
+
} catch (err) {
|
|
1489
|
+
if (!(err instanceof AliasDriftError)) throw err;
|
|
1490
|
+
if (options.json) throw err;
|
|
1491
|
+
error(
|
|
1492
|
+
`drift: production moved to ${err.current}, expected ${initialExpected}`
|
|
1493
|
+
);
|
|
1494
|
+
const ok = await promptConfirm(
|
|
1495
|
+
`Retry promote with expectedCurrent='${err.current}'?`
|
|
1496
|
+
);
|
|
1497
|
+
if (!ok) throw err;
|
|
1498
|
+
result = await client.sitePromote({
|
|
1499
|
+
site: config.site,
|
|
1500
|
+
deployId: preview.deployId,
|
|
1501
|
+
expectedCurrent: err.current
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1380
1504
|
}
|
|
1381
1505
|
if (options.json) {
|
|
1382
1506
|
emitJson5(
|
|
@@ -1388,20 +1512,27 @@ async function promote(options, deps = {}) {
|
|
|
1388
1512
|
})
|
|
1389
1513
|
);
|
|
1390
1514
|
} else {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1515
|
+
const lines = [
|
|
1516
|
+
`Promoted ${result.deployId} to production`,
|
|
1517
|
+
``,
|
|
1518
|
+
` Site: ${config.site}`,
|
|
1519
|
+
` Deploy: ${result.deployId}`,
|
|
1520
|
+
` Production: ${result.url}`
|
|
1521
|
+
];
|
|
1522
|
+
if (options.from) {
|
|
1523
|
+
lines.push(``, "Preview alias unchanged.");
|
|
1524
|
+
}
|
|
1525
|
+
success(lines.join("\n"));
|
|
1400
1526
|
}
|
|
1401
1527
|
} catch (err) {
|
|
1402
1528
|
const { code, message } = wrapProxyError("promote", err);
|
|
1403
1529
|
if (options.json) {
|
|
1404
|
-
|
|
1530
|
+
const envelope = buildErrorEnvelope("promote", code, message);
|
|
1531
|
+
if (err instanceof AliasDriftError) {
|
|
1532
|
+
emitJson5({ ...envelope, current: err.current });
|
|
1533
|
+
} else {
|
|
1534
|
+
emitJson5(envelope);
|
|
1535
|
+
}
|
|
1405
1536
|
} else {
|
|
1406
1537
|
error(message);
|
|
1407
1538
|
}
|
|
@@ -1412,7 +1543,12 @@ async function promote(options, deps = {}) {
|
|
|
1412
1543
|
// src/commands/rollback.ts
|
|
1413
1544
|
import { readFile as readFile5 } from "fs/promises";
|
|
1414
1545
|
import { resolve as resolve5 } from "path";
|
|
1415
|
-
import { log as log6 } from "@clack/prompts";
|
|
1546
|
+
import { confirm as confirm2, isCancel as isCancel2, log as log6 } from "@clack/prompts";
|
|
1547
|
+
var defaultPromptConfirm2 = async (msg) => {
|
|
1548
|
+
const r = await confirm2({ message: msg, initialValue: false });
|
|
1549
|
+
if (isCancel2(r)) return false;
|
|
1550
|
+
return r === true;
|
|
1551
|
+
};
|
|
1416
1552
|
var defaultReadPlatformYaml4 = async (cwd) => {
|
|
1417
1553
|
return readFile5(resolve5(cwd, "platform.yaml"), "utf-8");
|
|
1418
1554
|
};
|
|
@@ -1444,6 +1580,7 @@ async function rollback(options, deps = {}) {
|
|
|
1444
1580
|
const success = deps.logSuccess ?? ((s) => log6.success(s));
|
|
1445
1581
|
const error = deps.logError ?? ((s) => log6.error(s));
|
|
1446
1582
|
const exit = deps.exit ?? exitWithCode;
|
|
1583
|
+
const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm2;
|
|
1447
1584
|
try {
|
|
1448
1585
|
if (!options.to || options.to.trim().length === 0) {
|
|
1449
1586
|
throw new UsageError(
|
|
@@ -1462,10 +1599,35 @@ async function rollback(options, deps = {}) {
|
|
|
1462
1599
|
baseUrl,
|
|
1463
1600
|
getAuthToken: () => identity.token
|
|
1464
1601
|
});
|
|
1465
|
-
const
|
|
1602
|
+
const to = options.to.trim();
|
|
1603
|
+
const prod = await client.getAlias({
|
|
1466
1604
|
site: config.site,
|
|
1467
|
-
|
|
1605
|
+
mode: "production"
|
|
1468
1606
|
});
|
|
1607
|
+
const initialExpected = prod?.deployId ?? "";
|
|
1608
|
+
let result;
|
|
1609
|
+
try {
|
|
1610
|
+
result = await client.siteRollback({
|
|
1611
|
+
site: config.site,
|
|
1612
|
+
to,
|
|
1613
|
+
expectedCurrent: initialExpected
|
|
1614
|
+
});
|
|
1615
|
+
} catch (err) {
|
|
1616
|
+
if (!(err instanceof AliasDriftError)) throw err;
|
|
1617
|
+
if (options.json) throw err;
|
|
1618
|
+
error(
|
|
1619
|
+
`drift: production moved to ${err.current}, expected ${initialExpected}`
|
|
1620
|
+
);
|
|
1621
|
+
const ok = await promptConfirm(
|
|
1622
|
+
`Retry rollback with expectedCurrent='${err.current}'?`
|
|
1623
|
+
);
|
|
1624
|
+
if (!ok) throw err;
|
|
1625
|
+
result = await client.siteRollback({
|
|
1626
|
+
site: config.site,
|
|
1627
|
+
to,
|
|
1628
|
+
expectedCurrent: err.current
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1469
1631
|
if (options.json) {
|
|
1470
1632
|
emitJson6(
|
|
1471
1633
|
buildEnvelope("rollback", true, {
|
|
@@ -1489,7 +1651,12 @@ async function rollback(options, deps = {}) {
|
|
|
1489
1651
|
} catch (err) {
|
|
1490
1652
|
const { code, message } = wrapProxyError("rollback", err);
|
|
1491
1653
|
if (options.json) {
|
|
1492
|
-
|
|
1654
|
+
const envelope = buildErrorEnvelope("rollback", code, message);
|
|
1655
|
+
if (err instanceof AliasDriftError) {
|
|
1656
|
+
emitJson6({ ...envelope, current: err.current });
|
|
1657
|
+
} else {
|
|
1658
|
+
emitJson6(envelope);
|
|
1659
|
+
}
|
|
1493
1660
|
} else {
|
|
1494
1661
|
error(message);
|
|
1495
1662
|
}
|
|
@@ -1849,7 +2016,7 @@ function outputError(ctx, code, message, issues) {
|
|
|
1849
2016
|
}
|
|
1850
2017
|
|
|
1851
2018
|
// src/cli.ts
|
|
1852
|
-
var version = true ? "0.
|
|
2019
|
+
var version = true ? "0.6.0" : "0.0.0";
|
|
1853
2020
|
function handleActionError(command, json, err) {
|
|
1854
2021
|
const ctx = { json, command };
|
|
1855
2022
|
const message = err instanceof Error ? err.message : "unknown error";
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@freecodecamp/universe-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "vitest run",
|
|
7
|
+
"test:smoke": "UNIVERSE_E2E_REAL=1 vitest run tests/e2e/smoke-real-artemis.test.ts",
|
|
7
8
|
"build": "tsup",
|
|
8
9
|
"lint": "oxlint src tests",
|
|
9
10
|
"typecheck": "tsc --noEmit",
|