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