@blinkk/root 3.0.3 → 3.0.4
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/{chunk-JTZWI4MH.js → chunk-URG2INLL.js} +1251 -65
- package/dist/chunk-URG2INLL.js.map +7 -0
- package/dist/{chunk-DU7JUEDY.js → chunk-X2C2MEJ2.js} +3 -1
- package/dist/chunk-X2C2MEJ2.js.map +7 -0
- package/dist/cli/secrets.d.ts +9 -0
- package/dist/cli.js +1 -1
- package/dist/functions.js +1 -1
- package/dist/jsx.js +1 -1
- package/dist/render.js +1 -1
- package/dist/secrets/env-file.d.ts +44 -0
- package/dist/secrets/gcloud.d.ts +28 -0
- package/dist/secrets/hash.d.ts +11 -0
- package/dist/secrets/manifest.d.ts +60 -0
- package/dist/secrets/secrets.d.ts +118 -0
- package/package.json +1 -1
- package/dist/chunk-DU7JUEDY.js.map +0 -7
- package/dist/chunk-JTZWI4MH.js.map +0 -7
|
@@ -1107,11 +1107,11 @@ async function bundleTsFile(srcPath, outPath) {
|
|
|
1107
1107
|
}
|
|
1108
1108
|
|
|
1109
1109
|
// src/cli/dev.ts
|
|
1110
|
-
import
|
|
1110
|
+
import path12 from "node:path";
|
|
1111
1111
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
1112
1112
|
import cookieParser from "cookie-parser";
|
|
1113
1113
|
import { default as express } from "express";
|
|
1114
|
-
import { dim as
|
|
1114
|
+
import { dim as dim5 } from "kleur/colors";
|
|
1115
1115
|
import sirv from "sirv";
|
|
1116
1116
|
import glob4 from "tiny-glob";
|
|
1117
1117
|
|
|
@@ -1391,12 +1391,1196 @@ function getSessionCookieSecret(rootConfig, rootDir) {
|
|
|
1391
1391
|
return randString(36);
|
|
1392
1392
|
}
|
|
1393
1393
|
|
|
1394
|
+
// src/cli/secrets.ts
|
|
1395
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1396
|
+
import fs8 from "node:fs";
|
|
1397
|
+
import path10 from "node:path";
|
|
1398
|
+
import * as readline from "node:readline";
|
|
1399
|
+
import { dim as dim3, green, red, yellow } from "kleur/colors";
|
|
1400
|
+
|
|
1401
|
+
// src/secrets/manifest.ts
|
|
1402
|
+
import fs5 from "node:fs";
|
|
1403
|
+
import path7 from "node:path";
|
|
1404
|
+
var MANIFEST_FILENAME = ".root.secrets.json";
|
|
1405
|
+
var GSM_KEY_RE = /^[A-Za-z0-9_-]{1,255}$/;
|
|
1406
|
+
var ENV_NAME_RE = /^[A-Za-z0-9_]+$/;
|
|
1407
|
+
function manifestPath(dir) {
|
|
1408
|
+
return path7.join(dir, MANIFEST_FILENAME);
|
|
1409
|
+
}
|
|
1410
|
+
function isValidEnvName(name) {
|
|
1411
|
+
return ENV_NAME_RE.test(name);
|
|
1412
|
+
}
|
|
1413
|
+
async function readManifest(filePath) {
|
|
1414
|
+
let raw;
|
|
1415
|
+
try {
|
|
1416
|
+
raw = await fs5.promises.readFile(filePath, "utf8");
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
if (err.code === "ENOENT") {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
throw err;
|
|
1422
|
+
}
|
|
1423
|
+
let parsed;
|
|
1424
|
+
try {
|
|
1425
|
+
parsed = JSON.parse(raw);
|
|
1426
|
+
} catch {
|
|
1427
|
+
throw new Error(`invalid JSON in ${filePath}`);
|
|
1428
|
+
}
|
|
1429
|
+
return validateManifest(parsed, filePath);
|
|
1430
|
+
}
|
|
1431
|
+
async function writeManifest(filePath, manifest) {
|
|
1432
|
+
const sortedSecrets = {};
|
|
1433
|
+
for (const name of Object.keys(manifest.secrets).sort()) {
|
|
1434
|
+
sortedSecrets[name] = manifest.secrets[name];
|
|
1435
|
+
}
|
|
1436
|
+
const ordered = {
|
|
1437
|
+
version: manifest.version,
|
|
1438
|
+
gcpProjectId: manifest.gcpProjectId,
|
|
1439
|
+
gsmKey: manifest.gsmKey,
|
|
1440
|
+
...manifest.import ? { import: manifest.import } : {},
|
|
1441
|
+
secrets: sortedSecrets
|
|
1442
|
+
};
|
|
1443
|
+
await fs5.promises.mkdir(path7.dirname(filePath), { recursive: true });
|
|
1444
|
+
await fs5.promises.writeFile(
|
|
1445
|
+
filePath,
|
|
1446
|
+
JSON.stringify(ordered, null, 2) + "\n",
|
|
1447
|
+
"utf8"
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
function emptyManifest(gcpProjectId, gsmKey) {
|
|
1451
|
+
return { version: 1, gcpProjectId, gsmKey, secrets: {} };
|
|
1452
|
+
}
|
|
1453
|
+
async function resolveManagedKeys(rootDir) {
|
|
1454
|
+
const sitePath = manifestPath(rootDir);
|
|
1455
|
+
const site = await readManifest(sitePath);
|
|
1456
|
+
if (!site) {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
const keys = [];
|
|
1460
|
+
const siteNames = /* @__PURE__ */ new Set();
|
|
1461
|
+
for (const [name, entry] of Object.entries(site.secrets)) {
|
|
1462
|
+
siteNames.add(name);
|
|
1463
|
+
keys.push({
|
|
1464
|
+
name,
|
|
1465
|
+
gsmKey: site.gsmKey,
|
|
1466
|
+
gcpProjectId: site.gcpProjectId,
|
|
1467
|
+
updatedAt: entry.updatedAt
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
let shared;
|
|
1471
|
+
if (site.import) {
|
|
1472
|
+
const sharedPath = path7.resolve(rootDir, site.import.manifest);
|
|
1473
|
+
const loaded = await readManifest(sharedPath);
|
|
1474
|
+
if (!loaded) {
|
|
1475
|
+
throw new Error(`imported manifest not found: ${sharedPath}`);
|
|
1476
|
+
}
|
|
1477
|
+
shared = loaded;
|
|
1478
|
+
if (shared.gcpProjectId !== site.gcpProjectId) {
|
|
1479
|
+
throw new Error(
|
|
1480
|
+
`imported manifest ${sharedPath} uses gcpProjectId "${shared.gcpProjectId}" but the site uses "${site.gcpProjectId}"`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
const requested = resolveImportedNames(site.import, shared, sharedPath);
|
|
1484
|
+
for (const name of requested) {
|
|
1485
|
+
if (siteNames.has(name)) {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
`"${name}" is declared in both the site manifest and the imported shared manifest; remove it from one`
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const entry = shared.secrets[name];
|
|
1491
|
+
keys.push({
|
|
1492
|
+
name,
|
|
1493
|
+
gsmKey: shared.gsmKey,
|
|
1494
|
+
gcpProjectId: shared.gcpProjectId,
|
|
1495
|
+
updatedAt: entry.updatedAt
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return { rootDir, site, shared, keys };
|
|
1500
|
+
}
|
|
1501
|
+
function resolveImportedNames(config, shared, sharedPath) {
|
|
1502
|
+
if (!config.keys || config.keys === "*") {
|
|
1503
|
+
return Object.keys(shared.secrets);
|
|
1504
|
+
}
|
|
1505
|
+
for (const name of config.keys) {
|
|
1506
|
+
if (!Object.prototype.hasOwnProperty.call(shared.secrets, name)) {
|
|
1507
|
+
throw new Error(`imported key "${name}" is not defined in ${sharedPath}`);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return config.keys;
|
|
1511
|
+
}
|
|
1512
|
+
function validateManifest(value, filePath) {
|
|
1513
|
+
if (typeof value !== "object" || value === null) {
|
|
1514
|
+
throw new Error(`invalid manifest ${filePath}: expected an object`);
|
|
1515
|
+
}
|
|
1516
|
+
const obj = value;
|
|
1517
|
+
if (typeof obj.gcpProjectId !== "string" || !obj.gcpProjectId) {
|
|
1518
|
+
throw new Error(`invalid manifest ${filePath}: missing "gcpProjectId"`);
|
|
1519
|
+
}
|
|
1520
|
+
if (typeof obj.gsmKey !== "string" || !GSM_KEY_RE.test(obj.gsmKey)) {
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
`invalid manifest ${filePath}: "gsmKey" must match ${GSM_KEY_RE}`
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const secretsValue = obj.secrets ?? {};
|
|
1526
|
+
if (typeof secretsValue !== "object" || secretsValue === null) {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
`invalid manifest ${filePath}: "secrets" must be an object`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const secrets = {};
|
|
1532
|
+
for (const [name, entry] of Object.entries(
|
|
1533
|
+
secretsValue
|
|
1534
|
+
)) {
|
|
1535
|
+
if (!ENV_NAME_RE.test(name)) {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`invalid manifest ${filePath}: secret name "${name}" must match ${ENV_NAME_RE}`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (typeof entry !== "object" || entry === null) {
|
|
1541
|
+
throw new Error(
|
|
1542
|
+
`invalid manifest ${filePath}: secret "${name}" must be an object`
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
const e = entry;
|
|
1546
|
+
if (typeof e.updatedAt !== "string" || !e.updatedAt) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
`invalid manifest ${filePath}: secret "${name}" missing "updatedAt"`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
secrets[name] = {
|
|
1552
|
+
updatedAt: e.updatedAt,
|
|
1553
|
+
...typeof e.updatedBy === "string" ? { updatedBy: e.updatedBy } : {}
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
let importConfig;
|
|
1557
|
+
if (obj.import !== void 0) {
|
|
1558
|
+
if (typeof obj.import !== "object" || obj.import === null) {
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`invalid manifest ${filePath}: "import" must be an object`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
const imp = obj.import;
|
|
1564
|
+
if (typeof imp.manifest !== "string" || !imp.manifest) {
|
|
1565
|
+
throw new Error(
|
|
1566
|
+
`invalid manifest ${filePath}: "import.manifest" must be a path`
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
let keys;
|
|
1570
|
+
if (imp.keys === "*" || imp.keys === void 0) {
|
|
1571
|
+
keys = imp.keys;
|
|
1572
|
+
} else if (Array.isArray(imp.keys)) {
|
|
1573
|
+
keys = imp.keys.map((k) => {
|
|
1574
|
+
if (typeof k !== "string" || !ENV_NAME_RE.test(k)) {
|
|
1575
|
+
throw new Error(
|
|
1576
|
+
`invalid manifest ${filePath}: "import.keys" entries must match ${ENV_NAME_RE}`
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
return k;
|
|
1580
|
+
});
|
|
1581
|
+
} else {
|
|
1582
|
+
throw new Error(
|
|
1583
|
+
`invalid manifest ${filePath}: "import.keys" must be an array or "*"`
|
|
1584
|
+
);
|
|
1585
|
+
}
|
|
1586
|
+
importConfig = { manifest: imp.manifest, ...keys ? { keys } : {} };
|
|
1587
|
+
}
|
|
1588
|
+
return {
|
|
1589
|
+
version: typeof obj.version === "number" ? obj.version : 1,
|
|
1590
|
+
gcpProjectId: obj.gcpProjectId,
|
|
1591
|
+
gsmKey: obj.gsmKey,
|
|
1592
|
+
...importConfig ? { import: importConfig } : {},
|
|
1593
|
+
secrets
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// src/secrets/secrets.ts
|
|
1598
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1599
|
+
import fs7 from "node:fs";
|
|
1600
|
+
import path9 from "node:path";
|
|
1601
|
+
|
|
1602
|
+
// src/secrets/env-file.ts
|
|
1603
|
+
import fs6 from "node:fs";
|
|
1604
|
+
import path8 from "node:path";
|
|
1605
|
+
import dotenv from "dotenv";
|
|
1606
|
+
async function readEnvFile(envPath2) {
|
|
1607
|
+
try {
|
|
1608
|
+
return await fs6.promises.readFile(envPath2, "utf8");
|
|
1609
|
+
} catch (err) {
|
|
1610
|
+
if (err.code === "ENOENT") {
|
|
1611
|
+
return "";
|
|
1612
|
+
}
|
|
1613
|
+
throw err;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
async function writeEnvFile(envPath2, content) {
|
|
1617
|
+
const dir = path8.dirname(envPath2);
|
|
1618
|
+
await fs6.promises.mkdir(dir, { recursive: true });
|
|
1619
|
+
const tmp = path8.join(dir, `.env.tmp-${process.pid}-${Date.now()}`);
|
|
1620
|
+
await fs6.promises.writeFile(tmp, content, "utf8");
|
|
1621
|
+
await fs6.promises.rename(tmp, envPath2);
|
|
1622
|
+
}
|
|
1623
|
+
function parseEnv(content) {
|
|
1624
|
+
return dotenv.parse(content);
|
|
1625
|
+
}
|
|
1626
|
+
function serializeEnvValue(value) {
|
|
1627
|
+
if (!value.includes("'") && !value.includes("\n") && !value.includes("\r")) {
|
|
1628
|
+
return `'${value}'`;
|
|
1629
|
+
}
|
|
1630
|
+
const escaped = value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/"/g, '\\"');
|
|
1631
|
+
return `"${escaped}"`;
|
|
1632
|
+
}
|
|
1633
|
+
function upsertEnvVars(content, updates, removals = []) {
|
|
1634
|
+
const eol = detectEol(content);
|
|
1635
|
+
const removeSet = new Set(removals);
|
|
1636
|
+
const written = /* @__PURE__ */ new Set();
|
|
1637
|
+
const lines = content.length ? content.split(/\r?\n/) : [];
|
|
1638
|
+
if (lines.length > 0 && lines[lines.length - 1] === "" && /\r?\n$/.test(content)) {
|
|
1639
|
+
lines.pop();
|
|
1640
|
+
}
|
|
1641
|
+
const out = [];
|
|
1642
|
+
for (const line of lines) {
|
|
1643
|
+
const key = parseAssignmentKey(line);
|
|
1644
|
+
if (key && removeSet.has(key)) {
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
if (key && Object.prototype.hasOwnProperty.call(updates, key)) {
|
|
1648
|
+
if (!written.has(key)) {
|
|
1649
|
+
out.push(`${key}=${serializeEnvValue(updates[key])}`);
|
|
1650
|
+
written.add(key);
|
|
1651
|
+
}
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
out.push(line);
|
|
1655
|
+
}
|
|
1656
|
+
for (const key of Object.keys(updates)) {
|
|
1657
|
+
if (!written.has(key)) {
|
|
1658
|
+
out.push(`${key}=${serializeEnvValue(updates[key])}`);
|
|
1659
|
+
written.add(key);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
while (out.length > 0 && out[out.length - 1].trim() === "") {
|
|
1663
|
+
out.pop();
|
|
1664
|
+
}
|
|
1665
|
+
if (out.length === 0) {
|
|
1666
|
+
return "";
|
|
1667
|
+
}
|
|
1668
|
+
return out.join(eol) + eol;
|
|
1669
|
+
}
|
|
1670
|
+
function parseAssignmentKey(line) {
|
|
1671
|
+
const match = line.match(/^\s*(?:export\s+)?([\w.-]+)\s*=/);
|
|
1672
|
+
return match ? match[1] : null;
|
|
1673
|
+
}
|
|
1674
|
+
function detectEol(content) {
|
|
1675
|
+
const crlf = (content.match(/\r\n/g) || []).length;
|
|
1676
|
+
const lf = (content.match(/\n/g) || []).length - crlf;
|
|
1677
|
+
return crlf > lf ? "\r\n" : "\n";
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// src/secrets/gcloud.ts
|
|
1681
|
+
import { spawn } from "node:child_process";
|
|
1682
|
+
var GcloudError = class extends Error {
|
|
1683
|
+
code;
|
|
1684
|
+
stderr;
|
|
1685
|
+
constructor(code, message, stderr = "") {
|
|
1686
|
+
super(message);
|
|
1687
|
+
this.name = "GcloudError";
|
|
1688
|
+
this.code = code;
|
|
1689
|
+
this.stderr = stderr;
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
function runGcloud(args, options = {}) {
|
|
1693
|
+
return new Promise((resolve, reject) => {
|
|
1694
|
+
const child = spawn("gcloud", args, {
|
|
1695
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1696
|
+
});
|
|
1697
|
+
let stdout = "";
|
|
1698
|
+
let stderr = "";
|
|
1699
|
+
child.stdout.on("data", (chunk) => {
|
|
1700
|
+
stdout += chunk.toString();
|
|
1701
|
+
});
|
|
1702
|
+
child.stderr.on("data", (chunk) => {
|
|
1703
|
+
stderr += chunk.toString();
|
|
1704
|
+
});
|
|
1705
|
+
child.on("error", (err) => {
|
|
1706
|
+
if (err.code === "ENOENT") {
|
|
1707
|
+
reject(
|
|
1708
|
+
new GcloudError(
|
|
1709
|
+
"ENOENT",
|
|
1710
|
+
"The Google Cloud CLI (gcloud) was not found. Install it: https://cloud.google.com/sdk/docs/install"
|
|
1711
|
+
)
|
|
1712
|
+
);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
reject(err);
|
|
1716
|
+
});
|
|
1717
|
+
child.on("close", (code) => {
|
|
1718
|
+
if (code === 0) {
|
|
1719
|
+
resolve(stdout);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
reject(classifyError(stderr));
|
|
1723
|
+
});
|
|
1724
|
+
if (options.input !== void 0) {
|
|
1725
|
+
child.stdin.end(options.input);
|
|
1726
|
+
} else {
|
|
1727
|
+
child.stdin.end();
|
|
1728
|
+
}
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
async function accessSecretJson(gsmKey, project) {
|
|
1732
|
+
let stdout;
|
|
1733
|
+
try {
|
|
1734
|
+
stdout = await runGcloud([
|
|
1735
|
+
"secrets",
|
|
1736
|
+
"versions",
|
|
1737
|
+
"access",
|
|
1738
|
+
"latest",
|
|
1739
|
+
"--secret",
|
|
1740
|
+
gsmKey,
|
|
1741
|
+
"--project",
|
|
1742
|
+
project
|
|
1743
|
+
]);
|
|
1744
|
+
} catch (err) {
|
|
1745
|
+
if (err instanceof GcloudError && err.code === "NOT_FOUND") {
|
|
1746
|
+
return {};
|
|
1747
|
+
}
|
|
1748
|
+
throw err;
|
|
1749
|
+
}
|
|
1750
|
+
const trimmed = stdout.trim();
|
|
1751
|
+
if (!trimmed) {
|
|
1752
|
+
return {};
|
|
1753
|
+
}
|
|
1754
|
+
let parsed;
|
|
1755
|
+
try {
|
|
1756
|
+
parsed = JSON.parse(trimmed);
|
|
1757
|
+
} catch {
|
|
1758
|
+
throw new Error(
|
|
1759
|
+
`secret "${gsmKey}" does not contain valid JSON (expected an object of env-name to value)`
|
|
1760
|
+
);
|
|
1761
|
+
}
|
|
1762
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1763
|
+
throw new Error(`secret "${gsmKey}" must be a JSON object`);
|
|
1764
|
+
}
|
|
1765
|
+
return parsed;
|
|
1766
|
+
}
|
|
1767
|
+
async function writeSecretJson(gsmKey, project, data) {
|
|
1768
|
+
await ensureSecret(gsmKey, project);
|
|
1769
|
+
await runGcloud(
|
|
1770
|
+
[
|
|
1771
|
+
"secrets",
|
|
1772
|
+
"versions",
|
|
1773
|
+
"add",
|
|
1774
|
+
gsmKey,
|
|
1775
|
+
"--project",
|
|
1776
|
+
project,
|
|
1777
|
+
"--data-file=-"
|
|
1778
|
+
],
|
|
1779
|
+
{ input: JSON.stringify(data, null, 2) }
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
async function ensureSecret(gsmKey, project) {
|
|
1783
|
+
try {
|
|
1784
|
+
await runGcloud([
|
|
1785
|
+
"secrets",
|
|
1786
|
+
"create",
|
|
1787
|
+
gsmKey,
|
|
1788
|
+
"--project",
|
|
1789
|
+
project,
|
|
1790
|
+
"--replication-policy",
|
|
1791
|
+
"automatic",
|
|
1792
|
+
"--labels",
|
|
1793
|
+
"managed-by=root"
|
|
1794
|
+
]);
|
|
1795
|
+
} catch (err) {
|
|
1796
|
+
if (err instanceof GcloudError && (err.code === "ALREADY_EXISTS" || /already exists/i.test(err.stderr))) {
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
throw err;
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
function classifyError(stderr) {
|
|
1803
|
+
const text = stderr.trim();
|
|
1804
|
+
if (/NOT_FOUND|was not found|does not exist/i.test(text)) {
|
|
1805
|
+
return new GcloudError("NOT_FOUND", text || "not found", stderr);
|
|
1806
|
+
}
|
|
1807
|
+
if (/ALREADY_EXISTS|already exists/i.test(text)) {
|
|
1808
|
+
return new GcloudError("ALREADY_EXISTS", text || "already exists", stderr);
|
|
1809
|
+
}
|
|
1810
|
+
if (/PERMISSION_DENIED|permission|forbidden|403/i.test(text)) {
|
|
1811
|
+
return new GcloudError(
|
|
1812
|
+
"PERMISSION_DENIED",
|
|
1813
|
+
"Permission denied by Google Cloud. Ensure your account has the right IAM roles (roles/secretmanager.admin to write, roles/secretmanager.secretAccessor to read).\n" + text,
|
|
1814
|
+
stderr
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
if (/UNAUTHENTICATED|not authenticated|reauth|login/i.test(text)) {
|
|
1818
|
+
return new GcloudError(
|
|
1819
|
+
"UNAUTHENTICATED",
|
|
1820
|
+
"Not authenticated with Google Cloud. Run `gcloud auth login`.\n" + text,
|
|
1821
|
+
stderr
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
return new GcloudError("UNKNOWN", text || "gcloud command failed", stderr);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/secrets/hash.ts
|
|
1828
|
+
import crypto2 from "node:crypto";
|
|
1829
|
+
function randomSalt() {
|
|
1830
|
+
return crypto2.randomBytes(16).toString("hex");
|
|
1831
|
+
}
|
|
1832
|
+
function hashValue(salt, value) {
|
|
1833
|
+
return crypto2.createHash("sha256").update(salt).update("\0").update(value, "utf8").digest("hex");
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// src/secrets/secrets.ts
|
|
1837
|
+
var STATE_FILENAME = "secrets-sync.json";
|
|
1838
|
+
function localStatePath(rootDir) {
|
|
1839
|
+
return path9.join(rootDir, ".root", STATE_FILENAME);
|
|
1840
|
+
}
|
|
1841
|
+
function envPath(rootDir) {
|
|
1842
|
+
return path9.join(rootDir, ".env");
|
|
1843
|
+
}
|
|
1844
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
1845
|
+
function syncSecrets(options) {
|
|
1846
|
+
const { rootDir, apply, force = false } = options;
|
|
1847
|
+
if (apply && !force) {
|
|
1848
|
+
const existing = inFlight.get(rootDir);
|
|
1849
|
+
if (existing) {
|
|
1850
|
+
return existing;
|
|
1851
|
+
}
|
|
1852
|
+
const promise = doSync(options).finally(() => inFlight.delete(rootDir));
|
|
1853
|
+
inFlight.set(rootDir, promise);
|
|
1854
|
+
return promise;
|
|
1855
|
+
}
|
|
1856
|
+
return doSync(options);
|
|
1857
|
+
}
|
|
1858
|
+
async function doSync(options) {
|
|
1859
|
+
const { rootDir, apply, force = false } = options;
|
|
1860
|
+
const result = {
|
|
1861
|
+
changed: [],
|
|
1862
|
+
kept: [],
|
|
1863
|
+
conflicts: [],
|
|
1864
|
+
overwritten: [],
|
|
1865
|
+
removed: [],
|
|
1866
|
+
errors: [],
|
|
1867
|
+
ranNetwork: false
|
|
1868
|
+
};
|
|
1869
|
+
const resolved = await resolveManagedKeys(rootDir);
|
|
1870
|
+
if (!resolved) {
|
|
1871
|
+
return result;
|
|
1872
|
+
}
|
|
1873
|
+
const envFilePath = envPath(rootDir);
|
|
1874
|
+
const content = await readEnvFile(envFilePath);
|
|
1875
|
+
const parsed = parseEnv(content);
|
|
1876
|
+
const state = await readLocalState(rootDir);
|
|
1877
|
+
const plans = resolved.keys.map((key) => {
|
|
1878
|
+
const synced = state.secrets[key.name];
|
|
1879
|
+
const envVal = parsed[key.name];
|
|
1880
|
+
const remoteChanged = force || !synced || key.updatedAt !== synced.updatedAt;
|
|
1881
|
+
const localChanged = synced ? hashValue(state.salt, envVal ?? "") !== synced.hash : envVal !== void 0;
|
|
1882
|
+
return { key, synced, envVal, remoteChanged, localChanged };
|
|
1883
|
+
});
|
|
1884
|
+
const needsTheirs = (plan) => force || !plan.synced || plan.remoteChanged && !plan.localChanged;
|
|
1885
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1886
|
+
const toFetch = /* @__PURE__ */ new Map();
|
|
1887
|
+
for (const plan of plans) {
|
|
1888
|
+
if (needsTheirs(plan)) {
|
|
1889
|
+
toFetch.set(plan.key.gsmKey, plan.key.gcpProjectId);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
for (const [gsmKey, project] of toFetch) {
|
|
1893
|
+
try {
|
|
1894
|
+
blobs.set(gsmKey, await accessSecretJson(gsmKey, project));
|
|
1895
|
+
result.ranNetwork = true;
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
result.errors.push({ gsmKey, message: err?.message || String(err) });
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
const updates = {};
|
|
1901
|
+
const nextState = { salt: state.salt, secrets: { ...state.secrets } };
|
|
1902
|
+
for (const plan of plans) {
|
|
1903
|
+
const name = plan.key.name;
|
|
1904
|
+
if (!needsTheirs(plan)) {
|
|
1905
|
+
if (plan.remoteChanged && plan.localChanged) {
|
|
1906
|
+
result.conflicts.push(name);
|
|
1907
|
+
} else if (!plan.remoteChanged && plan.localChanged) {
|
|
1908
|
+
result.kept.push(name);
|
|
1909
|
+
}
|
|
1910
|
+
continue;
|
|
1911
|
+
}
|
|
1912
|
+
if (!blobs.has(plan.key.gsmKey)) {
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
const theirs = blobs.get(plan.key.gsmKey)[name];
|
|
1916
|
+
if (theirs === void 0) {
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
if (!plan.synced && plan.envVal !== void 0 && plan.envVal !== theirs) {
|
|
1920
|
+
result.overwritten.push(name);
|
|
1921
|
+
}
|
|
1922
|
+
updates[name] = theirs;
|
|
1923
|
+
nextState.secrets[name] = {
|
|
1924
|
+
updatedAt: plan.key.updatedAt,
|
|
1925
|
+
hash: hashValue(state.salt, theirs)
|
|
1926
|
+
};
|
|
1927
|
+
result.changed.push(name);
|
|
1928
|
+
}
|
|
1929
|
+
const managed = new Set(resolved.keys.map((key) => key.name));
|
|
1930
|
+
const removals = [];
|
|
1931
|
+
for (const name of Object.keys(state.secrets)) {
|
|
1932
|
+
if (!managed.has(name)) {
|
|
1933
|
+
removals.push(name);
|
|
1934
|
+
delete nextState.secrets[name];
|
|
1935
|
+
result.removed.push(name);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
if (apply) {
|
|
1939
|
+
if (Object.keys(updates).length > 0 || removals.length > 0) {
|
|
1940
|
+
const next = upsertEnvVars(content, updates, removals);
|
|
1941
|
+
if (next !== content) {
|
|
1942
|
+
await writeEnvFile(envFilePath, next);
|
|
1943
|
+
}
|
|
1944
|
+
for (const [name, value] of Object.entries(updates)) {
|
|
1945
|
+
process.env[name] = value;
|
|
1946
|
+
}
|
|
1947
|
+
for (const name of removals) {
|
|
1948
|
+
delete process.env[name];
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
await writeLocalState(rootDir, nextState);
|
|
1952
|
+
}
|
|
1953
|
+
return result;
|
|
1954
|
+
}
|
|
1955
|
+
async function setSecret(options) {
|
|
1956
|
+
const { rootDir, manifestFilePath, name, value } = options;
|
|
1957
|
+
if (!isValidEnvName(name)) {
|
|
1958
|
+
throw new Error(`invalid secret name "${name}" (must match [A-Za-z0-9_]+)`);
|
|
1959
|
+
}
|
|
1960
|
+
const manifest = await readManifest(manifestFilePath);
|
|
1961
|
+
if (!manifest) {
|
|
1962
|
+
throw new Error(
|
|
1963
|
+
`no manifest at ${manifestFilePath}; run \`root secrets init\` first`
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
const blob = await accessSecretJson(manifest.gsmKey, manifest.gcpProjectId);
|
|
1967
|
+
blob[name] = value;
|
|
1968
|
+
await writeSecretJson(manifest.gsmKey, manifest.gcpProjectId, blob);
|
|
1969
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1970
|
+
const updatedBy = options.updatedBy ?? await getGitEmail();
|
|
1971
|
+
manifest.secrets[name] = { updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
1972
|
+
await writeManifest(manifestFilePath, manifest);
|
|
1973
|
+
const envFilePath = envPath(rootDir);
|
|
1974
|
+
const content = await readEnvFile(envFilePath);
|
|
1975
|
+
const next = upsertEnvVars(content, { [name]: value });
|
|
1976
|
+
if (next !== content) {
|
|
1977
|
+
await writeEnvFile(envFilePath, next);
|
|
1978
|
+
}
|
|
1979
|
+
const state = await readLocalState(rootDir);
|
|
1980
|
+
state.secrets[name] = { updatedAt, hash: hashValue(state.salt, value) };
|
|
1981
|
+
await writeLocalState(rootDir, state);
|
|
1982
|
+
}
|
|
1983
|
+
async function removeSecret(options) {
|
|
1984
|
+
const { rootDir, manifestFilePath, name } = options;
|
|
1985
|
+
const manifest = await readManifest(manifestFilePath);
|
|
1986
|
+
if (!manifest) {
|
|
1987
|
+
throw new Error(`no manifest at ${manifestFilePath}`);
|
|
1988
|
+
}
|
|
1989
|
+
if (Object.prototype.hasOwnProperty.call(manifest.secrets, name)) {
|
|
1990
|
+
delete manifest.secrets[name];
|
|
1991
|
+
await writeManifest(manifestFilePath, manifest);
|
|
1992
|
+
}
|
|
1993
|
+
const blob = await accessSecretJson(manifest.gsmKey, manifest.gcpProjectId);
|
|
1994
|
+
if (Object.prototype.hasOwnProperty.call(blob, name)) {
|
|
1995
|
+
delete blob[name];
|
|
1996
|
+
await writeSecretJson(manifest.gsmKey, manifest.gcpProjectId, blob);
|
|
1997
|
+
}
|
|
1998
|
+
const envFilePath = envPath(rootDir);
|
|
1999
|
+
const content = await readEnvFile(envFilePath);
|
|
2000
|
+
const next = upsertEnvVars(content, {}, [name]);
|
|
2001
|
+
if (next !== content) {
|
|
2002
|
+
await writeEnvFile(envFilePath, next);
|
|
2003
|
+
}
|
|
2004
|
+
const state = await readLocalState(rootDir);
|
|
2005
|
+
if (state.secrets[name]) {
|
|
2006
|
+
delete state.secrets[name];
|
|
2007
|
+
await writeLocalState(rootDir, state);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
async function pushEnvToSecrets(options) {
|
|
2011
|
+
const { rootDir, manifestFilePath, only } = options;
|
|
2012
|
+
const manifest = await readManifest(manifestFilePath);
|
|
2013
|
+
if (!manifest) {
|
|
2014
|
+
throw new Error(
|
|
2015
|
+
`no manifest at ${manifestFilePath}; run \`root secrets init\` first`
|
|
2016
|
+
);
|
|
2017
|
+
}
|
|
2018
|
+
const content = await readEnvFile(envPath(rootDir));
|
|
2019
|
+
const parsed = parseEnv(content);
|
|
2020
|
+
const skipped = [];
|
|
2021
|
+
const sharedNames = await sharedImportedNames(rootDir, manifestFilePath);
|
|
2022
|
+
const requested = only && only.length > 0 ? only : Object.keys(parsed);
|
|
2023
|
+
const candidates = [];
|
|
2024
|
+
for (const name of requested) {
|
|
2025
|
+
if (!Object.prototype.hasOwnProperty.call(parsed, name)) {
|
|
2026
|
+
skipped.push({ name, reason: "not found in .env" });
|
|
2027
|
+
} else if (!isValidEnvName(name)) {
|
|
2028
|
+
skipped.push({ name, reason: "invalid name" });
|
|
2029
|
+
} else if (sharedNames.has(name)) {
|
|
2030
|
+
skipped.push({ name, reason: "provided by shared manifest" });
|
|
2031
|
+
} else {
|
|
2032
|
+
candidates.push(name);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
if (candidates.length === 0) {
|
|
2036
|
+
return { pushed: [], skipped, aborted: false, gsmKey: manifest.gsmKey };
|
|
2037
|
+
}
|
|
2038
|
+
if (options.confirm && !await options.confirm(candidates, manifest.gsmKey)) {
|
|
2039
|
+
return { pushed: [], skipped, aborted: true, gsmKey: manifest.gsmKey };
|
|
2040
|
+
}
|
|
2041
|
+
const blob = await accessSecretJson(manifest.gsmKey, manifest.gcpProjectId);
|
|
2042
|
+
for (const name of candidates) {
|
|
2043
|
+
blob[name] = parsed[name];
|
|
2044
|
+
}
|
|
2045
|
+
await writeSecretJson(manifest.gsmKey, manifest.gcpProjectId, blob);
|
|
2046
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2047
|
+
const updatedBy = options.updatedBy ?? await getGitEmail();
|
|
2048
|
+
for (const name of candidates) {
|
|
2049
|
+
manifest.secrets[name] = { updatedAt, ...updatedBy ? { updatedBy } : {} };
|
|
2050
|
+
}
|
|
2051
|
+
await writeManifest(manifestFilePath, manifest);
|
|
2052
|
+
const state = await readLocalState(rootDir);
|
|
2053
|
+
for (const name of candidates) {
|
|
2054
|
+
state.secrets[name] = {
|
|
2055
|
+
updatedAt,
|
|
2056
|
+
hash: hashValue(state.salt, parsed[name])
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
await writeLocalState(rootDir, state);
|
|
2060
|
+
return {
|
|
2061
|
+
pushed: candidates,
|
|
2062
|
+
skipped,
|
|
2063
|
+
aborted: false,
|
|
2064
|
+
gsmKey: manifest.gsmKey
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
async function sharedImportedNames(rootDir, manifestFilePath) {
|
|
2068
|
+
if (path9.resolve(manifestFilePath) !== path9.resolve(manifestPath(rootDir))) {
|
|
2069
|
+
return /* @__PURE__ */ new Set();
|
|
2070
|
+
}
|
|
2071
|
+
try {
|
|
2072
|
+
const resolved = await resolveManagedKeys(rootDir);
|
|
2073
|
+
if (!resolved || !resolved.shared) {
|
|
2074
|
+
return /* @__PURE__ */ new Set();
|
|
2075
|
+
}
|
|
2076
|
+
const sharedGsmKey = resolved.shared.gsmKey;
|
|
2077
|
+
return new Set(
|
|
2078
|
+
resolved.keys.filter((key) => key.gsmKey === sharedGsmKey).map((key) => key.name)
|
|
2079
|
+
);
|
|
2080
|
+
} catch {
|
|
2081
|
+
return /* @__PURE__ */ new Set();
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
async function getSecretsStatus(rootDir) {
|
|
2085
|
+
const resolved = await resolveManagedKeys(rootDir);
|
|
2086
|
+
if (!resolved) {
|
|
2087
|
+
return { resolved: null, keys: [] };
|
|
2088
|
+
}
|
|
2089
|
+
const content = await readEnvFile(envPath(rootDir));
|
|
2090
|
+
const parsed = parseEnv(content);
|
|
2091
|
+
const state = await readLocalState(rootDir);
|
|
2092
|
+
const keys = resolved.keys.map((key) => {
|
|
2093
|
+
const synced = state.secrets[key.name];
|
|
2094
|
+
const envVal = parsed[key.name];
|
|
2095
|
+
const remoteChanged = !synced || key.updatedAt !== synced.updatedAt;
|
|
2096
|
+
const localChanged = synced ? hashValue(state.salt, envVal ?? "") !== synced.hash : envVal !== void 0;
|
|
2097
|
+
let kind;
|
|
2098
|
+
if (!synced) {
|
|
2099
|
+
kind = "not-pulled";
|
|
2100
|
+
} else if (remoteChanged && localChanged) {
|
|
2101
|
+
kind = "conflict";
|
|
2102
|
+
} else if (remoteChanged) {
|
|
2103
|
+
kind = "remote-newer";
|
|
2104
|
+
} else if (localChanged) {
|
|
2105
|
+
kind = "locally-edited";
|
|
2106
|
+
} else {
|
|
2107
|
+
kind = "in-sync";
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
name: key.name,
|
|
2111
|
+
gsmKey: key.gsmKey,
|
|
2112
|
+
updatedAt: key.updatedAt,
|
|
2113
|
+
kind,
|
|
2114
|
+
inEnv: envVal !== void 0
|
|
2115
|
+
};
|
|
2116
|
+
});
|
|
2117
|
+
return { resolved, keys };
|
|
2118
|
+
}
|
|
2119
|
+
async function isSecretsSyncEnabled(rootDir) {
|
|
2120
|
+
if (isSyncDisabledByEnv()) {
|
|
2121
|
+
return false;
|
|
2122
|
+
}
|
|
2123
|
+
return fileExists(manifestPath(rootDir));
|
|
2124
|
+
}
|
|
2125
|
+
function isSyncDisabledByEnv() {
|
|
2126
|
+
const value = (process.env.ROOT_DISABLE_SECRETS_SYNC || "").trim().toLowerCase();
|
|
2127
|
+
return value !== "" && value !== "0" && value !== "false";
|
|
2128
|
+
}
|
|
2129
|
+
async function readLocalState(rootDir) {
|
|
2130
|
+
try {
|
|
2131
|
+
const raw = await fs7.promises.readFile(localStatePath(rootDir), "utf8");
|
|
2132
|
+
const parsed = JSON.parse(raw);
|
|
2133
|
+
if (parsed && typeof parsed === "object" && typeof parsed.salt === "string" && parsed.secrets && typeof parsed.secrets === "object") {
|
|
2134
|
+
return {
|
|
2135
|
+
salt: parsed.salt,
|
|
2136
|
+
secrets: parsed.secrets
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
2139
|
+
} catch {
|
|
2140
|
+
}
|
|
2141
|
+
return { salt: randomSalt(), secrets: {} };
|
|
2142
|
+
}
|
|
2143
|
+
async function writeLocalState(rootDir, state) {
|
|
2144
|
+
const file = localStatePath(rootDir);
|
|
2145
|
+
await fs7.promises.mkdir(path9.dirname(file), { recursive: true });
|
|
2146
|
+
await fs7.promises.writeFile(
|
|
2147
|
+
file,
|
|
2148
|
+
JSON.stringify(state, null, 2) + "\n",
|
|
2149
|
+
"utf8"
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
function getGitEmail() {
|
|
2153
|
+
return new Promise((resolve) => {
|
|
2154
|
+
try {
|
|
2155
|
+
const child = spawn2("git", ["config", "user.email"]);
|
|
2156
|
+
let out = "";
|
|
2157
|
+
child.stdout.on("data", (chunk) => {
|
|
2158
|
+
out += chunk.toString();
|
|
2159
|
+
});
|
|
2160
|
+
child.on("error", () => resolve(void 0));
|
|
2161
|
+
child.on("close", (code) => {
|
|
2162
|
+
resolve(code === 0 && out.trim() ? out.trim() : void 0);
|
|
2163
|
+
});
|
|
2164
|
+
child.stdin.end();
|
|
2165
|
+
} catch {
|
|
2166
|
+
resolve(void 0);
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// src/cli/secrets.ts
|
|
2172
|
+
var BAR = dim3("\u2503");
|
|
2173
|
+
var DEV_SYNC_TIMEOUT_MS = 1e4;
|
|
2174
|
+
function registerSecretsCommands(program) {
|
|
2175
|
+
const secrets = program.command("secrets").description(
|
|
2176
|
+
"manage shared secrets backed by Google Cloud Secret Manager (requires the gcloud CLI)"
|
|
2177
|
+
);
|
|
2178
|
+
secrets.command("init").description("create a secrets manifest").requiredOption("--gcp-project <id>", "GCP project id").requiredOption("--gsm-key <key>", "Secret Manager key for this manifest").option(
|
|
2179
|
+
"--manifest <path>",
|
|
2180
|
+
"manifest path (defaults to ./.root.secrets.json)"
|
|
2181
|
+
).option("--import <path>", "path to a shared manifest to import keys from").option(
|
|
2182
|
+
"--import-keys <names>",
|
|
2183
|
+
"comma-separated subset of shared keys to import"
|
|
2184
|
+
).action(action(secretsInit));
|
|
2185
|
+
secrets.command("set <name>").description(
|
|
2186
|
+
"store a secret value, read from a prompt (TTY) or piped stdin"
|
|
2187
|
+
).option(
|
|
2188
|
+
"--manifest <path>",
|
|
2189
|
+
"target manifest (defaults to ./.root.secrets.json)"
|
|
2190
|
+
).action(action(secretsSet));
|
|
2191
|
+
secrets.command("rm <name>").description("remove a secret").option(
|
|
2192
|
+
"--manifest <path>",
|
|
2193
|
+
"target manifest (defaults to ./.root.secrets.json)"
|
|
2194
|
+
).action(action(secretsRm));
|
|
2195
|
+
secrets.command("sync").description("three-way merge managed secrets into .env").action(action(secretsSync));
|
|
2196
|
+
secrets.command("pull").description(
|
|
2197
|
+
"force-download managed secrets into .env (overwrites local edits)"
|
|
2198
|
+
).action(action(secretsPull));
|
|
2199
|
+
secrets.command("push").description(
|
|
2200
|
+
"push .env values up to Secret Manager and record them in the manifest"
|
|
2201
|
+
).option(
|
|
2202
|
+
"--manifest <path>",
|
|
2203
|
+
"target manifest (defaults to ./.root.secrets.json)"
|
|
2204
|
+
).option(
|
|
2205
|
+
"--keys <names>",
|
|
2206
|
+
"comma-separated subset of .env keys (default: all)"
|
|
2207
|
+
).option("--yes", "skip the confirmation prompt").action(action(secretsPush));
|
|
2208
|
+
secrets.command("status").description("show managed secrets and their sync status (no network)").action(action(secretsStatus));
|
|
2209
|
+
}
|
|
2210
|
+
async function secretsInit(opts) {
|
|
2211
|
+
const rootDir = process.cwd();
|
|
2212
|
+
const target = opts.manifest ? path10.resolve(rootDir, opts.manifest) : manifestPath(rootDir);
|
|
2213
|
+
if (await readManifest(target)) {
|
|
2214
|
+
console.log(
|
|
2215
|
+
`${BAR} ${yellow("secrets:")} manifest already exists at ${rel(rootDir, target)}`
|
|
2216
|
+
);
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
const manifest = emptyManifest(opts.gcpProject, opts.gsmKey);
|
|
2220
|
+
if (opts.import) {
|
|
2221
|
+
const importConfig = { manifest: opts.import };
|
|
2222
|
+
if (opts.importKeys) {
|
|
2223
|
+
const keys = opts.importKeys.split(",").map((k) => k.trim()).filter(Boolean);
|
|
2224
|
+
if (keys.length > 0) {
|
|
2225
|
+
importConfig.keys = keys;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
manifest.import = importConfig;
|
|
2229
|
+
}
|
|
2230
|
+
await writeManifest(target, manifest);
|
|
2231
|
+
const addedGitignore = await ensureRootGitignored(rootDir);
|
|
2232
|
+
console.log();
|
|
2233
|
+
console.log(`${BAR} ${green("secrets:")} created ${rel(rootDir, target)}`);
|
|
2234
|
+
if (addedGitignore) {
|
|
2235
|
+
console.log(`${BAR} added ${dim3(".root/")} to .gitignore`);
|
|
2236
|
+
}
|
|
2237
|
+
await warnIfEnvNotIgnored(rootDir);
|
|
2238
|
+
console.log(
|
|
2239
|
+
`${BAR} ${dim3("next: `root secrets set <NAME>` to store a value")}`
|
|
2240
|
+
);
|
|
2241
|
+
console.log();
|
|
2242
|
+
}
|
|
2243
|
+
async function secretsSet(name, opts) {
|
|
2244
|
+
const rootDir = process.cwd();
|
|
2245
|
+
const manifestFilePath = opts.manifest ? path10.resolve(rootDir, opts.manifest) : manifestPath(rootDir);
|
|
2246
|
+
const secretValue = await readSecretValue(name);
|
|
2247
|
+
if (!secretValue) {
|
|
2248
|
+
throw new Error("no value provided on stdin");
|
|
2249
|
+
}
|
|
2250
|
+
await setSecret({ rootDir, manifestFilePath, name, value: secretValue });
|
|
2251
|
+
console.log(`${BAR} ${green("secrets:")} set ${name}`);
|
|
2252
|
+
console.log(
|
|
2253
|
+
`${BAR} ${dim3(`commit ${rel(rootDir, manifestFilePath)} to share this change`)}`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
async function secretsRm(name, opts) {
|
|
2257
|
+
const rootDir = process.cwd();
|
|
2258
|
+
const manifestFilePath = opts.manifest ? path10.resolve(rootDir, opts.manifest) : manifestPath(rootDir);
|
|
2259
|
+
await removeSecret({ rootDir, manifestFilePath, name });
|
|
2260
|
+
console.log(`${BAR} ${green("secrets:")} removed ${name}`);
|
|
2261
|
+
console.log(
|
|
2262
|
+
`${BAR} ${dim3(`commit ${rel(rootDir, manifestFilePath)} to share this change`)}`
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
async function secretsPush(opts) {
|
|
2266
|
+
const rootDir = process.cwd();
|
|
2267
|
+
const manifestFilePath = opts.manifest ? path10.resolve(rootDir, opts.manifest) : manifestPath(rootDir);
|
|
2268
|
+
const only = opts.keys ? opts.keys.split(",").map((k) => k.trim()).filter(Boolean) : void 0;
|
|
2269
|
+
const confirm = opts.yes ? void 0 : async (names, gsmKey) => {
|
|
2270
|
+
if (!process.stdin.isTTY) {
|
|
2271
|
+
throw new Error(
|
|
2272
|
+
"refusing to import without --yes on a non-interactive shell"
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
console.log();
|
|
2276
|
+
console.log(
|
|
2277
|
+
`${BAR} ${green("secrets:")} push ${names.length} value(s) from .env to "${gsmKey}":`
|
|
2278
|
+
);
|
|
2279
|
+
for (const name of names) {
|
|
2280
|
+
console.log(`${BAR} ${name}`);
|
|
2281
|
+
}
|
|
2282
|
+
return promptYesNo(`${BAR} continue? [y/N] `);
|
|
2283
|
+
};
|
|
2284
|
+
const result = await pushEnvToSecrets({
|
|
2285
|
+
rootDir,
|
|
2286
|
+
manifestFilePath,
|
|
2287
|
+
only,
|
|
2288
|
+
confirm
|
|
2289
|
+
});
|
|
2290
|
+
printPushResult(result, rootDir, manifestFilePath);
|
|
2291
|
+
}
|
|
2292
|
+
async function secretsSync() {
|
|
2293
|
+
const rootDir = process.cwd();
|
|
2294
|
+
const result = await syncSecrets({ rootDir, apply: true });
|
|
2295
|
+
printSyncSummary(result);
|
|
2296
|
+
failOnSyncErrors(result);
|
|
2297
|
+
}
|
|
2298
|
+
async function secretsPull() {
|
|
2299
|
+
const rootDir = process.cwd();
|
|
2300
|
+
const result = await syncSecrets({ rootDir, apply: true, force: true });
|
|
2301
|
+
printSyncSummary(result, { pull: true });
|
|
2302
|
+
failOnSyncErrors(result);
|
|
2303
|
+
}
|
|
2304
|
+
function failOnSyncErrors(result) {
|
|
2305
|
+
if (result.errors.length > 0) {
|
|
2306
|
+
process.exitCode = 1;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
async function secretsStatus() {
|
|
2310
|
+
const rootDir = process.cwd();
|
|
2311
|
+
const { resolved, keys } = await getSecretsStatus(rootDir);
|
|
2312
|
+
if (!resolved) {
|
|
2313
|
+
console.log(
|
|
2314
|
+
`${BAR} ${yellow("secrets:")} no ${MANIFEST_FILENAME} here; run \`root secrets init\``
|
|
2315
|
+
);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
console.log();
|
|
2319
|
+
console.log(
|
|
2320
|
+
`${BAR} ${green("secrets:")} ${resolved.site.gsmKey} ${dim3(`(${resolved.site.gcpProjectId})`)}`
|
|
2321
|
+
);
|
|
2322
|
+
if (resolved.shared) {
|
|
2323
|
+
console.log(`${BAR} imports ${dim3(resolved.shared.gsmKey)}`);
|
|
2324
|
+
}
|
|
2325
|
+
if (keys.length === 0) {
|
|
2326
|
+
console.log(`${BAR} ${dim3("no managed keys yet")}`);
|
|
2327
|
+
}
|
|
2328
|
+
for (const key of keys) {
|
|
2329
|
+
console.log(`${BAR} ${formatStatus(key)}`);
|
|
2330
|
+
}
|
|
2331
|
+
console.log();
|
|
2332
|
+
}
|
|
2333
|
+
async function syncSecretsOnDev(rootDir) {
|
|
2334
|
+
if (!await isSecretsSyncEnabled(rootDir)) {
|
|
2335
|
+
return;
|
|
2336
|
+
}
|
|
2337
|
+
try {
|
|
2338
|
+
const result = await withTimeout(
|
|
2339
|
+
syncSecrets({ rootDir, apply: true }),
|
|
2340
|
+
DEV_SYNC_TIMEOUT_MS
|
|
2341
|
+
);
|
|
2342
|
+
printSyncSummary(result, { dev: true });
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
console.log();
|
|
2345
|
+
console.log(
|
|
2346
|
+
`${BAR} ${yellow("secrets:")} sync skipped (${firstLine(err?.message || String(err))})`
|
|
2347
|
+
);
|
|
2348
|
+
console.log();
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
function formatStatus(key) {
|
|
2352
|
+
switch (key.kind) {
|
|
2353
|
+
case "in-sync":
|
|
2354
|
+
return `${green("\u2713")} ${key.name}`;
|
|
2355
|
+
case "remote-newer":
|
|
2356
|
+
return `${yellow("\u2193")} ${key.name} ${dim3("(update available \u2014 run `root secrets sync`)")}`;
|
|
2357
|
+
case "locally-edited":
|
|
2358
|
+
return `${dim3("\u2022")} ${key.name} ${dim3("(locally edited)")}`;
|
|
2359
|
+
case "conflict":
|
|
2360
|
+
return `${yellow("!")} ${key.name} ${dim3("(changed locally & remotely)")}`;
|
|
2361
|
+
case "not-pulled":
|
|
2362
|
+
return `${yellow("\u2193")} ${key.name} ${dim3("(not pulled yet)")}`;
|
|
2363
|
+
default:
|
|
2364
|
+
return key.name;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
function printSyncSummary(result, opts = {}) {
|
|
2368
|
+
const notable = result.changed.length > 0 || result.removed.length > 0 || result.conflicts.length > 0 || result.overwritten.length > 0 || result.errors.length > 0;
|
|
2369
|
+
if (opts.dev && !notable) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const parts = [];
|
|
2373
|
+
if (result.changed.length) {
|
|
2374
|
+
parts.push(green(`${result.changed.length} updated`));
|
|
2375
|
+
}
|
|
2376
|
+
if (result.removed.length) {
|
|
2377
|
+
parts.push(`${result.removed.length} removed`);
|
|
2378
|
+
}
|
|
2379
|
+
if (result.kept.length) {
|
|
2380
|
+
parts.push(dim3(`${result.kept.length} local`));
|
|
2381
|
+
}
|
|
2382
|
+
if (result.conflicts.length) {
|
|
2383
|
+
parts.push(yellow(`${result.conflicts.length} conflict`));
|
|
2384
|
+
}
|
|
2385
|
+
if (result.errors.length) {
|
|
2386
|
+
parts.push(red(`${result.errors.length} error`));
|
|
2387
|
+
}
|
|
2388
|
+
console.log();
|
|
2389
|
+
console.log(
|
|
2390
|
+
`${BAR} ${green("secrets:")} ${parts.length ? parts.join(", ") : "up to date"}`
|
|
2391
|
+
);
|
|
2392
|
+
for (const name of result.changed) {
|
|
2393
|
+
console.log(`${BAR} ${green("+")} ${name}`);
|
|
2394
|
+
}
|
|
2395
|
+
for (const name of result.removed) {
|
|
2396
|
+
console.log(`${BAR} ${dim3("-")} ${name} ${dim3("(removed)")}`);
|
|
2397
|
+
}
|
|
2398
|
+
for (const name of result.overwritten) {
|
|
2399
|
+
console.log(
|
|
2400
|
+
`${BAR} ${yellow("!")} ${name} ${dim3("(local value replaced)")}`
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
for (const name of result.conflicts) {
|
|
2404
|
+
console.log(
|
|
2405
|
+
`${BAR} ${yellow("!")} ${name} ${dim3(
|
|
2406
|
+
"(changed locally & remotely; keeping yours \u2014 `root secrets pull` to take remote)"
|
|
2407
|
+
)}`
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
for (const err of result.errors) {
|
|
2411
|
+
console.log(
|
|
2412
|
+
`${BAR} ${red("\xD7")} ${err.gsmKey}: ${firstLine(err.message)}`
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
console.log();
|
|
2416
|
+
}
|
|
2417
|
+
function printPushResult(result, rootDir, manifestFilePath) {
|
|
2418
|
+
if (result.aborted) {
|
|
2419
|
+
console.log(`${BAR} ${yellow("secrets:")} aborted, nothing written`);
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
console.log();
|
|
2423
|
+
if (result.pushed.length === 0) {
|
|
2424
|
+
console.log(`${BAR} ${yellow("secrets:")} nothing to push`);
|
|
2425
|
+
} else {
|
|
2426
|
+
console.log(
|
|
2427
|
+
`${BAR} ${green("secrets:")} pushed ${result.pushed.length} value(s) to "${result.gsmKey}"`
|
|
2428
|
+
);
|
|
2429
|
+
for (const name of result.pushed) {
|
|
2430
|
+
console.log(`${BAR} ${green("+")} ${name}`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
for (const skip of result.skipped) {
|
|
2434
|
+
console.log(`${BAR} ${dim3("-")} ${skip.name} ${dim3(`(${skip.reason})`)}`);
|
|
2435
|
+
}
|
|
2436
|
+
if (result.pushed.length > 0) {
|
|
2437
|
+
console.log(
|
|
2438
|
+
`${BAR} ${dim3(`commit ${rel(rootDir, manifestFilePath)} to share`)}`
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
console.log();
|
|
2442
|
+
}
|
|
2443
|
+
async function ensureRootGitignored(rootDir) {
|
|
2444
|
+
const gitignore = path10.join(rootDir, ".gitignore");
|
|
2445
|
+
let content = "";
|
|
2446
|
+
try {
|
|
2447
|
+
content = await fs8.promises.readFile(gitignore, "utf8");
|
|
2448
|
+
} catch {
|
|
2449
|
+
}
|
|
2450
|
+
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
2451
|
+
if (lines.includes(".root/") || lines.includes(".root")) {
|
|
2452
|
+
return false;
|
|
2453
|
+
}
|
|
2454
|
+
const prefix = content && !content.endsWith("\n") ? "\n" : "";
|
|
2455
|
+
await fs8.promises.writeFile(gitignore, `${content}${prefix}.root/
|
|
2456
|
+
`, "utf8");
|
|
2457
|
+
return true;
|
|
2458
|
+
}
|
|
2459
|
+
async function warnIfEnvNotIgnored(rootDir) {
|
|
2460
|
+
const ignored = await isPathGitIgnored(rootDir, ".env");
|
|
2461
|
+
if (ignored === false) {
|
|
2462
|
+
console.log(
|
|
2463
|
+
`${BAR} ${yellow("warning:")} ${dim3(".env is not git-ignored \u2014 add it to .gitignore")}`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function isPathGitIgnored(rootDir, relPath) {
|
|
2468
|
+
return new Promise((resolve) => {
|
|
2469
|
+
try {
|
|
2470
|
+
const child = spawn3("git", ["check-ignore", "--quiet", relPath], {
|
|
2471
|
+
cwd: rootDir
|
|
2472
|
+
});
|
|
2473
|
+
child.on("error", () => resolve(void 0));
|
|
2474
|
+
child.on("close", (code) => {
|
|
2475
|
+
if (code === 0) {
|
|
2476
|
+
resolve(true);
|
|
2477
|
+
} else if (code === 1) {
|
|
2478
|
+
resolve(false);
|
|
2479
|
+
} else {
|
|
2480
|
+
resolve(void 0);
|
|
2481
|
+
}
|
|
2482
|
+
});
|
|
2483
|
+
} catch {
|
|
2484
|
+
resolve(void 0);
|
|
2485
|
+
}
|
|
2486
|
+
});
|
|
2487
|
+
}
|
|
2488
|
+
function readSecretValue(name) {
|
|
2489
|
+
if (process.stdin.isTTY) {
|
|
2490
|
+
return promptSecret(`Value for ${name}: `);
|
|
2491
|
+
}
|
|
2492
|
+
return readStdin().then(stripOneTrailingNewline);
|
|
2493
|
+
}
|
|
2494
|
+
function readStdin() {
|
|
2495
|
+
return new Promise((resolve, reject) => {
|
|
2496
|
+
let data = "";
|
|
2497
|
+
process.stdin.setEncoding("utf8");
|
|
2498
|
+
process.stdin.on("data", (chunk) => {
|
|
2499
|
+
data += chunk;
|
|
2500
|
+
});
|
|
2501
|
+
process.stdin.on("end", () => resolve(data));
|
|
2502
|
+
process.stdin.on("error", reject);
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
function stripOneTrailingNewline(value) {
|
|
2506
|
+
return value.replace(/\r?\n$/, "");
|
|
2507
|
+
}
|
|
2508
|
+
function promptYesNo(question) {
|
|
2509
|
+
return new Promise((resolve) => {
|
|
2510
|
+
const rl = readline.createInterface({
|
|
2511
|
+
input: process.stdin,
|
|
2512
|
+
output: process.stdout
|
|
2513
|
+
});
|
|
2514
|
+
rl.question(question, (answer) => {
|
|
2515
|
+
rl.close();
|
|
2516
|
+
const normalized = answer.trim().toLowerCase();
|
|
2517
|
+
resolve(normalized === "y" || normalized === "yes");
|
|
2518
|
+
});
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
function promptSecret(question) {
|
|
2522
|
+
return new Promise((resolve) => {
|
|
2523
|
+
const rl = readline.createInterface({
|
|
2524
|
+
input: process.stdin,
|
|
2525
|
+
output: process.stdout,
|
|
2526
|
+
terminal: true
|
|
2527
|
+
});
|
|
2528
|
+
let muted = false;
|
|
2529
|
+
const rlAny = rl;
|
|
2530
|
+
rlAny._writeToOutput = (chunk) => {
|
|
2531
|
+
if (!muted) {
|
|
2532
|
+
rlAny.output.write(chunk);
|
|
2533
|
+
} else if (chunk.includes("\n") || chunk.includes("\r")) {
|
|
2534
|
+
rlAny.output.write("\n");
|
|
2535
|
+
}
|
|
2536
|
+
};
|
|
2537
|
+
rl.question(question, (answer) => {
|
|
2538
|
+
rl.close();
|
|
2539
|
+
resolve(answer.trim());
|
|
2540
|
+
});
|
|
2541
|
+
muted = true;
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
function withTimeout(promise, ms) {
|
|
2545
|
+
return new Promise((resolve, reject) => {
|
|
2546
|
+
const timer = setTimeout(() => {
|
|
2547
|
+
reject(new Error(`timed out after ${ms}ms`));
|
|
2548
|
+
}, ms);
|
|
2549
|
+
promise.then(
|
|
2550
|
+
(value) => {
|
|
2551
|
+
clearTimeout(timer);
|
|
2552
|
+
resolve(value);
|
|
2553
|
+
},
|
|
2554
|
+
(err) => {
|
|
2555
|
+
clearTimeout(timer);
|
|
2556
|
+
reject(err);
|
|
2557
|
+
}
|
|
2558
|
+
);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
function rel(rootDir, filePath) {
|
|
2562
|
+
return path10.relative(rootDir, filePath) || path10.basename(filePath);
|
|
2563
|
+
}
|
|
2564
|
+
function firstLine(text) {
|
|
2565
|
+
return String(text).split("\n")[0];
|
|
2566
|
+
}
|
|
2567
|
+
function action(fn) {
|
|
2568
|
+
return async (...args) => {
|
|
2569
|
+
try {
|
|
2570
|
+
await fn(...args);
|
|
2571
|
+
} catch (err) {
|
|
2572
|
+
console.error(`${red("error:")} ${err?.message || err}`);
|
|
2573
|
+
process.exitCode = 1;
|
|
2574
|
+
}
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
|
|
1394
2578
|
// src/cli/startup/startup-tasks.ts
|
|
1395
2579
|
import os from "node:os";
|
|
1396
|
-
import
|
|
2580
|
+
import path11 from "node:path";
|
|
1397
2581
|
|
|
1398
2582
|
// src/cli/startup/check-version.ts
|
|
1399
|
-
import { dim as
|
|
2583
|
+
import { dim as dim4, green as green2, yellow as yellow2 } from "kleur/colors";
|
|
1400
2584
|
var PACKAGE_NAME = "@blinkk/root";
|
|
1401
2585
|
var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1402
2586
|
var REGISTRY_TIMEOUT_MS = 3e3;
|
|
@@ -1467,16 +2651,16 @@ function parseVersion(version) {
|
|
|
1467
2651
|
return [toInt(parts[0]), toInt(parts[1]), toInt(parts[2])];
|
|
1468
2652
|
}
|
|
1469
2653
|
function printUpdateNotice(current, latest) {
|
|
1470
|
-
const bar =
|
|
1471
|
-
const label =
|
|
2654
|
+
const bar = dim4("\u2503");
|
|
2655
|
+
const label = yellow2(`Update available for ${PACKAGE_NAME}:`);
|
|
1472
2656
|
console.log();
|
|
1473
|
-
console.log(`${bar} ${label} ${
|
|
2657
|
+
console.log(`${bar} ${label} ${dim4(current)} \u2192 ${green2(latest)}`);
|
|
1474
2658
|
console.log();
|
|
1475
2659
|
}
|
|
1476
2660
|
|
|
1477
2661
|
// src/cli/startup/startup-tasks.ts
|
|
1478
2662
|
var STARTUP_TASKS = [checkVersionTask];
|
|
1479
|
-
var STATE_FILE =
|
|
2663
|
+
var STATE_FILE = path11.join(os.homedir(), ".root", "startup-tasks.json");
|
|
1480
2664
|
async function runStartupTasks(ctx) {
|
|
1481
2665
|
try {
|
|
1482
2666
|
const state = await readState();
|
|
@@ -1527,16 +2711,17 @@ async function writeState(state) {
|
|
|
1527
2711
|
}
|
|
1528
2712
|
|
|
1529
2713
|
// src/cli/dev.ts
|
|
1530
|
-
var __dirname3 =
|
|
2714
|
+
var __dirname3 = path12.dirname(fileURLToPath3(import.meta.url));
|
|
1531
2715
|
var DEV_SERVER_404_LOG_IGNORE_PATHS = /* @__PURE__ */ new Set([
|
|
1532
2716
|
"/.well-known/appspecific/com.chrome.devtools.json"
|
|
1533
2717
|
]);
|
|
1534
2718
|
async function dev(rootProjectDir, options) {
|
|
1535
2719
|
process.env.NODE_ENV = "development";
|
|
1536
|
-
const rootDir =
|
|
2720
|
+
const rootDir = path12.resolve(rootProjectDir || process.cwd());
|
|
1537
2721
|
const defaultPort = parseInt(process.env.PORT || "4007");
|
|
1538
2722
|
const host = options?.host || "localhost";
|
|
1539
2723
|
const port = await findOpenPort(defaultPort, defaultPort + 10);
|
|
2724
|
+
await syncSecretsOnDev(rootDir);
|
|
1540
2725
|
void runStartupTasks({ rootDir, version: options?.version });
|
|
1541
2726
|
let currentServer = null;
|
|
1542
2727
|
let currentViteServer = null;
|
|
@@ -1548,19 +2733,19 @@ async function dev(rootProjectDir, options) {
|
|
|
1548
2733
|
const basePath = rootConfig.base || "";
|
|
1549
2734
|
const homePagePath = rootConfig.server?.homePagePath || basePath;
|
|
1550
2735
|
console.log();
|
|
1551
|
-
console.log(`${
|
|
1552
|
-
console.log(`${
|
|
2736
|
+
console.log(`${dim5("\u2503")} project: ${rootDir}`);
|
|
2737
|
+
console.log(`${dim5("\u2503")} server: http://${host}:${port}${homePagePath}`);
|
|
1553
2738
|
if (testCmsEnabled(rootConfig)) {
|
|
1554
|
-
console.log(`${
|
|
2739
|
+
console.log(`${dim5("\u2503")} cms: http://${host}:${port}/cms/`);
|
|
1555
2740
|
}
|
|
1556
|
-
console.log(`${
|
|
2741
|
+
console.log(`${dim5("\u2503")} mode: development`);
|
|
1557
2742
|
console.log();
|
|
1558
2743
|
currentServer = server.listen(port, host);
|
|
1559
2744
|
const rootConfigDependencies = server.get(
|
|
1560
2745
|
"rootConfigDependencies"
|
|
1561
2746
|
);
|
|
1562
2747
|
const dependencies = [
|
|
1563
|
-
|
|
2748
|
+
path12.resolve(rootDir, "root.config.ts"),
|
|
1564
2749
|
...rootConfigDependencies
|
|
1565
2750
|
];
|
|
1566
2751
|
viteServer.watcher.add(dependencies);
|
|
@@ -1568,7 +2753,7 @@ async function dev(rootProjectDir, options) {
|
|
|
1568
2753
|
if (dependencies.includes(file)) {
|
|
1569
2754
|
console.log(
|
|
1570
2755
|
`
|
|
1571
|
-
${
|
|
2756
|
+
${dim5("\u2503")} root.config.ts changed. restarting server...`
|
|
1572
2757
|
);
|
|
1573
2758
|
await restart();
|
|
1574
2759
|
}
|
|
@@ -1588,7 +2773,7 @@ ${dim4("\u2503")} root.config.ts changed. restarting server...`
|
|
|
1588
2773
|
await start2();
|
|
1589
2774
|
}
|
|
1590
2775
|
async function createDevServer(options) {
|
|
1591
|
-
const rootDir =
|
|
2776
|
+
const rootDir = path12.resolve(options?.rootDir || process.cwd());
|
|
1592
2777
|
const { rootConfig, dependencies } = await loadRootConfigWithDeps(rootDir, {
|
|
1593
2778
|
command: "dev"
|
|
1594
2779
|
});
|
|
@@ -1624,7 +2809,7 @@ async function createDevServer(options) {
|
|
|
1624
2809
|
if (rootConfig.server?.headers) {
|
|
1625
2810
|
server.use(headersMiddleware({ rootConfig }));
|
|
1626
2811
|
}
|
|
1627
|
-
const publicDir =
|
|
2812
|
+
const publicDir = path12.join(rootDir, "public");
|
|
1628
2813
|
if (await dirExists(publicDir)) {
|
|
1629
2814
|
server.use(rootPublicDirMiddleware({ publicDir, viteServer }));
|
|
1630
2815
|
}
|
|
@@ -1652,10 +2837,10 @@ async function createViteMiddleware(options) {
|
|
|
1652
2837
|
return sourceFile.relPath;
|
|
1653
2838
|
});
|
|
1654
2839
|
const bundleScripts = [];
|
|
1655
|
-
if (await isDirectory(
|
|
2840
|
+
if (await isDirectory(path12.join(rootDir, "bundles"))) {
|
|
1656
2841
|
const bundleFiles = await glob4("bundles/*", { cwd: rootDir });
|
|
1657
2842
|
bundleFiles.forEach((file) => {
|
|
1658
|
-
const parts =
|
|
2843
|
+
const parts = path12.parse(file);
|
|
1659
2844
|
if (isJsFile(parts.base)) {
|
|
1660
2845
|
bundleScripts.push(file);
|
|
1661
2846
|
}
|
|
@@ -1672,7 +2857,7 @@ async function createViteMiddleware(options) {
|
|
|
1672
2857
|
optimizeDeps
|
|
1673
2858
|
});
|
|
1674
2859
|
function isInElementsDir(changedFilePath) {
|
|
1675
|
-
const filePath =
|
|
2860
|
+
const filePath = path12.resolve(changedFilePath);
|
|
1676
2861
|
const elementsDirs = getElementsDirs(rootConfig, pods);
|
|
1677
2862
|
return elementsDirs.some((dirPath) => filePath.startsWith(dirPath));
|
|
1678
2863
|
}
|
|
@@ -1691,7 +2876,7 @@ async function createViteMiddleware(options) {
|
|
|
1691
2876
|
const viteMiddleware = async (req, res, next) => {
|
|
1692
2877
|
try {
|
|
1693
2878
|
req.viteServer = viteServer;
|
|
1694
|
-
const renderModulePath =
|
|
2879
|
+
const renderModulePath = path12.resolve(__dirname3, "./render.js");
|
|
1695
2880
|
const render = await viteServer.ssrLoadModule(
|
|
1696
2881
|
renderModulePath
|
|
1697
2882
|
);
|
|
@@ -1715,7 +2900,7 @@ function rootPublicDirMiddleware(options) {
|
|
|
1715
2900
|
handler = sirv(publicDir, sirvOptions);
|
|
1716
2901
|
}, 1e3);
|
|
1717
2902
|
function isInPublicDir(changedFilePath) {
|
|
1718
|
-
const filePath =
|
|
2903
|
+
const filePath = path12.resolve(changedFilePath);
|
|
1719
2904
|
return filePath.startsWith(publicDir);
|
|
1720
2905
|
}
|
|
1721
2906
|
const watcher = options.viteServer.watcher;
|
|
@@ -1747,7 +2932,7 @@ function rootDevServer404Middleware() {
|
|
|
1747
2932
|
}
|
|
1748
2933
|
if (req.renderer) {
|
|
1749
2934
|
const url = req.path;
|
|
1750
|
-
const ext =
|
|
2935
|
+
const ext = path12.extname(url);
|
|
1751
2936
|
if (!ext) {
|
|
1752
2937
|
const renderer = req.renderer;
|
|
1753
2938
|
const data = await renderer.renderDevServer404(req);
|
|
@@ -1765,7 +2950,7 @@ function rootDevServer500Middleware() {
|
|
|
1765
2950
|
console.error(String(err.stack || err));
|
|
1766
2951
|
if (req.renderer) {
|
|
1767
2952
|
const url = req.path;
|
|
1768
|
-
const ext =
|
|
2953
|
+
const ext = path12.extname(url);
|
|
1769
2954
|
if (!ext) {
|
|
1770
2955
|
const renderer = req.renderer;
|
|
1771
2956
|
const data = await renderer.renderDevServer500(req, err);
|
|
@@ -1791,7 +2976,7 @@ function debounce(fn, timeout) {
|
|
|
1791
2976
|
|
|
1792
2977
|
// src/cli/gae-deploy.ts
|
|
1793
2978
|
import { execSync } from "node:child_process";
|
|
1794
|
-
import
|
|
2979
|
+
import fs9 from "node:fs";
|
|
1795
2980
|
import yaml from "js-yaml";
|
|
1796
2981
|
async function gaeDeploy(appDir, options) {
|
|
1797
2982
|
if (!appDir) {
|
|
@@ -1805,10 +2990,10 @@ async function gaeDeploy(appDir, options) {
|
|
|
1805
2990
|
}
|
|
1806
2991
|
process.chdir(appDir);
|
|
1807
2992
|
const appYamlPath = "app.yaml";
|
|
1808
|
-
if (!
|
|
2993
|
+
if (!fs9.existsSync(appYamlPath)) {
|
|
1809
2994
|
throw new Error(`[gae-deplopy] Missing: ${appYamlPath}`);
|
|
1810
2995
|
}
|
|
1811
|
-
const appYaml = yaml.load(
|
|
2996
|
+
const appYaml = yaml.load(fs9.readFileSync(appYamlPath, "utf8"));
|
|
1812
2997
|
const service = appYaml.service;
|
|
1813
2998
|
if (!service) {
|
|
1814
2999
|
throw new Error(
|
|
@@ -1834,7 +3019,7 @@ async function gaeDeploy(appDir, options) {
|
|
|
1834
3019
|
{ stdio: "inherit" }
|
|
1835
3020
|
);
|
|
1836
3021
|
if (backupAppYaml) {
|
|
1837
|
-
|
|
3022
|
+
fs9.copyFileSync(backupAppYaml, "app.yaml");
|
|
1838
3023
|
}
|
|
1839
3024
|
if (options?.healthcheckUrl) {
|
|
1840
3025
|
const healthcheckPassed = await testHealth(
|
|
@@ -1921,8 +3106,8 @@ function testIsEnvPlaceholder(envValue) {
|
|
|
1921
3106
|
}
|
|
1922
3107
|
function updateAppYamlEnv(appYamlPath, appYaml) {
|
|
1923
3108
|
const backupAppYaml = `${appYamlPath}.bak`;
|
|
1924
|
-
|
|
1925
|
-
let content =
|
|
3109
|
+
fs9.copyFileSync(appYamlPath, backupAppYaml);
|
|
3110
|
+
let content = fs9.readFileSync(appYamlPath, "utf8");
|
|
1926
3111
|
const envVars = appYaml.env_variables;
|
|
1927
3112
|
Object.entries(envVars).forEach(([envVar, envValue]) => {
|
|
1928
3113
|
if (testIsEnvPlaceholder(envValue)) {
|
|
@@ -1935,7 +3120,7 @@ function updateAppYamlEnv(appYamlPath, appYaml) {
|
|
|
1935
3120
|
}
|
|
1936
3121
|
}
|
|
1937
3122
|
});
|
|
1938
|
-
|
|
3123
|
+
fs9.writeFileSync(appYamlPath, content, "utf8");
|
|
1939
3124
|
return backupAppYaml;
|
|
1940
3125
|
}
|
|
1941
3126
|
function getTimestamp() {
|
|
@@ -1959,29 +3144,29 @@ function testManagedVersion(service, appInfo, prefix) {
|
|
|
1959
3144
|
}
|
|
1960
3145
|
|
|
1961
3146
|
// src/cli/preview.ts
|
|
1962
|
-
import
|
|
3147
|
+
import path13 from "node:path";
|
|
1963
3148
|
import compression from "compression";
|
|
1964
3149
|
import cookieParser2 from "cookie-parser";
|
|
1965
3150
|
import { default as express2 } from "express";
|
|
1966
|
-
import { dim as
|
|
3151
|
+
import { dim as dim6 } from "kleur/colors";
|
|
1967
3152
|
import sirv2 from "sirv";
|
|
1968
3153
|
async function preview(rootProjectDir, options) {
|
|
1969
3154
|
process.env.NODE_ENV = "development";
|
|
1970
|
-
const rootDir =
|
|
3155
|
+
const rootDir = path13.resolve(rootProjectDir || process.cwd());
|
|
1971
3156
|
const server = await createPreviewServer({ rootDir });
|
|
1972
3157
|
const port = parseInt(process.env.PORT || "4007");
|
|
1973
3158
|
const host = options?.host || "localhost";
|
|
1974
3159
|
console.log();
|
|
1975
|
-
console.log(`${
|
|
1976
|
-
console.log(`${
|
|
1977
|
-
console.log(`${
|
|
3160
|
+
console.log(`${dim6("\u2503")} project: ${rootDir}`);
|
|
3161
|
+
console.log(`${dim6("\u2503")} server: http://${host}:${port}`);
|
|
3162
|
+
console.log(`${dim6("\u2503")} mode: preview`);
|
|
1978
3163
|
console.log();
|
|
1979
3164
|
server.listen(port, host);
|
|
1980
3165
|
}
|
|
1981
3166
|
async function createPreviewServer(options) {
|
|
1982
3167
|
const rootDir = options.rootDir;
|
|
1983
3168
|
const rootConfig = await loadBundledConfig(rootDir, { command: "preview" });
|
|
1984
|
-
const distDir =
|
|
3169
|
+
const distDir = path13.join(rootDir, "dist");
|
|
1985
3170
|
const server = express2();
|
|
1986
3171
|
server.disable("x-powered-by");
|
|
1987
3172
|
server.use(compression());
|
|
@@ -2007,7 +3192,7 @@ async function createPreviewServer(options) {
|
|
|
2007
3192
|
if (rootConfig.server?.headers) {
|
|
2008
3193
|
server.use(headersMiddleware({ rootConfig }));
|
|
2009
3194
|
}
|
|
2010
|
-
const publicDir =
|
|
3195
|
+
const publicDir = path13.join(distDir, "html");
|
|
2011
3196
|
server.use(sirv2(publicDir, { dev: false }));
|
|
2012
3197
|
server.use(trailingSlashMiddleware({ rootConfig }));
|
|
2013
3198
|
server.use(rootPreviewServerMiddleware());
|
|
@@ -2026,20 +3211,20 @@ async function createPreviewServer(options) {
|
|
|
2026
3211
|
}
|
|
2027
3212
|
async function rootPreviewRendererMiddleware(options) {
|
|
2028
3213
|
const { distDir, rootConfig } = options;
|
|
2029
|
-
const render = await import(
|
|
2030
|
-
const
|
|
2031
|
-
if (!await fileExists(
|
|
3214
|
+
const render = await import(path13.join(distDir, "server/render.js"));
|
|
3215
|
+
const manifestPath2 = path13.join(distDir, ".root/manifest.json");
|
|
3216
|
+
if (!await fileExists(manifestPath2)) {
|
|
2032
3217
|
throw new Error(
|
|
2033
|
-
`could not find ${
|
|
3218
|
+
`could not find ${manifestPath2}. run \`root build\` before \`root preview\`.`
|
|
2034
3219
|
);
|
|
2035
3220
|
}
|
|
2036
|
-
const elementGraphJsonPath =
|
|
3221
|
+
const elementGraphJsonPath = path13.join(distDir, ".root/elements.json");
|
|
2037
3222
|
if (!await fileExists(elementGraphJsonPath)) {
|
|
2038
3223
|
throw new Error(
|
|
2039
3224
|
`could not find ${elementGraphJsonPath}. run \`root build\` before \`root preview\`.`
|
|
2040
3225
|
);
|
|
2041
3226
|
}
|
|
2042
|
-
const rootManifest = await loadJson(
|
|
3227
|
+
const rootManifest = await loadJson(manifestPath2);
|
|
2043
3228
|
const assetMap = BuildAssetMap.fromRootManifest(rootConfig, rootManifest);
|
|
2044
3229
|
const elementGraphJson = await loadJson(elementGraphJsonPath);
|
|
2045
3230
|
const elementGraph = ElementGraph.fromJson(elementGraphJson);
|
|
@@ -2073,7 +3258,7 @@ function rootPreviewServer404Middleware() {
|
|
|
2073
3258
|
console.error(`\u2753 404 ${req.originalUrl}`);
|
|
2074
3259
|
if (req.renderer) {
|
|
2075
3260
|
const url = req.path;
|
|
2076
|
-
const ext =
|
|
3261
|
+
const ext = path13.extname(url);
|
|
2077
3262
|
if (!ext) {
|
|
2078
3263
|
const renderer = req.renderer;
|
|
2079
3264
|
const data = await renderer.render404({ currentPath: url });
|
|
@@ -2091,7 +3276,7 @@ function rootPreviewServer500Middleware() {
|
|
|
2091
3276
|
console.error(String(err.stack || err));
|
|
2092
3277
|
if (req.renderer) {
|
|
2093
3278
|
const url = req.path;
|
|
2094
|
-
const ext =
|
|
3279
|
+
const ext = path13.extname(url);
|
|
2095
3280
|
if (!ext) {
|
|
2096
3281
|
const renderer = req.renderer;
|
|
2097
3282
|
const data = await renderer.renderDevServer500(req, err);
|
|
@@ -2105,29 +3290,29 @@ function rootPreviewServer500Middleware() {
|
|
|
2105
3290
|
}
|
|
2106
3291
|
|
|
2107
3292
|
// src/cli/start.ts
|
|
2108
|
-
import
|
|
3293
|
+
import path14 from "node:path";
|
|
2109
3294
|
import compression2 from "compression";
|
|
2110
3295
|
import cookieParser3 from "cookie-parser";
|
|
2111
3296
|
import { default as express3 } from "express";
|
|
2112
|
-
import { dim as
|
|
3297
|
+
import { dim as dim7 } from "kleur/colors";
|
|
2113
3298
|
import sirv3 from "sirv";
|
|
2114
3299
|
async function start(rootProjectDir, options) {
|
|
2115
3300
|
process.env.NODE_ENV = "production";
|
|
2116
|
-
const rootDir =
|
|
3301
|
+
const rootDir = path14.resolve(rootProjectDir || process.cwd());
|
|
2117
3302
|
const server = await createProdServer({ rootDir });
|
|
2118
3303
|
const port = parseInt(process.env.PORT || "4007");
|
|
2119
3304
|
const host = options?.host || "localhost";
|
|
2120
3305
|
console.log();
|
|
2121
|
-
console.log(`${
|
|
2122
|
-
console.log(`${
|
|
2123
|
-
console.log(`${
|
|
3306
|
+
console.log(`${dim7("\u2503")} project: ${rootDir}`);
|
|
3307
|
+
console.log(`${dim7("\u2503")} server: http://${host}:${port}`);
|
|
3308
|
+
console.log(`${dim7("\u2503")} mode: production`);
|
|
2124
3309
|
console.log();
|
|
2125
3310
|
server.listen(port, host);
|
|
2126
3311
|
}
|
|
2127
3312
|
async function createProdServer(options) {
|
|
2128
3313
|
const rootDir = options.rootDir;
|
|
2129
3314
|
const rootConfig = await loadBundledConfig(rootDir, { command: "start" });
|
|
2130
|
-
const distDir =
|
|
3315
|
+
const distDir = path14.join(rootDir, "dist");
|
|
2131
3316
|
const server = express3();
|
|
2132
3317
|
server.disable("x-powered-by");
|
|
2133
3318
|
server.use(compression2());
|
|
@@ -2153,7 +3338,7 @@ async function createProdServer(options) {
|
|
|
2153
3338
|
if (rootConfig.server?.headers) {
|
|
2154
3339
|
server.use(headersMiddleware({ rootConfig }));
|
|
2155
3340
|
}
|
|
2156
|
-
const publicDir =
|
|
3341
|
+
const publicDir = path14.join(distDir, "html");
|
|
2157
3342
|
server.use(sirv3(publicDir, { dev: false }));
|
|
2158
3343
|
server.use(trailingSlashMiddleware({ rootConfig }));
|
|
2159
3344
|
server.use(rootProdServerMiddleware());
|
|
@@ -2172,20 +3357,20 @@ async function createProdServer(options) {
|
|
|
2172
3357
|
}
|
|
2173
3358
|
async function rootProdRendererMiddleware(options) {
|
|
2174
3359
|
const { distDir, rootConfig } = options;
|
|
2175
|
-
const render = await import(
|
|
2176
|
-
const
|
|
2177
|
-
if (!await fileExists(
|
|
3360
|
+
const render = await import(path14.join(distDir, "server/render.js"));
|
|
3361
|
+
const manifestPath2 = path14.join(distDir, ".root/manifest.json");
|
|
3362
|
+
if (!await fileExists(manifestPath2)) {
|
|
2178
3363
|
throw new Error(
|
|
2179
|
-
`could not find ${
|
|
3364
|
+
`could not find ${manifestPath2}. run \`root build\` before \`root start\`.`
|
|
2180
3365
|
);
|
|
2181
3366
|
}
|
|
2182
|
-
const elementGraphJsonPath =
|
|
3367
|
+
const elementGraphJsonPath = path14.join(distDir, ".root/elements.json");
|
|
2183
3368
|
if (!await fileExists(elementGraphJsonPath)) {
|
|
2184
3369
|
throw new Error(
|
|
2185
3370
|
`could not find ${elementGraphJsonPath}. run \`root build\` before \`root start\`.`
|
|
2186
3371
|
);
|
|
2187
3372
|
}
|
|
2188
|
-
const rootManifest = await loadJson(
|
|
3373
|
+
const rootManifest = await loadJson(manifestPath2);
|
|
2189
3374
|
const assetMap = BuildAssetMap.fromRootManifest(rootConfig, rootManifest);
|
|
2190
3375
|
const elementGraphJson = await loadJson(elementGraphJsonPath);
|
|
2191
3376
|
const elementGraph = ElementGraph.fromJson(elementGraphJson);
|
|
@@ -2219,7 +3404,7 @@ function rootProdServer404Middleware() {
|
|
|
2219
3404
|
console.error(`\u2753 404 ${req.originalUrl}`);
|
|
2220
3405
|
if (req.renderer) {
|
|
2221
3406
|
const url = req.path;
|
|
2222
|
-
const ext =
|
|
3407
|
+
const ext = path14.extname(url);
|
|
2223
3408
|
if (!ext) {
|
|
2224
3409
|
const renderer = req.renderer;
|
|
2225
3410
|
const data = await renderer.render404({ currentPath: url });
|
|
@@ -2237,7 +3422,7 @@ function rootProdServer500Middleware() {
|
|
|
2237
3422
|
console.error(String(err.stack || err));
|
|
2238
3423
|
if (req.renderer) {
|
|
2239
3424
|
const url = req.path;
|
|
2240
|
-
const ext =
|
|
3425
|
+
const ext = path14.extname(url);
|
|
2241
3426
|
if (!ext) {
|
|
2242
3427
|
const renderer = req.renderer;
|
|
2243
3428
|
const data = await renderer.renderError(err);
|
|
@@ -2321,6 +3506,7 @@ var CliRunner = class {
|
|
|
2321
3506
|
"--host <host>",
|
|
2322
3507
|
"network address the server should listen on, e.g. 127.0.0.1"
|
|
2323
3508
|
).action(start);
|
|
3509
|
+
registerSecretsCommands(program);
|
|
2324
3510
|
await program.parseAsync(argv);
|
|
2325
3511
|
}
|
|
2326
3512
|
};
|
|
@@ -2343,4 +3529,4 @@ export {
|
|
|
2343
3529
|
createProdServer,
|
|
2344
3530
|
CliRunner
|
|
2345
3531
|
};
|
|
2346
|
-
//# sourceMappingURL=chunk-
|
|
3532
|
+
//# sourceMappingURL=chunk-URG2INLL.js.map
|