@blinkk/root 3.0.2 → 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.
Files changed (123) hide show
  1. package/dist/{chunk-FQBA5QSG.js → chunk-3Z5TNZEP.js} +3 -3
  2. package/dist/chunk-3Z5TNZEP.js.map +7 -0
  3. package/dist/{chunk-JCRXBB26.js → chunk-6NBKAR5Y.js} +2 -2
  4. package/dist/chunk-6NBKAR5Y.js.map +7 -0
  5. package/dist/chunk-6P3B7ZXL.js +1 -1
  6. package/dist/chunk-6P3B7ZXL.js.map +7 -1
  7. package/dist/{chunk-PVBPP5LN.js → chunk-7PSEEE6C.js} +10 -10
  8. package/dist/chunk-7PSEEE6C.js.map +7 -0
  9. package/dist/{chunk-WAZTR3DX.js → chunk-KW7KTYGC.js} +2 -2
  10. package/dist/chunk-KW7KTYGC.js.map +7 -0
  11. package/dist/{chunk-GSR74KEI.js → chunk-URG2INLL.js} +1477 -85
  12. package/dist/chunk-URG2INLL.js.map +7 -0
  13. package/dist/{chunk-4PO5HYET.js → chunk-X2C2MEJ2.js} +39 -4
  14. package/dist/chunk-X2C2MEJ2.js.map +7 -0
  15. package/dist/chunk-XSNCF7WU.js +1 -1
  16. package/dist/chunk-XSNCF7WU.js.map +7 -1
  17. package/dist/cli/build.d.ts +7 -0
  18. package/dist/cli/cli.d.ts +12 -0
  19. package/dist/cli/codegen.d.ts +5 -0
  20. package/dist/cli/create-package.d.ts +14 -0
  21. package/dist/cli/dev.d.ts +10 -0
  22. package/dist/cli/gae-deploy.d.ts +18 -0
  23. package/dist/cli/preview.d.ts +8 -0
  24. package/dist/cli/secrets.d.ts +9 -0
  25. package/dist/cli/start.d.ts +8 -0
  26. package/dist/cli/startup/check-version.d.ts +17 -0
  27. package/dist/cli/startup/startup-tasks.d.ts +39 -0
  28. package/dist/cli.js +4 -4
  29. package/dist/cli.js.map +7 -1
  30. package/dist/core/components/Body.d.ts +23 -0
  31. package/dist/core/components/Head.d.ts +18 -0
  32. package/dist/core/components/Html.d.ts +24 -0
  33. package/dist/core/components/Script.d.ts +16 -0
  34. package/dist/core/config.d.ts +285 -0
  35. package/dist/core/core.d.ts +14 -0
  36. package/dist/core/hooks/useI18nContext.d.ts +11 -0
  37. package/dist/core/hooks/useRequestContext.d.ts +36 -0
  38. package/dist/core/hooks/useStringParams.d.ts +34 -0
  39. package/dist/core/hooks/useTranslations.d.ts +20 -0
  40. package/dist/core/hooks/useTranslationsMiddleware.d.ts +25 -0
  41. package/dist/core/pages/DevErrorPage.d.ts +9 -0
  42. package/dist/core/pages/DevNotFoundPage.d.ts +10 -0
  43. package/dist/core/pages/ErrorPage.d.ts +9 -0
  44. package/dist/core/plugin.d.ts +80 -0
  45. package/dist/core/pod.d.ts +54 -0
  46. package/dist/core/types.d.ts +228 -0
  47. package/dist/core.js +3 -3
  48. package/dist/core.js.map +7 -1
  49. package/dist/functions/functions.d.ts +1 -0
  50. package/dist/functions/server.d.ts +10 -0
  51. package/dist/functions.js +5 -5
  52. package/dist/functions.js.map +7 -1
  53. package/dist/jsx/jsx-dev-runtime.d.ts +10 -1
  54. package/dist/jsx/jsx-dev-runtime.js +2 -2
  55. package/dist/jsx/jsx-dev-runtime.js.map +7 -1
  56. package/dist/{jsx-C8cJO55i.d.ts → jsx/jsx-render.d.ts} +3 -6
  57. package/dist/jsx/jsx-runtime.d.ts +120 -1
  58. package/dist/jsx/jsx-runtime.js +2 -2
  59. package/dist/jsx/jsx-runtime.js.map +7 -1
  60. package/dist/jsx/jsx.d.ts +37 -0
  61. package/dist/{jsx-dev-runtime-DHf9jiFH.d.ts → jsx/types.d.ts} +8 -124
  62. package/dist/jsx.js +3 -3
  63. package/dist/jsx.js.map +7 -1
  64. package/dist/middleware/common.d.ts +22 -0
  65. package/dist/middleware/hooks.d.ts +8 -0
  66. package/dist/middleware/middleware.d.ts +3 -0
  67. package/dist/middleware/multipart.d.ts +11 -0
  68. package/dist/middleware/redirects.d.ts +10 -0
  69. package/dist/middleware/session.d.ts +24 -0
  70. package/dist/middleware.js +2 -2
  71. package/dist/middleware.js.map +7 -1
  72. package/dist/node/element-graph.d.ts +51 -0
  73. package/dist/node/load-config.d.ts +17 -0
  74. package/dist/node/monorepo.d.ts +14 -0
  75. package/dist/node/node.d.ts +4 -0
  76. package/dist/node/pod-collector.d.ts +33 -0
  77. package/dist/node/pods-vite-plugin.d.ts +6 -0
  78. package/dist/node/vite-plugin-preact-alias.d.ts +10 -0
  79. package/dist/node/vite-plugin-prune-empty-chunks.d.ts +27 -0
  80. package/dist/node/vite-plugin-root-jsx-virtual.d.ts +16 -0
  81. package/dist/node/vite.d.ts +18 -0
  82. package/dist/node.js +2 -2
  83. package/dist/node.js.map +7 -1
  84. package/dist/render/accept-language.d.ts +8 -0
  85. package/dist/render/asset-map/asset-map.d.ts +25 -0
  86. package/dist/render/asset-map/build-asset-map.d.ts +47 -0
  87. package/dist/render/asset-map/dev-asset-map.d.ts +26 -0
  88. package/dist/render/countrie-language.d.ts +13 -0
  89. package/dist/render/html-minify.d.ts +3 -0
  90. package/dist/render/html-pretty.d.ts +3 -0
  91. package/dist/render/i18n-fallbacks.d.ts +11 -0
  92. package/dist/render/render.d.ts +91 -0
  93. package/dist/render/route-trie.d.ts +44 -0
  94. package/dist/render/router.d.ts +18 -0
  95. package/dist/render.js +9 -9
  96. package/dist/render.js.map +7 -1
  97. package/dist/secrets/env-file.d.ts +44 -0
  98. package/dist/secrets/gcloud.d.ts +28 -0
  99. package/dist/secrets/hash.d.ts +11 -0
  100. package/dist/secrets/manifest.d.ts +60 -0
  101. package/dist/secrets/secrets.d.ts +118 -0
  102. package/dist/utils/batch.d.ts +1 -0
  103. package/dist/utils/elements.d.ts +12 -0
  104. package/dist/utils/fsutils.d.ts +24 -0
  105. package/dist/utils/i18n.d.ts +8 -0
  106. package/dist/utils/ports.d.ts +8 -0
  107. package/dist/utils/rand.d.ts +10 -0
  108. package/dist/utils/url-path-params.d.ts +2 -0
  109. package/package.json +14 -15
  110. package/dist/chunk-4PO5HYET.js.map +0 -1
  111. package/dist/chunk-FQBA5QSG.js.map +0 -1
  112. package/dist/chunk-GSR74KEI.js.map +0 -1
  113. package/dist/chunk-JCRXBB26.js.map +0 -1
  114. package/dist/chunk-PVBPP5LN.js.map +0 -1
  115. package/dist/chunk-WAZTR3DX.js.map +0 -1
  116. package/dist/cli.d.ts +0 -60
  117. package/dist/core.d.ts +0 -214
  118. package/dist/functions.d.ts +0 -14
  119. package/dist/jsx.d.ts +0 -2
  120. package/dist/middleware.d.ts +0 -43
  121. package/dist/node.d.ts +0 -91
  122. package/dist/render.d.ts +0 -8
  123. package/dist/types-B-pJtsB4.d.ts +0 -827
