@treeseed/sdk 0.4.7 → 0.4.8
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/operations/providers/default.js +1 -0
- package/dist/operations/services/config-runtime.d.ts +121 -26
- package/dist/operations/services/config-runtime.js +330 -196
- package/dist/operations/services/export-runtime.d.ts +18 -0
- package/dist/operations/services/export-runtime.js +136 -0
- package/dist/operations-registry.js +1 -0
- package/dist/operations-types.d.ts +1 -1
- package/dist/platform/book-export.d.ts +78 -0
- package/dist/platform/book-export.js +449 -0
- package/dist/platform/contracts.d.ts +5 -0
- package/dist/platform/deploy-config.d.ts +2 -0
- package/dist/platform/deploy-config.js +30 -2
- package/dist/platform/env.yaml +5 -0
- package/dist/platform/environment.d.ts +10 -1
- package/dist/platform/environment.js +82 -6
- package/dist/scripts/aggregate-book.js +13 -118
- package/dist/scripts/config-treeseed.js +18 -27
- package/dist/workflow/operations.d.ts +59 -15
- package/dist/workflow/operations.js +61 -81
- package/dist/workflow-support.d.ts +2 -1
- package/dist/workflow-support.js +14 -6
- package/dist/workflow.d.ts +11 -1
- package/dist/workflow.js +6 -0
- package/package.json +6 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
7
7
|
import {
|
|
8
8
|
getTreeseedEnvironmentSuggestedValues,
|
|
9
|
+
isTreeseedEnvironmentEntryRequired,
|
|
9
10
|
resolveTreeseedEnvironmentRegistry,
|
|
10
11
|
TREESEED_ENVIRONMENT_SCOPES,
|
|
11
12
|
validateTreeseedEnvironmentValues
|
|
@@ -14,6 +15,7 @@ import { loadTreeseedManifest } from "../../platform/tenant-config.js";
|
|
|
14
15
|
import {
|
|
15
16
|
createPersistentDeployTarget,
|
|
16
17
|
ensureGeneratedWranglerConfig,
|
|
18
|
+
loadDeployState,
|
|
17
19
|
markManagedServicesInitialized,
|
|
18
20
|
markDeploymentInitialized,
|
|
19
21
|
provisionCloudflareResources,
|
|
@@ -213,6 +215,10 @@ function createDefaultTreeseedMachineConfig({ tenantRoot, deployConfig, tenantCo
|
|
|
213
215
|
remote: createDefaultRemoteSettings(),
|
|
214
216
|
services: createDefaultServiceSettings()
|
|
215
217
|
},
|
|
218
|
+
shared: {
|
|
219
|
+
values: {},
|
|
220
|
+
secrets: {}
|
|
221
|
+
},
|
|
216
222
|
environments: Object.fromEntries(
|
|
217
223
|
TREESEED_ENVIRONMENT_SCOPES.map((scope) => [
|
|
218
224
|
scope,
|
|
@@ -284,7 +290,12 @@ function decryptValue(payload, key) {
|
|
|
284
290
|
return decrypted.toString("utf8");
|
|
285
291
|
}
|
|
286
292
|
function decryptMachineConfigSecrets(config, key) {
|
|
287
|
-
const secrets = {
|
|
293
|
+
const secrets = {
|
|
294
|
+
shared: {}
|
|
295
|
+
};
|
|
296
|
+
for (const [entryId, payload] of Object.entries(config.shared?.secrets ?? {})) {
|
|
297
|
+
secrets.shared[entryId] = decryptValue(payload, key);
|
|
298
|
+
}
|
|
288
299
|
for (const scope of TREESEED_ENVIRONMENT_SCOPES) {
|
|
289
300
|
secrets[scope] = {};
|
|
290
301
|
for (const [entryId, payload] of Object.entries(config.environments?.[scope]?.secrets ?? {})) {
|
|
@@ -294,6 +305,9 @@ function decryptMachineConfigSecrets(config, key) {
|
|
|
294
305
|
return secrets;
|
|
295
306
|
}
|
|
296
307
|
function applyMachineConfigSecrets(config, secrets, key) {
|
|
308
|
+
for (const [entryId, value] of Object.entries(secrets.shared ?? {})) {
|
|
309
|
+
config.shared.secrets[entryId] = encryptValue(value, key);
|
|
310
|
+
}
|
|
297
311
|
for (const scope of TREESEED_ENVIRONMENT_SCOPES) {
|
|
298
312
|
const scoped = config.environments?.[scope];
|
|
299
313
|
if (!scoped) {
|
|
@@ -507,6 +521,16 @@ function loadTreeseedMachineConfig(tenantRoot) {
|
|
|
507
521
|
...parsed.settings?.services ?? {}
|
|
508
522
|
})
|
|
509
523
|
},
|
|
524
|
+
shared: {
|
|
525
|
+
values: {
|
|
526
|
+
...defaults.shared?.values ?? {},
|
|
527
|
+
...parsed.shared?.values ?? {}
|
|
528
|
+
},
|
|
529
|
+
secrets: {
|
|
530
|
+
...defaults.shared?.secrets ?? {},
|
|
531
|
+
...parsed.shared?.secrets ?? {}
|
|
532
|
+
}
|
|
533
|
+
},
|
|
510
534
|
environments: Object.fromEntries(
|
|
511
535
|
TREESEED_ENVIRONMENT_SCOPES.map((scope) => [
|
|
512
536
|
scope,
|
|
@@ -613,34 +637,133 @@ function ensureTreeseedGitignoreEntries(tenantRoot) {
|
|
|
613
637
|
}
|
|
614
638
|
return gitignorePath;
|
|
615
639
|
}
|
|
640
|
+
function dedupeRepairActions(actions) {
|
|
641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
642
|
+
return actions.filter((action) => {
|
|
643
|
+
if (seen.has(action.id)) {
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
seen.add(action.id);
|
|
647
|
+
return true;
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
function applyTreeseedSafeRepairs(tenantRoot) {
|
|
651
|
+
const actions = [];
|
|
652
|
+
ensureTreeseedGitignoreEntries(tenantRoot);
|
|
653
|
+
actions.push({ id: "gitignore", detail: "Ensured Treeseed gitignore entries are present." });
|
|
654
|
+
const envLocalPath = resolve(tenantRoot, ".env.local");
|
|
655
|
+
const envLocalExamplePath = resolve(tenantRoot, ".env.local.example");
|
|
656
|
+
if (!existsSync(envLocalPath) && existsSync(envLocalExamplePath)) {
|
|
657
|
+
copyFileSync(envLocalExamplePath, envLocalPath);
|
|
658
|
+
actions.push({ id: "env-local", detail: "Created .env.local from .env.local.example." });
|
|
659
|
+
}
|
|
660
|
+
const deployConfig = loadTenantDeployConfig(tenantRoot);
|
|
661
|
+
const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
662
|
+
if (!existsSync(configPath)) {
|
|
663
|
+
const machineConfig2 = createDefaultTreeseedMachineConfig({
|
|
664
|
+
tenantRoot,
|
|
665
|
+
deployConfig,
|
|
666
|
+
tenantConfig: void 0
|
|
667
|
+
});
|
|
668
|
+
writeTreeseedMachineConfig(tenantRoot, machineConfig2);
|
|
669
|
+
actions.push({ id: "machine-config", detail: "Created the default Treeseed machine config." });
|
|
670
|
+
}
|
|
671
|
+
resolveTreeseedMachineEnvironmentValues(tenantRoot, "local");
|
|
672
|
+
actions.push({ id: "machine-key", detail: "Ensured the Treeseed machine key exists." });
|
|
673
|
+
const machineConfig = loadTreeseedMachineConfig(tenantRoot);
|
|
674
|
+
writeTreeseedMachineConfig(tenantRoot, machineConfig);
|
|
675
|
+
writeTreeseedLocalEnvironmentFiles(tenantRoot);
|
|
676
|
+
actions.push({ id: "local-env", detail: "Regenerated .env.local and .dev.vars from the current machine config." });
|
|
677
|
+
for (const scope of TREESEED_ENVIRONMENT_SCOPES) {
|
|
678
|
+
const target = createPersistentDeployTarget(scope);
|
|
679
|
+
const state = loadDeployState(tenantRoot, deployConfig, { target });
|
|
680
|
+
if (state.readiness?.initialized || scope === "local") {
|
|
681
|
+
ensureGeneratedWranglerConfig(tenantRoot, { target });
|
|
682
|
+
actions.push({ id: `wrangler-${scope}`, detail: `Regenerated the ${scope} generated Wrangler config.` });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return dedupeRepairActions(actions);
|
|
686
|
+
}
|
|
687
|
+
function decryptMachineEnvironmentBucket(tenantRoot, config, key, bucket) {
|
|
688
|
+
const values = {
|
|
689
|
+
...bucket?.values ?? {}
|
|
690
|
+
};
|
|
691
|
+
for (const [entryId, payload] of Object.entries(bucket?.secrets ?? {})) {
|
|
692
|
+
values[entryId] = decryptValueWithMachineKey(tenantRoot, payload, key);
|
|
693
|
+
}
|
|
694
|
+
return values;
|
|
695
|
+
}
|
|
696
|
+
function resolveEntryValueFromBuckets(entry, entryId, scope, bucketValuesByScope) {
|
|
697
|
+
if (!entry) {
|
|
698
|
+
return bucketValuesByScope[scope]?.[entryId] ?? bucketValuesByScope.shared?.[entryId] ?? "";
|
|
699
|
+
}
|
|
700
|
+
if (entry?.storage === "shared") {
|
|
701
|
+
const sharedValue = bucketValuesByScope.shared?.[entryId];
|
|
702
|
+
if (typeof sharedValue === "string" && sharedValue.length > 0) {
|
|
703
|
+
return sharedValue;
|
|
704
|
+
}
|
|
705
|
+
const searchScopes = [scope, ...TREESEED_ENVIRONMENT_SCOPES.filter((candidate) => candidate !== scope)];
|
|
706
|
+
for (const candidateScope of searchScopes) {
|
|
707
|
+
const candidateValue = bucketValuesByScope[candidateScope]?.[entryId];
|
|
708
|
+
if (typeof candidateValue === "string" && candidateValue.length > 0) {
|
|
709
|
+
return candidateValue;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return "";
|
|
713
|
+
}
|
|
714
|
+
return bucketValuesByScope[scope]?.[entryId] ?? "";
|
|
715
|
+
}
|
|
616
716
|
function resolveTreeseedMachineEnvironmentValues(tenantRoot, scope) {
|
|
617
717
|
const key = loadMachineKey(tenantRoot);
|
|
618
718
|
const config = loadTreeseedMachineConfig(tenantRoot);
|
|
619
|
-
const
|
|
620
|
-
|
|
719
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
720
|
+
const bucketValuesByScope = {
|
|
721
|
+
shared: decryptMachineEnvironmentBucket(tenantRoot, config, key, config.shared),
|
|
722
|
+
...Object.fromEntries(
|
|
723
|
+
TREESEED_ENVIRONMENT_SCOPES.map((candidateScope) => [
|
|
724
|
+
candidateScope,
|
|
725
|
+
decryptMachineEnvironmentBucket(tenantRoot, config, key, config.environments?.[candidateScope])
|
|
726
|
+
])
|
|
727
|
+
)
|
|
621
728
|
};
|
|
622
|
-
|
|
623
|
-
|
|
729
|
+
const entryById = new Map(registry.entries.map((entry) => [entry.id, entry]));
|
|
730
|
+
const values = {};
|
|
731
|
+
const knownKeys = /* @__PURE__ */ new Set([
|
|
732
|
+
...Object.keys(bucketValuesByScope.shared ?? {}),
|
|
733
|
+
...Object.keys(bucketValuesByScope[scope] ?? {}),
|
|
734
|
+
...registry.entries.map((entry) => entry.id)
|
|
735
|
+
]);
|
|
736
|
+
for (const entryId of knownKeys) {
|
|
737
|
+
const resolved = resolveEntryValueFromBuckets(entryById.get(entryId), entryId, scope, bucketValuesByScope);
|
|
738
|
+
if (typeof resolved === "string" && resolved.length > 0) {
|
|
739
|
+
values[entryId] = resolved;
|
|
740
|
+
}
|
|
624
741
|
}
|
|
625
742
|
return values;
|
|
626
743
|
}
|
|
627
744
|
function setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, value) {
|
|
628
745
|
const key = loadMachineKey(tenantRoot);
|
|
629
746
|
const config = loadTreeseedMachineConfig(tenantRoot);
|
|
630
|
-
const
|
|
747
|
+
const target = entry.storage === "shared" ? config.shared : config.environments[scope];
|
|
748
|
+
if (entry.storage === "shared") {
|
|
749
|
+
for (const candidateScope of TREESEED_ENVIRONMENT_SCOPES) {
|
|
750
|
+
delete config.environments[candidateScope].values[entry.id];
|
|
751
|
+
delete config.environments[candidateScope].secrets[entry.id];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
631
754
|
if (entry.sensitivity === "secret") {
|
|
632
|
-
delete
|
|
755
|
+
delete target.values[entry.id];
|
|
633
756
|
if (value) {
|
|
634
|
-
|
|
757
|
+
target.secrets[entry.id] = encryptValue(value, key);
|
|
635
758
|
} else {
|
|
636
|
-
delete
|
|
759
|
+
delete target.secrets[entry.id];
|
|
637
760
|
}
|
|
638
761
|
} else {
|
|
639
|
-
delete
|
|
762
|
+
delete target.secrets[entry.id];
|
|
640
763
|
if (value) {
|
|
641
|
-
|
|
764
|
+
target.values[entry.id] = value;
|
|
642
765
|
} else {
|
|
643
|
-
delete
|
|
766
|
+
delete target.values[entry.id];
|
|
644
767
|
}
|
|
645
768
|
}
|
|
646
769
|
writeTreeseedMachineConfig(tenantRoot, config);
|
|
@@ -687,7 +810,7 @@ function formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env = proces
|
|
|
687
810
|
formatConfigSectionTitle(`Resolved environment values for ${scope}`),
|
|
688
811
|
revealSecrets ? "Secrets are shown because --show-secrets was provided." : "Secret values are masked. Re-run with --show-secrets to print full values."
|
|
689
812
|
];
|
|
690
|
-
for (const entry of registry
|
|
813
|
+
for (const entry of listRelevantTreeseedConfigEntries(registry, scope)) {
|
|
691
814
|
const value = values[entry.id];
|
|
692
815
|
const displayValue = typeof value === "string" && value.length > 0 ? entry.sensitivity === "secret" && !revealSecrets ? maskValue(value) : value : "(unset)";
|
|
693
816
|
lines.push(`${entry.id}=${displayValue} (${sources[entry.id] ?? "unset"})`);
|
|
@@ -750,10 +873,11 @@ function writeTreeseedLocalEnvironmentFiles(tenantRoot) {
|
|
|
750
873
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
751
874
|
const scope = "local";
|
|
752
875
|
const values = resolveTreeseedMachineEnvironmentValues(tenantRoot, scope);
|
|
753
|
-
const
|
|
876
|
+
const orderedEntries = listRelevantTreeseedConfigEntries(registry, scope);
|
|
877
|
+
const envEntries = orderedEntries.filter(
|
|
754
878
|
(entry) => entry.scopes.includes(scope) && entry.targets.includes("local-file")
|
|
755
879
|
);
|
|
756
|
-
const devVarsEntries =
|
|
880
|
+
const devVarsEntries = orderedEntries.filter(
|
|
757
881
|
(entry) => entry.scopes.includes(scope) && entry.targets.includes("wrangler-dev-vars")
|
|
758
882
|
);
|
|
759
883
|
writeFileSync(resolve(tenantRoot, ".env.local"), `${renderEnvEntries(envEntries, values)}
|
|
@@ -1113,20 +1237,6 @@ function formatConfigSectionTitle(label) {
|
|
|
1113
1237
|
return colorize(`
|
|
1114
1238
|
== ${label}`, "1;36");
|
|
1115
1239
|
}
|
|
1116
|
-
function formatConfigFieldPrompt(entry, currentValue) {
|
|
1117
|
-
const current = entry.sensitivity === "secret" ? maskValue(currentValue) : currentValue ?? "(unset)";
|
|
1118
|
-
return [
|
|
1119
|
-
colorize(`
|
|
1120
|
-
-- ${entry.label}`, "1;37"),
|
|
1121
|
-
colorize(` ${entry.id}`, "36"),
|
|
1122
|
-
` ${entry.description}`,
|
|
1123
|
-
` How to get it: ${entry.howToGet}`,
|
|
1124
|
-
` Used for: ${entry.purposes.join(", ")}`,
|
|
1125
|
-
` Targets: ${entry.targets.join(", ")}`,
|
|
1126
|
-
` Current: ${current}`,
|
|
1127
|
-
colorize(' Enter value, press Enter to keep current/default, or "-" to clear', "90")
|
|
1128
|
-
].join("\n");
|
|
1129
|
-
}
|
|
1130
1240
|
function hasConfigValue(values, key) {
|
|
1131
1241
|
return typeof values[key] === "string" && values[key].trim().length > 0;
|
|
1132
1242
|
}
|
|
@@ -1147,165 +1257,165 @@ function createConfigAuthStatus(values) {
|
|
|
1147
1257
|
}
|
|
1148
1258
|
};
|
|
1149
1259
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1260
|
+
const CONFIG_GROUP_ORDER = ["auth", "cloudflare", "local-development", "forms", "smtp"];
|
|
1261
|
+
function configGroupRank(group) {
|
|
1262
|
+
const index = CONFIG_GROUP_ORDER.indexOf(group);
|
|
1263
|
+
return index === -1 ? CONFIG_GROUP_ORDER.length : index;
|
|
1264
|
+
}
|
|
1265
|
+
function listRelevantTreeseedConfigEntries(registry, scope) {
|
|
1266
|
+
return registry.entries.filter(
|
|
1267
|
+
(entry) => entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config"))
|
|
1268
|
+
).sort((left, right) => {
|
|
1269
|
+
const leftRequired = isTreeseedEnvironmentEntryRequired(left, registry.context, scope, "config");
|
|
1270
|
+
const rightRequired = isTreeseedEnvironmentEntryRequired(right, registry.context, scope, "config");
|
|
1271
|
+
if (leftRequired !== rightRequired) {
|
|
1272
|
+
return leftRequired ? -1 : 1;
|
|
1273
|
+
}
|
|
1274
|
+
if (left.purposes.length !== right.purposes.length) {
|
|
1275
|
+
return right.purposes.length - left.purposes.length;
|
|
1276
|
+
}
|
|
1277
|
+
if (configGroupRank(left.group) !== configGroupRank(right.group)) {
|
|
1278
|
+
return configGroupRank(left.group) - configGroupRank(right.group);
|
|
1279
|
+
}
|
|
1280
|
+
return left.label.localeCompare(right.label);
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
function buildConfigEntrySnapshot(scope, entry, currentValue, suggestedValue) {
|
|
1284
|
+
return {
|
|
1285
|
+
id: entry.id,
|
|
1286
|
+
label: entry.label,
|
|
1287
|
+
group: entry.group,
|
|
1288
|
+
description: entry.description,
|
|
1289
|
+
howToGet: entry.howToGet,
|
|
1290
|
+
sensitivity: entry.sensitivity,
|
|
1291
|
+
targets: [...entry.targets],
|
|
1292
|
+
purposes: [...entry.purposes],
|
|
1293
|
+
storage: entry.storage ?? "scoped",
|
|
1294
|
+
scope,
|
|
1295
|
+
sharedScopes: entry.storage === "shared" ? [...entry.scopes] : [scope],
|
|
1296
|
+
required: false,
|
|
1297
|
+
currentValue,
|
|
1298
|
+
suggestedValue,
|
|
1299
|
+
effectiveValue: currentValue || suggestedValue || ""
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
function collectTreeseedConfigContext({
|
|
1303
|
+
tenantRoot,
|
|
1304
|
+
scopes = [...TREESEED_ENVIRONMENT_SCOPES],
|
|
1305
|
+
env = process.env
|
|
1306
|
+
}) {
|
|
1307
|
+
ensureTreeseedGitignoreEntries(tenantRoot);
|
|
1308
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1309
|
+
const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
1310
|
+
const valuesByScope = Object.fromEntries(
|
|
1311
|
+
scopes.map((scope) => [scope, collectTreeseedConfigSeedValues(tenantRoot, scope, env)])
|
|
1312
|
+
);
|
|
1313
|
+
const suggestedValuesByScope = Object.fromEntries(
|
|
1314
|
+
scopes.map((scope) => [scope, getTreeseedEnvironmentSuggestedValues({
|
|
1315
|
+
scope,
|
|
1316
|
+
purpose: "config",
|
|
1317
|
+
deployConfig: registry.context.deployConfig,
|
|
1318
|
+
tenantConfig: registry.context.tenantConfig,
|
|
1319
|
+
plugins: registry.context.plugins,
|
|
1320
|
+
values: valuesByScope[scope]
|
|
1321
|
+
})])
|
|
1322
|
+
);
|
|
1323
|
+
const authStatusByScope = Object.fromEntries(
|
|
1324
|
+
scopes.map((scope) => [scope, createConfigAuthStatus(valuesByScope[scope])])
|
|
1325
|
+
);
|
|
1326
|
+
const validationByScope = Object.fromEntries(
|
|
1327
|
+
scopes.map((scope) => [scope, validateTreeseedEnvironmentValues({
|
|
1328
|
+
values: {
|
|
1329
|
+
...suggestedValuesByScope[scope],
|
|
1330
|
+
...valuesByScope[scope]
|
|
1174
1331
|
},
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1332
|
+
scope,
|
|
1333
|
+
purpose: "config",
|
|
1334
|
+
deployConfig: registry.context.deployConfig,
|
|
1335
|
+
tenantConfig: registry.context.tenantConfig,
|
|
1336
|
+
plugins: registry.context.plugins
|
|
1337
|
+
})])
|
|
1338
|
+
);
|
|
1339
|
+
const entriesByScope = Object.fromEntries(
|
|
1340
|
+
scopes.map((scope) => [scope, listRelevantTreeseedConfigEntries(registry, scope).map((entry) => ({
|
|
1341
|
+
...buildConfigEntrySnapshot(
|
|
1342
|
+
scope,
|
|
1343
|
+
entry,
|
|
1344
|
+
valuesByScope[scope][entry.id] ?? "",
|
|
1345
|
+
suggestedValuesByScope[scope][entry.id] ?? ""
|
|
1346
|
+
),
|
|
1347
|
+
required: isTreeseedEnvironmentEntryRequired(entry, registry.context, scope, "config")
|
|
1348
|
+
}))])
|
|
1349
|
+
);
|
|
1350
|
+
return {
|
|
1351
|
+
tenantRoot,
|
|
1352
|
+
scopes,
|
|
1353
|
+
project: {
|
|
1354
|
+
name: registry.context.deployConfig.name,
|
|
1355
|
+
slug: registry.context.deployConfig.slug,
|
|
1356
|
+
siteUrl: registry.context.deployConfig.siteUrl
|
|
1357
|
+
},
|
|
1358
|
+
configPath,
|
|
1359
|
+
keyPath,
|
|
1360
|
+
entriesByScope,
|
|
1361
|
+
valuesByScope,
|
|
1362
|
+
suggestedValuesByScope,
|
|
1363
|
+
authStatusByScope,
|
|
1364
|
+
validationByScope,
|
|
1365
|
+
registry
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
function applyTreeseedConfigValues({
|
|
1369
|
+
tenantRoot,
|
|
1370
|
+
updates,
|
|
1371
|
+
writeLocalFiles = true,
|
|
1372
|
+
applyLocalEnvironment = true
|
|
1373
|
+
}) {
|
|
1374
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1375
|
+
const entryById = new Map(registry.entries.map((entry) => [entry.id, entry]));
|
|
1376
|
+
const applied = [];
|
|
1377
|
+
for (const update of updates) {
|
|
1378
|
+
const entry = entryById.get(update.entryId);
|
|
1379
|
+
if (!entry) {
|
|
1380
|
+
throw new Error(`Unknown Treeseed config entry "${update.entryId}".`);
|
|
1381
|
+
}
|
|
1382
|
+
if (!entry.scopes.includes(update.scope)) {
|
|
1383
|
+
throw new Error(`Treeseed config entry "${update.entryId}" does not apply to ${update.scope}.`);
|
|
1384
|
+
}
|
|
1385
|
+
setTreeseedMachineEnvironmentValue(tenantRoot, update.scope, entry, update.value);
|
|
1386
|
+
applied.push({
|
|
1387
|
+
scope: entry.storage === "shared" ? "shared" : update.scope,
|
|
1388
|
+
id: entry.id,
|
|
1389
|
+
reused: update.reused === true,
|
|
1390
|
+
cleared: update.value.length === 0
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
const envFiles = writeLocalFiles ? writeTreeseedLocalEnvironmentFiles(tenantRoot) : null;
|
|
1394
|
+
if (applyLocalEnvironment) {
|
|
1395
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
|
|
1218
1396
|
}
|
|
1397
|
+
return {
|
|
1398
|
+
updated: applied,
|
|
1399
|
+
envFiles
|
|
1400
|
+
};
|
|
1219
1401
|
}
|
|
1220
|
-
|
|
1402
|
+
function finalizeTreeseedConfig({
|
|
1221
1403
|
tenantRoot,
|
|
1222
|
-
scopes = [
|
|
1404
|
+
scopes = [...TREESEED_ENVIRONMENT_SCOPES],
|
|
1223
1405
|
sync = "all",
|
|
1224
|
-
prompt,
|
|
1225
|
-
authStatus,
|
|
1226
|
-
write = console.log,
|
|
1227
1406
|
env = process.env,
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
revealSecrets = false,
|
|
1231
|
-
checkConnections = true
|
|
1407
|
+
checkConnections = true,
|
|
1408
|
+
initializePersistent = true
|
|
1232
1409
|
}) {
|
|
1233
|
-
ensureTreeseedGitignoreEntries(tenantRoot);
|
|
1234
1410
|
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1235
|
-
const groups = ["auth", "local-development", "forms", "smtp", "cloudflare"];
|
|
1236
1411
|
const summary = {
|
|
1237
1412
|
scopes,
|
|
1238
|
-
updated: [],
|
|
1239
1413
|
synced: {},
|
|
1240
1414
|
initialized: [],
|
|
1241
|
-
connectionChecks: []
|
|
1415
|
+
connectionChecks: [],
|
|
1416
|
+
validationByScope: {}
|
|
1242
1417
|
};
|
|
1243
1418
|
for (const scope of scopes) {
|
|
1244
|
-
const existingValues = collectTreeseedConfigSeedValues(tenantRoot, scope, env);
|
|
1245
|
-
const configAuthStatus = createConfigAuthStatus(existingValues);
|
|
1246
|
-
const suggested = getTreeseedEnvironmentSuggestedValues({
|
|
1247
|
-
scope,
|
|
1248
|
-
deployConfig: registry.context.deployConfig,
|
|
1249
|
-
tenantConfig: registry.context.tenantConfig,
|
|
1250
|
-
plugins: registry.context.plugins
|
|
1251
|
-
});
|
|
1252
|
-
write(formatConfigSectionTitle(`Treeseed configuration for ${scope}`));
|
|
1253
|
-
write(`Tenant: ${registry.context.deployConfig.name} (${registry.context.deployConfig.slug})`);
|
|
1254
|
-
if (authStatus) {
|
|
1255
|
-
write(`GitHub token: ${configAuthStatus.gh.authenticated ? colorize("ready", "32") : colorize("missing", "31")}`);
|
|
1256
|
-
write(`Cloudflare token: ${configAuthStatus.wrangler.authenticated ? colorize("ready", "32") : colorize("missing", "31")}`);
|
|
1257
|
-
write(`Railway token: ${configAuthStatus.railway.authenticated ? colorize("ready", "32") : colorize("missing", "31")}`);
|
|
1258
|
-
}
|
|
1259
|
-
for (const group of groups) {
|
|
1260
|
-
const groupEntries = registry.entries.filter(
|
|
1261
|
-
(entry) => entry.group === group && entry.scopes.includes(scope) && (!entry.isRelevant || entry.isRelevant(registry.context, scope, "config"))
|
|
1262
|
-
);
|
|
1263
|
-
if (groupEntries.length === 0) {
|
|
1264
|
-
continue;
|
|
1265
|
-
}
|
|
1266
|
-
write(formatConfigSectionTitle(group));
|
|
1267
|
-
for (const entry of groupEntries) {
|
|
1268
|
-
const currentValue = existingValues[entry.id];
|
|
1269
|
-
const suggestedValue = suggested[entry.id];
|
|
1270
|
-
const displayValue = currentValue ?? suggestedValue ?? "";
|
|
1271
|
-
const entryAuthStatus = createConfigAuthStatus(existingValues);
|
|
1272
|
-
const renderedInk = useInk && await renderInkConfigFrame({
|
|
1273
|
-
scope,
|
|
1274
|
-
tenantName: registry.context.deployConfig.name,
|
|
1275
|
-
tenantSlug: registry.context.deployConfig.slug,
|
|
1276
|
-
group,
|
|
1277
|
-
entry,
|
|
1278
|
-
currentValue,
|
|
1279
|
-
authStatus: entryAuthStatus
|
|
1280
|
-
});
|
|
1281
|
-
if (!renderedInk) {
|
|
1282
|
-
write(formatConfigFieldPrompt(entry, currentValue));
|
|
1283
|
-
}
|
|
1284
|
-
const answer = (await prompt(
|
|
1285
|
-
`${entry.id}${displayValue ? ` [${entry.sensitivity === "secret" ? "keep current" : displayValue}]` : ""}: `
|
|
1286
|
-
)).trim();
|
|
1287
|
-
if (answer === "" && displayValue) {
|
|
1288
|
-
setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, displayValue);
|
|
1289
|
-
existingValues[entry.id] = displayValue;
|
|
1290
|
-
summary.updated.push({ scope, id: entry.id, reused: true });
|
|
1291
|
-
continue;
|
|
1292
|
-
}
|
|
1293
|
-
if (answer === "" && !displayValue) {
|
|
1294
|
-
setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, "");
|
|
1295
|
-
existingValues[entry.id] = "";
|
|
1296
|
-
continue;
|
|
1297
|
-
}
|
|
1298
|
-
if (answer === "-") {
|
|
1299
|
-
setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, "");
|
|
1300
|
-
existingValues[entry.id] = "";
|
|
1301
|
-
summary.updated.push({ scope, id: entry.id, cleared: true });
|
|
1302
|
-
continue;
|
|
1303
|
-
}
|
|
1304
|
-
setTreeseedMachineEnvironmentValue(tenantRoot, scope, entry, answer);
|
|
1305
|
-
existingValues[entry.id] = answer;
|
|
1306
|
-
summary.updated.push({ scope, id: entry.id, reused: false });
|
|
1307
|
-
}
|
|
1308
|
-
}
|
|
1309
1419
|
const validation = validateTreeseedEnvironmentValues({
|
|
1310
1420
|
values: resolveTreeseedMachineEnvironmentValues(tenantRoot, scope),
|
|
1311
1421
|
scope,
|
|
@@ -1314,37 +1424,32 @@ async function runTreeseedConfigWizard({
|
|
|
1314
1424
|
tenantConfig: registry.context.tenantConfig,
|
|
1315
1425
|
plugins: registry.context.plugins
|
|
1316
1426
|
});
|
|
1427
|
+
summary.validationByScope[scope] = validation;
|
|
1317
1428
|
if (!validation.ok) {
|
|
1318
1429
|
const details = [...validation.missing, ...validation.invalid].map((problem) => `- ${problem.message}`).join("\n");
|
|
1319
1430
|
throw new Error(`Treeseed config validation failed for ${scope}:
|
|
1320
1431
|
${details}`);
|
|
1321
1432
|
}
|
|
1322
|
-
if (printEnv) {
|
|
1323
|
-
write(formatTreeseedConfigEnvironmentReport({ tenantRoot, scope, env, revealSecrets }));
|
|
1324
|
-
}
|
|
1325
1433
|
if (checkConnections) {
|
|
1326
|
-
|
|
1327
|
-
summary.connectionChecks.push(connectionReport);
|
|
1328
|
-
writeProviderConnectionReport(write, connectionReport);
|
|
1434
|
+
summary.connectionChecks.push(checkTreeseedProviderConnections({ tenantRoot, scope, env }));
|
|
1329
1435
|
}
|
|
1330
1436
|
}
|
|
1331
1437
|
writeTreeseedLocalEnvironmentFiles(tenantRoot);
|
|
1332
1438
|
syncManagedServiceSettingsFromDeployConfig(tenantRoot);
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1439
|
+
if (initializePersistent) {
|
|
1440
|
+
for (const scope of scopes) {
|
|
1441
|
+
if (scope === "local") {
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
applyTreeseedEnvironmentToProcess({ tenantRoot, scope, override: true });
|
|
1445
|
+
const initialized = initializeTreeseedPersistentEnvironment({ tenantRoot, scope });
|
|
1446
|
+
summary.initialized.push({
|
|
1447
|
+
scope,
|
|
1448
|
+
secrets: initialized.secrets.length,
|
|
1449
|
+
target: initialized.summary.target
|
|
1450
|
+
});
|
|
1451
|
+
markManagedServicesInitialized(tenantRoot, { scope });
|
|
1341
1452
|
}
|
|
1342
|
-
summary.initialized.push({
|
|
1343
|
-
scope,
|
|
1344
|
-
secrets: initialized.secrets.length,
|
|
1345
|
-
target: initialized.summary.target
|
|
1346
|
-
});
|
|
1347
|
-
markManagedServicesInitialized(tenantRoot, { scope });
|
|
1348
1453
|
}
|
|
1349
1454
|
if (sync === "github" || sync === "all") {
|
|
1350
1455
|
summary.synced.github = syncTreeseedGitHubEnvironment({ tenantRoot, scope: scopes.at(-1) ?? "prod" });
|
|
@@ -1357,25 +1462,55 @@ ${details}`);
|
|
|
1357
1462
|
}
|
|
1358
1463
|
return summary;
|
|
1359
1464
|
}
|
|
1465
|
+
function collectTreeseedPrintEnvReport({
|
|
1466
|
+
tenantRoot,
|
|
1467
|
+
scope,
|
|
1468
|
+
env = process.env,
|
|
1469
|
+
revealSecrets = false
|
|
1470
|
+
}) {
|
|
1471
|
+
const registry = collectTreeseedEnvironmentContext(tenantRoot);
|
|
1472
|
+
const { values, sources } = collectTreeseedConfigSeedValueSources(tenantRoot, scope, env);
|
|
1473
|
+
return {
|
|
1474
|
+
scope,
|
|
1475
|
+
revealSecrets,
|
|
1476
|
+
entries: listRelevantTreeseedConfigEntries(registry, scope).map((entry) => {
|
|
1477
|
+
const rawValue = values[entry.id] ?? "";
|
|
1478
|
+
return {
|
|
1479
|
+
id: entry.id,
|
|
1480
|
+
label: entry.label,
|
|
1481
|
+
sensitivity: entry.sensitivity,
|
|
1482
|
+
value: rawValue,
|
|
1483
|
+
displayValue: rawValue ? entry.sensitivity === "secret" && !revealSecrets ? maskValue(rawValue) : rawValue : "(unset)",
|
|
1484
|
+
source: sources[entry.id] ?? "unset"
|
|
1485
|
+
};
|
|
1486
|
+
})
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1360
1489
|
export {
|
|
1361
1490
|
DEFAULT_TEMPLATE_CATALOG_URL,
|
|
1362
1491
|
DEFAULT_TREESEED_API_BASE_URL,
|
|
1363
1492
|
TREESEED_API_BASE_URL_ENV,
|
|
1364
1493
|
TREESEED_TEMPLATE_CATALOG_URL_ENV,
|
|
1494
|
+
applyTreeseedConfigValues,
|
|
1365
1495
|
applyTreeseedEnvironmentToProcess,
|
|
1496
|
+
applyTreeseedSafeRepairs,
|
|
1366
1497
|
assertTreeseedCommandEnvironment,
|
|
1367
1498
|
checkTreeseedProviderConnections,
|
|
1368
1499
|
clearTreeseedRemoteSession,
|
|
1500
|
+
collectTreeseedConfigContext,
|
|
1369
1501
|
collectTreeseedConfigSeedValues,
|
|
1370
1502
|
collectTreeseedEnvironmentContext,
|
|
1503
|
+
collectTreeseedPrintEnvReport,
|
|
1371
1504
|
createDefaultTreeseedMachineConfig,
|
|
1372
1505
|
ensureTreeseedActVerificationTooling,
|
|
1373
1506
|
ensureTreeseedGitignoreEntries,
|
|
1507
|
+
finalizeTreeseedConfig,
|
|
1374
1508
|
formatTreeseedConfigEnvironmentReport,
|
|
1375
1509
|
formatTreeseedProviderConnectionReport,
|
|
1376
1510
|
getTreeseedMachineConfigPaths,
|
|
1377
1511
|
getTreeseedRemoteAuthPaths,
|
|
1378
1512
|
initializeTreeseedPersistentEnvironment,
|
|
1513
|
+
listRelevantTreeseedConfigEntries,
|
|
1379
1514
|
loadTreeseedMachineConfig,
|
|
1380
1515
|
loadTreeseedRemoteAuthState,
|
|
1381
1516
|
resolveTreeseedMachineEnvironmentValues,
|
|
@@ -1384,7 +1519,6 @@ export {
|
|
|
1384
1519
|
resolveTreeseedTemplateCatalogCachePath,
|
|
1385
1520
|
resolveTreeseedTemplateCatalogEndpoint,
|
|
1386
1521
|
rotateTreeseedMachineKey,
|
|
1387
|
-
runTreeseedConfigWizard,
|
|
1388
1522
|
setTreeseedMachineEnvironmentValue,
|
|
1389
1523
|
setTreeseedRemoteSession,
|
|
1390
1524
|
syncTreeseedCloudflareEnvironment,
|