@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.
- package/dist/{chunk-FQBA5QSG.js → chunk-3Z5TNZEP.js} +3 -3
- package/dist/chunk-3Z5TNZEP.js.map +7 -0
- package/dist/{chunk-JCRXBB26.js → chunk-6NBKAR5Y.js} +2 -2
- package/dist/chunk-6NBKAR5Y.js.map +7 -0
- package/dist/chunk-6P3B7ZXL.js +1 -1
- package/dist/chunk-6P3B7ZXL.js.map +7 -1
- package/dist/{chunk-PVBPP5LN.js → chunk-7PSEEE6C.js} +10 -10
- package/dist/chunk-7PSEEE6C.js.map +7 -0
- package/dist/{chunk-WAZTR3DX.js → chunk-KW7KTYGC.js} +2 -2
- package/dist/chunk-KW7KTYGC.js.map +7 -0
- package/dist/{chunk-GSR74KEI.js → chunk-URG2INLL.js} +1477 -85
- package/dist/chunk-URG2INLL.js.map +7 -0
- package/dist/{chunk-4PO5HYET.js → chunk-X2C2MEJ2.js} +39 -4
- package/dist/chunk-X2C2MEJ2.js.map +7 -0
- package/dist/chunk-XSNCF7WU.js +1 -1
- package/dist/chunk-XSNCF7WU.js.map +7 -1
- package/dist/cli/build.d.ts +7 -0
- package/dist/cli/cli.d.ts +12 -0
- package/dist/cli/codegen.d.ts +5 -0
- package/dist/cli/create-package.d.ts +14 -0
- package/dist/cli/dev.d.ts +10 -0
- package/dist/cli/gae-deploy.d.ts +18 -0
- package/dist/cli/preview.d.ts +8 -0
- package/dist/cli/secrets.d.ts +9 -0
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/startup/check-version.d.ts +17 -0
- package/dist/cli/startup/startup-tasks.d.ts +39 -0
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +7 -1
- package/dist/core/components/Body.d.ts +23 -0
- package/dist/core/components/Head.d.ts +18 -0
- package/dist/core/components/Html.d.ts +24 -0
- package/dist/core/components/Script.d.ts +16 -0
- package/dist/core/config.d.ts +285 -0
- package/dist/core/core.d.ts +14 -0
- package/dist/core/hooks/useI18nContext.d.ts +11 -0
- package/dist/core/hooks/useRequestContext.d.ts +36 -0
- package/dist/core/hooks/useStringParams.d.ts +34 -0
- package/dist/core/hooks/useTranslations.d.ts +20 -0
- package/dist/core/hooks/useTranslationsMiddleware.d.ts +25 -0
- package/dist/core/pages/DevErrorPage.d.ts +9 -0
- package/dist/core/pages/DevNotFoundPage.d.ts +10 -0
- package/dist/core/pages/ErrorPage.d.ts +9 -0
- package/dist/core/plugin.d.ts +80 -0
- package/dist/core/pod.d.ts +54 -0
- package/dist/core/types.d.ts +228 -0
- package/dist/core.js +3 -3
- package/dist/core.js.map +7 -1
- package/dist/functions/functions.d.ts +1 -0
- package/dist/functions/server.d.ts +10 -0
- package/dist/functions.js +5 -5
- package/dist/functions.js.map +7 -1
- package/dist/jsx/jsx-dev-runtime.d.ts +10 -1
- package/dist/jsx/jsx-dev-runtime.js +2 -2
- package/dist/jsx/jsx-dev-runtime.js.map +7 -1
- package/dist/{jsx-C8cJO55i.d.ts → jsx/jsx-render.d.ts} +3 -6
- package/dist/jsx/jsx-runtime.d.ts +120 -1
- package/dist/jsx/jsx-runtime.js +2 -2
- package/dist/jsx/jsx-runtime.js.map +7 -1
- package/dist/jsx/jsx.d.ts +37 -0
- package/dist/{jsx-dev-runtime-DHf9jiFH.d.ts → jsx/types.d.ts} +8 -124
- package/dist/jsx.js +3 -3
- package/dist/jsx.js.map +7 -1
- package/dist/middleware/common.d.ts +22 -0
- package/dist/middleware/hooks.d.ts +8 -0
- package/dist/middleware/middleware.d.ts +3 -0
- package/dist/middleware/multipart.d.ts +11 -0
- package/dist/middleware/redirects.d.ts +10 -0
- package/dist/middleware/session.d.ts +24 -0
- package/dist/middleware.js +2 -2
- package/dist/middleware.js.map +7 -1
- package/dist/node/element-graph.d.ts +51 -0
- package/dist/node/load-config.d.ts +17 -0
- package/dist/node/monorepo.d.ts +14 -0
- package/dist/node/node.d.ts +4 -0
- package/dist/node/pod-collector.d.ts +33 -0
- package/dist/node/pods-vite-plugin.d.ts +6 -0
- package/dist/node/vite-plugin-preact-alias.d.ts +10 -0
- package/dist/node/vite-plugin-prune-empty-chunks.d.ts +27 -0
- package/dist/node/vite-plugin-root-jsx-virtual.d.ts +16 -0
- package/dist/node/vite.d.ts +18 -0
- package/dist/node.js +2 -2
- package/dist/node.js.map +7 -1
- package/dist/render/accept-language.d.ts +8 -0
- package/dist/render/asset-map/asset-map.d.ts +25 -0
- package/dist/render/asset-map/build-asset-map.d.ts +47 -0
- package/dist/render/asset-map/dev-asset-map.d.ts +26 -0
- package/dist/render/countrie-language.d.ts +13 -0
- package/dist/render/html-minify.d.ts +3 -0
- package/dist/render/html-pretty.d.ts +3 -0
- package/dist/render/i18n-fallbacks.d.ts +11 -0
- package/dist/render/render.d.ts +91 -0
- package/dist/render/route-trie.d.ts +44 -0
- package/dist/render/router.d.ts +18 -0
- package/dist/render.js +9 -9
- package/dist/render.js.map +7 -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/batch.d.ts +1 -0
- package/dist/utils/elements.d.ts +12 -0
- package/dist/utils/fsutils.d.ts +24 -0
- package/dist/utils/i18n.d.ts +8 -0
- package/dist/utils/ports.d.ts +8 -0
- package/dist/utils/rand.d.ts +10 -0
- package/dist/utils/url-path-params.d.ts +2 -0
- package/package.json +14 -15
- package/dist/chunk-4PO5HYET.js.map +0 -1
- package/dist/chunk-FQBA5QSG.js.map +0 -1
- package/dist/chunk-GSR74KEI.js.map +0 -1
- package/dist/chunk-JCRXBB26.js.map +0 -1
- package/dist/chunk-PVBPP5LN.js.map +0 -1
- package/dist/chunk-WAZTR3DX.js.map +0 -1
- package/dist/cli.d.ts +0 -60
- package/dist/core.d.ts +0 -214
- package/dist/functions.d.ts +0 -14
- package/dist/jsx.d.ts +0 -2
- package/dist/middleware.d.ts +0 -43
- package/dist/node.d.ts +0 -91
- package/dist/render.d.ts +0 -8
- 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-
|
|
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-
|
|
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(`${
|
|
360
|
-
console.log(`${
|
|
361
|
-
console.log(`${
|
|
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}${
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(`${
|
|
1349
|
-
console.log(`${
|
|
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(`${
|
|
2739
|
+
console.log(`${dim5("\u2503")} cms: http://${host}:${port}/cms/`);
|
|
1352
2740
|
}
|
|
1353
|
-
console.log(`${
|
|
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
|
-
|
|
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
|
-
${
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 (!
|
|
2993
|
+
if (!fs9.existsSync(appYamlPath)) {
|
|
1606
2994
|
throw new Error(`[gae-deplopy] Missing: ${appYamlPath}`);
|
|
1607
2995
|
}
|
|
1608
|
-
const appYaml = yaml.load(
|
|
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
|
-
|
|
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
|
-
|
|
1722
|
-
let content =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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(`${
|
|
1773
|
-
console.log(`${
|
|
1774
|
-
console.log(`${
|
|
3160
|
+
console.log(`${dim6("\u2503")} project: ${rootDir}`);
|
|
3161
|
+
console.log(`${dim6("\u2503")} server: http://${host}:${port}`);
|
|
3162
|
+
console.log(`${dim6("\u2503")} mode: preview`);
|
|
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 =
|
|
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 =
|
|
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(
|
|
1827
|
-
const
|
|
1828
|
-
if (!await fileExists(
|
|
3214
|
+
const render = await import(path13.join(distDir, "server/render.js"));
|
|
3215
|
+
const manifestPath2 = path13.join(distDir, ".root/manifest.json");
|
|
3216
|
+
if (!await fileExists(manifestPath2)) {
|
|
1829
3217
|
throw new Error(
|
|
1830
|
-
`could not find ${
|
|
3218
|
+
`could not find ${manifestPath2}. run \`root build\` before \`root preview\`.`
|
|
1831
3219
|
);
|
|
1832
3220
|
}
|
|
1833
|
-
const elementGraphJsonPath =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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(`${
|
|
1919
|
-
console.log(`${
|
|
1920
|
-
console.log(`${
|
|
3306
|
+
console.log(`${dim7("\u2503")} project: ${rootDir}`);
|
|
3307
|
+
console.log(`${dim7("\u2503")} server: http://${host}:${port}`);
|
|
3308
|
+
console.log(`${dim7("\u2503")} mode: production`);
|
|
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 =
|
|
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 =
|
|
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(
|
|
1973
|
-
const
|
|
1974
|
-
if (!await fileExists(
|
|
3360
|
+
const render = await import(path14.join(distDir, "server/render.js"));
|
|
3361
|
+
const manifestPath2 = path14.join(distDir, ".root/manifest.json");
|
|
3362
|
+
if (!await fileExists(manifestPath2)) {
|
|
1975
3363
|
throw new Error(
|
|
1976
|
-
`could not find ${
|
|
3364
|
+
`could not find ${manifestPath2}. run \`root build\` before \`root start\`.`
|
|
1977
3365
|
);
|
|
1978
3366
|
}
|
|
1979
|
-
const elementGraphJsonPath =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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-
|
|
3532
|
+
//# sourceMappingURL=chunk-URG2INLL.js.map
|