@@ -10,7 +10,7 @@ import {
10
10
  rootProjectMiddleware,
11
11
  sessionMiddleware,
12
12
  trailingSlashMiddleware
13
- } from "./chunk-JCRXBB26.js";
13
+ } from "./chunk-6NBKAR5Y.js";
14
14
  import {
15
15
  bundleRootConfig,
16
16
  collectPods,
@@ -33,7 +33,7 @@ import {
33
33
  rootPodsVitePlugin,
34
34
  writeFile,
35
35
  writeJson
36
- } from "./chunk-PVBPP5LN.js";
36
+ } from "./chunk-7PSEEE6C.js";
37
37
  import {
38
38
  configureServerPlugins,
39
39
  getVitePlugins
@@ -44,16 +44,16 @@ import { Command, InvalidArgumentError } from "commander";
44
44
  import { bgGreen, black } from "kleur/colors";
45
45
 
46
46
  // src/cli/build.ts
47
- import path3 from "path";
48
- import { fileURLToPath } from "url";
47
+ import path3 from "node:path";
48
+ import { fileURLToPath } from "node:url";
49
49
  import fsExtra from "fs-extra";
50
- import { dim, cyan } from "kleur/colors";
50
+ import { dim as dim2, cyan } from "kleur/colors";
51
51
  import glob2 from "tiny-glob";
52
52
  import { build as viteBuild } from "vite";
53
53
 
54
54
  // src/node/element-graph.ts
55
- import fs from "fs";
56
- import path from "path";
55
+ import fs from "node:fs";
56
+ import path from "node:path";
57
57
  import glob from "tiny-glob";
58
58
  import { searchForWorkspaceRoot } from "vite";
59
59
  var ElementGraph = class _ElementGraph {
@@ -167,9 +167,74 @@ function getElementsDirs(rootConfig, pods) {
167
167
  return elementsDirs;
168
168
  }
169
169
 
170
+ // src/node/vite-plugin-prune-empty-chunks.ts
171
+ import { dim } from "kleur/colors";
172
+ function pruneEmptyChunksPlugin() {
173
+ return {
174
+ name: "root:prune-empty-chunks",
175
+ generateBundle: {
176
+ // Run before Vite's manifest plugin so the manifest reflects removals.
177
+ order: "pre",
178
+ handler(_options, bundle) {
179
+ const emptyChunks = /* @__PURE__ */ new Set();
180
+ for (const [fileName, item] of Object.entries(bundle)) {
181
+ if (item.type !== "chunk" || item.code.trim() !== "") {
182
+ continue;
183
+ }
184
+ const css = item.viteMetadata?.importedCss;
185
+ const assets = item.viteMetadata?.importedAssets;
186
+ if (css && css.size > 0 || assets && assets.size > 0) {
187
+ continue;
188
+ }
189
+ emptyChunks.add(fileName);
190
+ }
191
+ if (emptyChunks.size === 0) {
192
+ return;
193
+ }
194
+ for (const item of Object.values(bundle)) {
195
+ if (item.type !== "chunk") {
196
+ continue;
197
+ }
198
+ const referenced = new Set(
199
+ [...item.imports || [], ...item.dynamicImports || []].filter(
200
+ (fileName) => emptyChunks.has(fileName)
201
+ )
202
+ );
203
+ for (const emptyFileName of referenced) {
204
+ item.code = removeSideEffectImport(item.code, emptyFileName);
205
+ }
206
+ if (item.imports) {
207
+ item.imports = item.imports.filter((f) => !emptyChunks.has(f));
208
+ }
209
+ if (item.dynamicImports) {
210
+ item.dynamicImports = item.dynamicImports.filter(
211
+ (f) => !emptyChunks.has(f)
212
+ );
213
+ }
214
+ }
215
+ for (const fileName of emptyChunks) {
216
+ delete bundle[fileName];
217
+ }
218
+ const removed = Array.from(emptyChunks).sort();
219
+ console.log(
220
+ `${dim("\u2503")} pruned ${removed.length} empty chunk(s): ${dim(
221
+ removed.join(", ")
222
+ )}`
223
+ );
224
+ }
225
+ }
226
+ };
227
+ }
228
+ function removeSideEffectImport(code, fileName) {
229
+ const base = fileName.split("/").pop() || fileName;
230
+ const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
231
+ const re = new RegExp(`import\\s*["'][^"']*${escaped}["']\\s*;?`, "g");
232
+ return code.replace(re, "");
233
+ }
234
+
170
235
  // src/render/asset-map/build-asset-map.ts
171
- import fs2 from "fs";
172
- import path2 from "path";
236
+ import fs2 from "node:fs";
237
+ import path2 from "node:path";
173
238
  var BuildAssetMap = class _BuildAssetMap {
174
239
  rootConfig;
175
240
  srcToAsset;
@@ -356,9 +421,9 @@ async function build(rootProjectDir, options) {
356
421
  const distDir = path3.join(rootDir, "dist");
357
422
  const ssrOnly = options?.ssrOnly || false;
358
423
  console.log();
359
- console.log(`${dim("\u2503")} project: ${rootDir}`);
360
- console.log(`${dim("\u2503")} output: ${distDir}/html`);
361
- console.log(`${dim("\u2503")} mode: ${mode}`);
424
+ console.log(`${dim2("\u2503")} project: ${rootDir}`);
425
+ console.log(`${dim2("\u2503")} output: ${distDir}/html`);
426
+ console.log(`${dim2("\u2503")} mode: ${mode}`);
362
427
  console.log();
363
428
  await rmDir(distDir);
364
429
  await makeDir(distDir);
@@ -408,7 +473,8 @@ async function build(rootProjectDir, options) {
408
473
  preactToRootJsxPlugin({ useRootJsx: !!rootConfig.jsxRenderer?.mode }),
409
474
  rootPodsVitePlugin(rootConfig),
410
475
  ...viteConfig.plugins || [],
411
- ...getVitePlugins(rootPlugins)
476
+ ...getVitePlugins(rootPlugins),
477
+ pruneEmptyChunksPlugin()
412
478
  ];
413
479
  const baseConfig = {
414
480
  ...viteConfig,
@@ -733,7 +799,8 @@ async function build(rootProjectDir, options) {
733
799
  e
734
800
  );
735
801
  throw new Error(
736
- `BuildError: ${urlPath} (${sitemapItem.route.src}) failed to build.`
802
+ `BuildError: ${urlPath} (${sitemapItem.route.src}) failed to build.`,
803
+ { cause: e }
737
804
  );
738
805
  }
739
806
  }
@@ -792,7 +859,7 @@ function printFileOutput(fileSize2, outputDir, outputFile) {
792
859
  const indent = " ".repeat(2);
793
860
  const paddedSize = fileSize2.padStart(9, " ");
794
861
  console.log(
795
- `${indent}${dim(paddedSize)} ${dim(outputDir)}${cyan(outputFile)}`
862
+ `${indent}${dim2(paddedSize)} ${dim2(outputDir)}${cyan(outputFile)}`
796
863
  );
797
864
  }
798
865
  function sanitizeFileName(name) {
@@ -827,9 +894,9 @@ function formatParams(params) {
827
894
  }
828
895
 
829
896
  // src/cli/codegen.ts
830
- import { promises as fs3 } from "fs";
831
- import path4 from "path";
832
- import { fileURLToPath as fileURLToPath2 } from "url";
897
+ import { promises as fs3 } from "node:fs";
898
+ import path4 from "node:path";
899
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
833
900
  import glob3 from "tiny-glob";
834
901
  var __dirname2 = path4.dirname(fileURLToPath2(import.meta.url));
835
902
  async function codegen(type, name, options) {
@@ -907,8 +974,8 @@ function toTitleCase(str) {
907
974
  }
908
975
 
909
976
  // src/cli/create-package.ts
910
- import { promises as fs4 } from "fs";
911
- import path5 from "path";
977
+ import { promises as fs4 } from "node:fs";
978
+ import path5 from "node:path";
912
979
  import { build as esbuild } from "esbuild";
913
980
  async function createPackage(rootProjectDir, options) {
914
981
  const mode = options?.mode || "production";
@@ -1040,11 +1107,11 @@ async function bundleTsFile(srcPath, outPath) {
1040
1107
  }
1041
1108
 
1042
1109
  // src/cli/dev.ts
1043
- import path7 from "path";
1044
- import { fileURLToPath as fileURLToPath3 } from "url";
1110
+ import path12 from "node:path";
1111
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
1045
1112
  import cookieParser from "cookie-parser";
1046
1113
  import { default as express } from "express";
1047
- import { dim as dim2 } from "kleur/colors";
1114
+ import { dim as dim5 } from "kleur/colors";
1048
1115
  import sirv from "sirv";
1049
1116
  import glob4 from "tiny-glob";
1050
1117
 
@@ -1134,7 +1201,7 @@ function testIsRedirectValid(url) {
1134
1201
  }
1135
1202
 
1136
1203
  // src/render/asset-map/dev-asset-map.ts
1137
- import path6 from "path";
1204
+ import path6 from "node:path";
1138
1205
  import { searchForWorkspaceRoot as searchForWorkspaceRoot2 } from "vite";
1139
1206
  var DevServerAssetMap = class {
1140
1207
  rootConfig;
@@ -1264,7 +1331,7 @@ var DevServerAsset = class _DevServerAsset {
1264
1331
  };
1265
1332
 
1266
1333
  // src/utils/ports.ts
1267
- import { createServer } from "net";
1334
+ import { createServer } from "node:net";
1268
1335
  function isPortOpen(port) {
1269
1336
  return new Promise((resolve, reject) => {
1270
1337
  const server = createServer();
@@ -1301,7 +1368,7 @@ async function findOpenPort(min, max) {
1301
1368
  }
1302
1369
 
1303
1370
  // src/utils/rand.ts
1304
- import crypto from "crypto";
1371
+ import crypto from "node:crypto";
1305
1372
  function randString(len) {
1306
1373
  const result = [];
1307
1374
  const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -1324,17 +1391,1338 @@ function getSessionCookieSecret(rootConfig, rootDir) {
1324
1391
  return randString(36);
1325
1392
  }
1326
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
+
2578
+ // src/cli/startup/startup-tasks.ts
2579
+ import os from "node:os";
2580
+ import path11 from "node:path";
2581
+
2582
+ // src/cli/startup/check-version.ts
2583
+ import { dim as dim4, green as green2, yellow as yellow2 } from "kleur/colors";
2584
+ var PACKAGE_NAME = "@blinkk/root";
2585
+ var ONE_DAY_MS = 24 * 60 * 60 * 1e3;
2586
+ var REGISTRY_TIMEOUT_MS = 3e3;
2587
+ var checkVersionTask = {
2588
+ name: "check-version",
2589
+ throttle: ONE_DAY_MS,
2590
+ // Skip the check when disabled via env var, when the version is unknown, or
2591
+ // for pre-release (alpha/beta/rc) builds that are intentionally ahead of the
2592
+ // latest stable release.
2593
+ enabled(ctx) {
2594
+ return !isVersionCheckDisabled() && !!ctx.version && !isPrerelease(ctx.version);
2595
+ },
2596
+ async run(ctx) {
2597
+ const current = ctx.version;
2598
+ const latest = await fetchLatestVersion();
2599
+ if (current && latest && isNewerVersion(current, latest)) {
2600
+ printUpdateNotice(current, latest);
2601
+ }
2602
+ }
2603
+ };
2604
+ function isVersionCheckDisabled() {
2605
+ return isEnvEnabled("ROOT_DISABLE_VERSION_CHECK") || isEnvEnabled("NO_UPDATE_NOTIFIER");
2606
+ }
2607
+ function isEnvEnabled(name) {
2608
+ const value = (process.env[name] || "").trim().toLowerCase();
2609
+ return value !== "" && value !== "0" && value !== "false";
2610
+ }
2611
+ async function fetchLatestVersion() {
2612
+ const controller = new AbortController();
2613
+ const timeout = setTimeout(() => controller.abort(), REGISTRY_TIMEOUT_MS);
2614
+ try {
2615
+ const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
2616
+ const res = await fetch(url, { signal: controller.signal });
2617
+ if (!res.ok) {
2618
+ return null;
2619
+ }
2620
+ const data = await res.json();
2621
+ return data.version || null;
2622
+ } catch {
2623
+ return null;
2624
+ } finally {
2625
+ clearTimeout(timeout);
2626
+ }
2627
+ }
2628
+ function isNewerVersion(current, latest) {
2629
+ const a = parseVersion(latest);
2630
+ const b = parseVersion(current);
2631
+ for (let i = 0; i < 3; i++) {
2632
+ if (a[i] > b[i]) {
2633
+ return true;
2634
+ }
2635
+ if (a[i] < b[i]) {
2636
+ return false;
2637
+ }
2638
+ }
2639
+ return false;
2640
+ }
2641
+ function isPrerelease(version) {
2642
+ return /-(alpha|beta|rc)\b/i.test(version);
2643
+ }
2644
+ function parseVersion(version) {
2645
+ const main = version.split("-")[0];
2646
+ const parts = main.split(".");
2647
+ const toInt = (value) => {
2648
+ const num = parseInt(value || "0", 10);
2649
+ return isNaN(num) ? 0 : num;
2650
+ };
2651
+ return [toInt(parts[0]), toInt(parts[1]), toInt(parts[2])];
2652
+ }
2653
+ function printUpdateNotice(current, latest) {
2654
+ const bar = dim4("\u2503");
2655
+ const label = yellow2(`Update available for ${PACKAGE_NAME}:`);
2656
+ console.log();
2657
+ console.log(`${bar} ${label} ${dim4(current)} \u2192 ${green2(latest)}`);
2658
+ console.log();
2659
+ }
2660
+
2661
+ // src/cli/startup/startup-tasks.ts
2662
+ var STARTUP_TASKS = [checkVersionTask];
2663
+ var STATE_FILE = path11.join(os.homedir(), ".root", "startup-tasks.json");
2664
+ async function runStartupTasks(ctx) {
2665
+ try {
2666
+ const state = await readState();
2667
+ const now = Date.now();
2668
+ const dueTasks = STARTUP_TASKS.filter(
2669
+ (task) => isEnabled(task, ctx) && isDue(task, state.lastRun[task.name], now)
2670
+ );
2671
+ if (dueTasks.length === 0) {
2672
+ return;
2673
+ }
2674
+ await Promise.all(
2675
+ dueTasks.map(async (task) => {
2676
+ try {
2677
+ await task.run(ctx);
2678
+ } catch {
2679
+ }
2680
+ state.lastRun[task.name] = Date.now();
2681
+ })
2682
+ );
2683
+ await writeState(state);
2684
+ } catch {
2685
+ }
2686
+ }
2687
+ function isEnabled(task, ctx) {
2688
+ return !task.enabled || task.enabled(ctx);
2689
+ }
2690
+ function isDue(task, lastRun, now) {
2691
+ if (!task.throttle || !lastRun) {
2692
+ return true;
2693
+ }
2694
+ return now - lastRun >= task.throttle;
2695
+ }
2696
+ async function readState() {
2697
+ if (await fileExists(STATE_FILE)) {
2698
+ try {
2699
+ const state = await loadJson(STATE_FILE);
2700
+ return { lastRun: state.lastRun || {} };
2701
+ } catch {
2702
+ }
2703
+ }
2704
+ return { lastRun: {} };
2705
+ }
2706
+ async function writeState(state) {
2707
+ try {
2708
+ await writeJson(STATE_FILE, state);
2709
+ } catch {
2710
+ }
2711
+ }
2712
+
1327
2713
  // src/cli/dev.ts
1328
- var __dirname3 = path7.dirname(fileURLToPath3(import.meta.url));
2714
+ var __dirname3 = path12.dirname(fileURLToPath3(import.meta.url));
1329
2715
  var DEV_SERVER_404_LOG_IGNORE_PATHS = /* @__PURE__ */ new Set([
1330
2716
  "/.well-known/appspecific/com.chrome.devtools.json"
1331
2717
  ]);
1332
2718
  async function dev(rootProjectDir, options) {
1333
2719
  process.env.NODE_ENV = "development";
1334
- const rootDir = path7.resolve(rootProjectDir || process.cwd());
2720
+ const rootDir = path12.resolve(rootProjectDir || process.cwd());
1335
2721
  const defaultPort = parseInt(process.env.PORT || "4007");
1336
2722
  const host = options?.host || "localhost";
1337
2723
  const port = await findOpenPort(defaultPort, defaultPort + 10);
2724
+ await syncSecretsOnDev(rootDir);
2725
+ void runStartupTasks({ rootDir, version: options?.version });
1338
2726
  let currentServer = null;
1339
2727
  let currentViteServer = null;
1340
2728
  async function start2() {
@@ -1345,19 +2733,19 @@ async function dev(rootProjectDir, options) {
1345
2733
  const basePath = rootConfig.base || "";
1346
2734
  const homePagePath = rootConfig.server?.homePagePath || basePath;
1347
2735
  console.log();
1348
- console.log(`${dim2("\u2503")} project: ${rootDir}`);
1349
- console.log(`${dim2("\u2503")} server: http://${host}:${port}${homePagePath}`);
2736
+ console.log(`${dim5("\u2503")} project: ${rootDir}`);
2737
+ console.log(`${dim5("\u2503")} server: http://${host}:${port}${homePagePath}`);
1350
2738
  if (testCmsEnabled(rootConfig)) {
1351
- console.log(`${dim2("\u2503")} cms: http://${host}:${port}/cms/`);
2739
+ console.log(`${dim5("\u2503")} cms: http://${host}:${port}/cms/`);
1352
2740
  }
1353
- console.log(`${dim2("\u2503")} mode: development`);
2741
+ console.log(`${dim5("\u2503")} mode: development`);
1354
2742
  console.log();
1355
2743
  currentServer = server.listen(port, host);
1356
2744
  const rootConfigDependencies = server.get(
1357
2745
  "rootConfigDependencies"
1358
2746
  );
1359
2747
  const dependencies = [
1360
- path7.resolve(rootDir, "root.config.ts"),
2748
+ path12.resolve(rootDir, "root.config.ts"),
1361
2749
  ...rootConfigDependencies
1362
2750
  ];
1363
2751
  viteServer.watcher.add(dependencies);
@@ -1365,7 +2753,7 @@ async function dev(rootProjectDir, options) {
1365
2753
  if (dependencies.includes(file)) {
1366
2754
  console.log(
1367
2755
  `
1368
- ${dim2("\u2503")} root.config.ts changed. restarting server...`
2756
+ ${dim5("\u2503")} root.config.ts changed. restarting server...`
1369
2757
  );
1370
2758
  await restart();
1371
2759
  }
@@ -1385,7 +2773,7 @@ ${dim2("\u2503")} root.config.ts changed. restarting server...`
1385
2773
  await start2();
1386
2774
  }
1387
2775
  async function createDevServer(options) {
1388
- const rootDir = path7.resolve(options?.rootDir || process.cwd());
2776
+ const rootDir = path12.resolve(options?.rootDir || process.cwd());
1389
2777
  const { rootConfig, dependencies } = await loadRootConfigWithDeps(rootDir, {
1390
2778
  command: "dev"
1391
2779
  });
@@ -1421,7 +2809,7 @@ async function createDevServer(options) {
1421
2809
  if (rootConfig.server?.headers) {
1422
2810
  server.use(headersMiddleware({ rootConfig }));
1423
2811
  }
1424
- const publicDir = path7.join(rootDir, "public");
2812
+ const publicDir = path12.join(rootDir, "public");
1425
2813
  if (await dirExists(publicDir)) {
1426
2814
  server.use(rootPublicDirMiddleware({ publicDir, viteServer }));
1427
2815
  }
@@ -1449,10 +2837,10 @@ async function createViteMiddleware(options) {
1449
2837
  return sourceFile.relPath;
1450
2838
  });
1451
2839
  const bundleScripts = [];
1452
- if (await isDirectory(path7.join(rootDir, "bundles"))) {
2840
+ if (await isDirectory(path12.join(rootDir, "bundles"))) {
1453
2841
  const bundleFiles = await glob4("bundles/*", { cwd: rootDir });
1454
2842
  bundleFiles.forEach((file) => {
1455
- const parts = path7.parse(file);
2843
+ const parts = path12.parse(file);
1456
2844
  if (isJsFile(parts.base)) {
1457
2845
  bundleScripts.push(file);
1458
2846
  }
@@ -1469,7 +2857,7 @@ async function createViteMiddleware(options) {
1469
2857
  optimizeDeps
1470
2858
  });
1471
2859
  function isInElementsDir(changedFilePath) {
1472
- const filePath = path7.resolve(changedFilePath);
2860
+ const filePath = path12.resolve(changedFilePath);
1473
2861
  const elementsDirs = getElementsDirs(rootConfig, pods);
1474
2862
  return elementsDirs.some((dirPath) => filePath.startsWith(dirPath));
1475
2863
  }
@@ -1488,7 +2876,7 @@ async function createViteMiddleware(options) {
1488
2876
  const viteMiddleware = async (req, res, next) => {
1489
2877
  try {
1490
2878
  req.viteServer = viteServer;
1491
- const renderModulePath = path7.resolve(__dirname3, "./render.js");
2879
+ const renderModulePath = path12.resolve(__dirname3, "./render.js");
1492
2880
  const render = await viteServer.ssrLoadModule(
1493
2881
  renderModulePath
1494
2882
  );
@@ -1512,7 +2900,7 @@ function rootPublicDirMiddleware(options) {
1512
2900
  handler = sirv(publicDir, sirvOptions);
1513
2901
  }, 1e3);
1514
2902
  function isInPublicDir(changedFilePath) {
1515
- const filePath = path7.resolve(changedFilePath);
2903
+ const filePath = path12.resolve(changedFilePath);
1516
2904
  return filePath.startsWith(publicDir);
1517
2905
  }
1518
2906
  const watcher = options.viteServer.watcher;
@@ -1544,7 +2932,7 @@ function rootDevServer404Middleware() {
1544
2932
  }
1545
2933
  if (req.renderer) {
1546
2934
  const url = req.path;
1547
- const ext = path7.extname(url);
2935
+ const ext = path12.extname(url);
1548
2936
  if (!ext) {
1549
2937
  const renderer = req.renderer;
1550
2938
  const data = await renderer.renderDevServer404(req);
@@ -1562,7 +2950,7 @@ function rootDevServer500Middleware() {
1562
2950
  console.error(String(err.stack || err));
1563
2951
  if (req.renderer) {
1564
2952
  const url = req.path;
1565
- const ext = path7.extname(url);
2953
+ const ext = path12.extname(url);
1566
2954
  if (!ext) {
1567
2955
  const renderer = req.renderer;
1568
2956
  const data = await renderer.renderDevServer500(req, err);
@@ -1587,8 +2975,8 @@ function debounce(fn, timeout) {
1587
2975
  }
1588
2976
 
1589
2977
  // src/cli/gae-deploy.ts
1590
- import { execSync } from "child_process";
1591
- import fs5 from "fs";
2978
+ import { execSync } from "node:child_process";
2979
+ import fs9 from "node:fs";
1592
2980
  import yaml from "js-yaml";
1593
2981
  async function gaeDeploy(appDir, options) {
1594
2982
  if (!appDir) {
@@ -1602,10 +2990,10 @@ async function gaeDeploy(appDir, options) {
1602
2990
  }
1603
2991
  process.chdir(appDir);
1604
2992
  const appYamlPath = "app.yaml";
1605
- if (!fs5.existsSync(appYamlPath)) {
2993
+ if (!fs9.existsSync(appYamlPath)) {
1606
2994
  throw new Error(`[gae-deplopy] Missing: ${appYamlPath}`);
1607
2995
  }
1608
- const appYaml = yaml.load(fs5.readFileSync(appYamlPath, "utf8"));
2996
+ const appYaml = yaml.load(fs9.readFileSync(appYamlPath, "utf8"));
1609
2997
  const service = appYaml.service;
1610
2998
  if (!service) {
1611
2999
  throw new Error(
@@ -1631,7 +3019,7 @@ async function gaeDeploy(appDir, options) {
1631
3019
  { stdio: "inherit" }
1632
3020
  );
1633
3021
  if (backupAppYaml) {
1634
- fs5.copyFileSync(backupAppYaml, "app.yaml");
3022
+ fs9.copyFileSync(backupAppYaml, "app.yaml");
1635
3023
  }
1636
3024
  if (options?.healthcheckUrl) {
1637
3025
  const healthcheckPassed = await testHealth(
@@ -1718,8 +3106,8 @@ function testIsEnvPlaceholder(envValue) {
1718
3106
  }
1719
3107
  function updateAppYamlEnv(appYamlPath, appYaml) {
1720
3108
  const backupAppYaml = `${appYamlPath}.bak`;
1721
- fs5.copyFileSync(appYamlPath, backupAppYaml);
1722
- let content = fs5.readFileSync(appYamlPath, "utf8");
3109
+ fs9.copyFileSync(appYamlPath, backupAppYaml);
3110
+ let content = fs9.readFileSync(appYamlPath, "utf8");
1723
3111
  const envVars = appYaml.env_variables;
1724
3112
  Object.entries(envVars).forEach(([envVar, envValue]) => {
1725
3113
  if (testIsEnvPlaceholder(envValue)) {
@@ -1732,7 +3120,7 @@ function updateAppYamlEnv(appYamlPath, appYaml) {
1732
3120
  }
1733
3121
  }
1734
3122
  });
1735
- fs5.writeFileSync(appYamlPath, content, "utf8");
3123
+ fs9.writeFileSync(appYamlPath, content, "utf8");
1736
3124
  return backupAppYaml;
1737
3125
  }
1738
3126
  function getTimestamp() {
@@ -1756,29 +3144,29 @@ function testManagedVersion(service, appInfo, prefix) {
1756
3144
  }
1757
3145
 
1758
3146
  // src/cli/preview.ts
1759
- import path8 from "path";
3147
+ import path13 from "node:path";
1760
3148
  import compression from "compression";
1761
3149
  import cookieParser2 from "cookie-parser";
1762
3150
  import { default as express2 } from "express";
1763
- import { dim as dim3 } from "kleur/colors";
3151
+ import { dim as dim6 } from "kleur/colors";
1764
3152
  import sirv2 from "sirv";
1765
3153
  async function preview(rootProjectDir, options) {
1766
3154
  process.env.NODE_ENV = "development";
1767
- const rootDir = path8.resolve(rootProjectDir || process.cwd());
3155
+ const rootDir = path13.resolve(rootProjectDir || process.cwd());
1768
3156
  const server = await createPreviewServer({ rootDir });
1769
3157
  const port = parseInt(process.env.PORT || "4007");
1770
3158
  const host = options?.host || "localhost";
1771
3159
  console.log();
1772
- console.log(`${dim3("\u2503")} project: ${rootDir}`);
1773
- console.log(`${dim3("\u2503")} server: http://${host}:${port}`);
1774
- console.log(`${dim3("\u2503")} mode: preview`);
3160
+ console.log(`${dim6("\u2503")} project: ${rootDir}`);
3161
+ console.log(`${dim6("\u2503")} server: http://${host}:${port}`);
3162
+ console.log(`${dim6("\u2503")} mode: preview`);
1775
3163
  console.log();
1776
3164
  server.listen(port, host);
1777
3165
  }
1778
3166
  async function createPreviewServer(options) {
1779
3167
  const rootDir = options.rootDir;
1780
3168
  const rootConfig = await loadBundledConfig(rootDir, { command: "preview" });
1781
- const distDir = path8.join(rootDir, "dist");
3169
+ const distDir = path13.join(rootDir, "dist");
1782
3170
  const server = express2();
1783
3171
  server.disable("x-powered-by");
1784
3172
  server.use(compression());
@@ -1804,7 +3192,7 @@ async function createPreviewServer(options) {
1804
3192
  if (rootConfig.server?.headers) {
1805
3193
  server.use(headersMiddleware({ rootConfig }));
1806
3194
  }
1807
- const publicDir = path8.join(distDir, "html");
3195
+ const publicDir = path13.join(distDir, "html");
1808
3196
  server.use(sirv2(publicDir, { dev: false }));
1809
3197
  server.use(trailingSlashMiddleware({ rootConfig }));
1810
3198
  server.use(rootPreviewServerMiddleware());
@@ -1823,20 +3211,20 @@ async function createPreviewServer(options) {
1823
3211
  }
1824
3212
  async function rootPreviewRendererMiddleware(options) {
1825
3213
  const { distDir, rootConfig } = options;
1826
- const render = await import(path8.join(distDir, "server/render.js"));
1827
- const manifestPath = path8.join(distDir, ".root/manifest.json");
1828
- if (!await fileExists(manifestPath)) {
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)) {
1829
3217
  throw new Error(
1830
- `could not find ${manifestPath}. run \`root build\` before \`root preview\`.`
3218
+ `could not find ${manifestPath2}. run \`root build\` before \`root preview\`.`
1831
3219
  );
1832
3220
  }
1833
- const elementGraphJsonPath = path8.join(distDir, ".root/elements.json");
3221
+ const elementGraphJsonPath = path13.join(distDir, ".root/elements.json");
1834
3222
  if (!await fileExists(elementGraphJsonPath)) {
1835
3223
  throw new Error(
1836
3224
  `could not find ${elementGraphJsonPath}. run \`root build\` before \`root preview\`.`
1837
3225
  );
1838
3226
  }
1839
- const rootManifest = await loadJson(manifestPath);
3227
+ const rootManifest = await loadJson(manifestPath2);
1840
3228
  const assetMap = BuildAssetMap.fromRootManifest(rootConfig, rootManifest);
1841
3229
  const elementGraphJson = await loadJson(elementGraphJsonPath);
1842
3230
  const elementGraph = ElementGraph.fromJson(elementGraphJson);
@@ -1870,7 +3258,7 @@ function rootPreviewServer404Middleware() {
1870
3258
  console.error(`\u2753 404 ${req.originalUrl}`);
1871
3259
  if (req.renderer) {
1872
3260
  const url = req.path;
1873
- const ext = path8.extname(url);
3261
+ const ext = path13.extname(url);
1874
3262
  if (!ext) {
1875
3263
  const renderer = req.renderer;
1876
3264
  const data = await renderer.render404({ currentPath: url });
@@ -1888,7 +3276,7 @@ function rootPreviewServer500Middleware() {
1888
3276
  console.error(String(err.stack || err));
1889
3277
  if (req.renderer) {
1890
3278
  const url = req.path;
1891
- const ext = path8.extname(url);
3279
+ const ext = path13.extname(url);
1892
3280
  if (!ext) {
1893
3281
  const renderer = req.renderer;
1894
3282
  const data = await renderer.renderDevServer500(req, err);
@@ -1902,29 +3290,29 @@ function rootPreviewServer500Middleware() {
1902
3290
  }
1903
3291
 
1904
3292
  // src/cli/start.ts
1905
- import path9 from "path";
3293
+ import path14 from "node:path";
1906
3294
  import compression2 from "compression";
1907
3295
  import cookieParser3 from "cookie-parser";
1908
3296
  import { default as express3 } from "express";
1909
- import { dim as dim4 } from "kleur/colors";
3297
+ import { dim as dim7 } from "kleur/colors";
1910
3298
  import sirv3 from "sirv";
1911
3299
  async function start(rootProjectDir, options) {
1912
3300
  process.env.NODE_ENV = "production";
1913
- const rootDir = path9.resolve(rootProjectDir || process.cwd());
3301
+ const rootDir = path14.resolve(rootProjectDir || process.cwd());
1914
3302
  const server = await createProdServer({ rootDir });
1915
3303
  const port = parseInt(process.env.PORT || "4007");
1916
3304
  const host = options?.host || "localhost";
1917
3305
  console.log();
1918
- console.log(`${dim4("\u2503")} project: ${rootDir}`);
1919
- console.log(`${dim4("\u2503")} server: http://${host}:${port}`);
1920
- console.log(`${dim4("\u2503")} mode: production`);
3306
+ console.log(`${dim7("\u2503")} project: ${rootDir}`);
3307
+ console.log(`${dim7("\u2503")} server: http://${host}:${port}`);
3308
+ console.log(`${dim7("\u2503")} mode: production`);
1921
3309
  console.log();
1922
3310
  server.listen(port, host);
1923
3311
  }
1924
3312
  async function createProdServer(options) {
1925
3313
  const rootDir = options.rootDir;
1926
3314
  const rootConfig = await loadBundledConfig(rootDir, { command: "start" });
1927
- const distDir = path9.join(rootDir, "dist");
3315
+ const distDir = path14.join(rootDir, "dist");
1928
3316
  const server = express3();
1929
3317
  server.disable("x-powered-by");
1930
3318
  server.use(compression2());
@@ -1950,7 +3338,7 @@ async function createProdServer(options) {
1950
3338
  if (rootConfig.server?.headers) {
1951
3339
  server.use(headersMiddleware({ rootConfig }));
1952
3340
  }
1953
- const publicDir = path9.join(distDir, "html");
3341
+ const publicDir = path14.join(distDir, "html");
1954
3342
  server.use(sirv3(publicDir, { dev: false }));
1955
3343
  server.use(trailingSlashMiddleware({ rootConfig }));
1956
3344
  server.use(rootProdServerMiddleware());
@@ -1969,20 +3357,20 @@ async function createProdServer(options) {
1969
3357
  }
1970
3358
  async function rootProdRendererMiddleware(options) {
1971
3359
  const { distDir, rootConfig } = options;
1972
- const render = await import(path9.join(distDir, "server/render.js"));
1973
- const manifestPath = path9.join(distDir, ".root/manifest.json");
1974
- if (!await fileExists(manifestPath)) {
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)) {
1975
3363
  throw new Error(
1976
- `could not find ${manifestPath}. run \`root build\` before \`root start\`.`
3364
+ `could not find ${manifestPath2}. run \`root build\` before \`root start\`.`
1977
3365
  );
1978
3366
  }
1979
- const elementGraphJsonPath = path9.join(distDir, ".root/elements.json");
3367
+ const elementGraphJsonPath = path14.join(distDir, ".root/elements.json");
1980
3368
  if (!await fileExists(elementGraphJsonPath)) {
1981
3369
  throw new Error(
1982
3370
  `could not find ${elementGraphJsonPath}. run \`root build\` before \`root start\`.`
1983
3371
  );
1984
3372
  }
1985
- const rootManifest = await loadJson(manifestPath);
3373
+ const rootManifest = await loadJson(manifestPath2);
1986
3374
  const assetMap = BuildAssetMap.fromRootManifest(rootConfig, rootManifest);
1987
3375
  const elementGraphJson = await loadJson(elementGraphJsonPath);
1988
3376
  const elementGraph = ElementGraph.fromJson(elementGraphJson);
@@ -2016,7 +3404,7 @@ function rootProdServer404Middleware() {
2016
3404
  console.error(`\u2753 404 ${req.originalUrl}`);
2017
3405
  if (req.renderer) {
2018
3406
  const url = req.path;
2019
- const ext = path9.extname(url);
3407
+ const ext = path14.extname(url);
2020
3408
  if (!ext) {
2021
3409
  const renderer = req.renderer;
2022
3410
  const data = await renderer.render404({ currentPath: url });
@@ -2034,7 +3422,7 @@ function rootProdServer500Middleware() {
2034
3422
  console.error(String(err.stack || err));
2035
3423
  if (req.renderer) {
2036
3424
  const url = req.path;
2037
- const ext = path9.extname(url);
3425
+ const ext = path14.extname(url);
2038
3426
  if (!ext) {
2039
3427
  const renderer = req.renderer;
2040
3428
  const data = await renderer.renderError(err);
@@ -2093,7 +3481,10 @@ var CliRunner = class {
2093
3481
  program.command("dev [path]").description("starts the server in development mode").option(
2094
3482
  "--host <host>",
2095
3483
  "network address the server should listen on, e.g. 127.0.0.1"
2096
- ).action(dev);
3484
+ ).action((rootProjectDir, options) => {
3485
+ options.version = this.version;
3486
+ dev(rootProjectDir, options);
3487
+ });
2097
3488
  program.command("gae-deploy <appdir>").description(
2098
3489
  "appengine deploy utility that can optionally run healthchecks before diverting traffic to the new version and clean up old versions"
2099
3490
  ).option("--project <project>", "GCP project id").option("--prefix <prefix>", "prefix to append the version").option(
@@ -2115,6 +3506,7 @@ var CliRunner = class {
2115
3506
  "--host <host>",
2116
3507
  "network address the server should listen on, e.g. 127.0.0.1"
2117
3508
  ).action(start);
3509
+ registerSecretsCommands(program);
2118
3510
  await program.parseAsync(argv);
2119
3511
  }
2120
3512
  };
@@ -2137,4 +3529,4 @@ export {
2137
3529
  createProdServer,
2138
3530
  CliRunner
2139
3531
  };
2140
- //# sourceMappingURL=chunk-GSR74KEI.js.map
3532
+ //# sourceMappingURL=chunk-URG2INLL.js.map