@monster-orange/den 0.0.0 → 0.2.0
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/CHANGELOG.md +204 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +68 -0
- package/docs/distributions.md +499 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +101 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +403 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +124 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +757 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/agent-actor.ts +77 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +688 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/status.ts +74 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +635 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +108 -0
- package/src/shared/environment-manager/log-tail.ts +213 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +335 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-container.ts +47 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1603 -0
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +326 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/merge-override.ts +75 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/placement.ts +321 -0
- package/src/shared/manifest/read-manifest.ts +278 -0
- package/src/shared/manifest/schema.ts +313 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +200 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +190 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/redact.ts +107 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +118 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/status.ts +99 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +701 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +148 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +81 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +570 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3742 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +117 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1319 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/mcp/api.ts +284 -0
- package/src/ui/mcp/guards.ts +141 -0
- package/src/ui/mcp/server.ts +147 -0
- package/src/ui/mcp/tools.ts +565 -0
- package/src/ui/server/audit.ts +71 -0
- package/src/ui/server/changelog-parser.ts +105 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3350 -0
- package/src/ui/server/log-history.ts +453 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +496 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +976 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +1373 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +5344 -0
- package/src/ui/server/public/logs.html +1496 -0
- package/src/ui/server/public/panel.js +787 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1103 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/readonly-routes.ts +200 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/ui-assets.ts +73 -0
- package/src/ui/server/workspace-routes.ts +2185 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate a repo's manifest layer by layer: the base (repo or held), the
|
|
3
|
+
* override, then the merged result. Each layer's errors name its file.
|
|
4
|
+
*
|
|
5
|
+
* The override is partial, so it is not checked against the whole schema
|
|
6
|
+
* (it need not repeat `name`, say). Instead each value it sets is checked
|
|
7
|
+
* against the schema at that path, and every key it names must exist in the
|
|
8
|
+
* base (see overrideKeyErrors).
|
|
9
|
+
*
|
|
10
|
+
* The schema and the single-file validator come in as `SchemaTools`, so this
|
|
11
|
+
* file stays under the typecheck gate: schema.ts and validate-manifest.ts are
|
|
12
|
+
* still baseline-excluded (zod v4 drift), and importing them here would pull
|
|
13
|
+
* them back into the checked program. validate-manifest.ts wires them in and
|
|
14
|
+
* exports validateManifestLayers(repoPath).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import type { ValidationError, ValidationResult } from "../types/index.js";
|
|
20
|
+
import {
|
|
21
|
+
isPlainMap,
|
|
22
|
+
mergeOverride,
|
|
23
|
+
overrideKeyErrors,
|
|
24
|
+
type ManifestMap,
|
|
25
|
+
} from "./merge-override.js";
|
|
26
|
+
import {
|
|
27
|
+
loadManifest,
|
|
28
|
+
orphanOverrideMessage,
|
|
29
|
+
parseManifestText,
|
|
30
|
+
type ManifestLayer,
|
|
31
|
+
} from "./read-manifest.js";
|
|
32
|
+
|
|
33
|
+
/** What layered validation needs from the schema module. */
|
|
34
|
+
export interface SchemaTools {
|
|
35
|
+
/** The current schema version and its schema. */
|
|
36
|
+
version: number;
|
|
37
|
+
schema: z.ZodType;
|
|
38
|
+
/** Validate one whole manifest (validateManifest). */
|
|
39
|
+
validate: (input: unknown) => ValidationResult;
|
|
40
|
+
/** Map Zod issues to ValidationErrors, as validateManifest does. */
|
|
41
|
+
issuesToErrors: (issues: z.core.$ZodIssue[]) => ValidationError[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface LayerValidation {
|
|
45
|
+
layer: ManifestLayer | "merged";
|
|
46
|
+
/** The layer's file; null for the merged result. */
|
|
47
|
+
file: string | null;
|
|
48
|
+
result: ValidationResult;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface LayeredValidation {
|
|
52
|
+
valid: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Base first, then override, then merged (only when there is an override
|
|
55
|
+
* and both layers pass on their own). Empty when no manifest file exists.
|
|
56
|
+
*/
|
|
57
|
+
layers: LayerValidation[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Strip optional/nullable/default wrappers to the schema underneath. */
|
|
61
|
+
function unwrap(schema: z.ZodType): z.ZodType {
|
|
62
|
+
let s = schema;
|
|
63
|
+
for (;;) {
|
|
64
|
+
if (s instanceof z.ZodOptional || s instanceof z.ZodNullable)
|
|
65
|
+
s = s.unwrap() as z.ZodType;
|
|
66
|
+
else if (s instanceof z.ZodDefault) s = s.def.innerType as z.ZodType;
|
|
67
|
+
else return s;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Schema errors in `value`, an override's value at `at`, merging into
|
|
73
|
+
* `base` (the base's value there). Maps are walked key by key where the
|
|
74
|
+
* schema is an object or a record; anything else is checked whole (a map
|
|
75
|
+
* merged onto its base first, so a partial union member still passes).
|
|
76
|
+
*/
|
|
77
|
+
function overrideSchemaErrors(
|
|
78
|
+
tools: SchemaTools,
|
|
79
|
+
schema: z.ZodType,
|
|
80
|
+
value: unknown,
|
|
81
|
+
base: unknown,
|
|
82
|
+
at: string[],
|
|
83
|
+
): ValidationError[] {
|
|
84
|
+
if (value === null) return []; // a delete: overrideKeyErrors checks the key
|
|
85
|
+
const node = unwrap(schema);
|
|
86
|
+
const baseAt = (key: string): unknown =>
|
|
87
|
+
isPlainMap(base) ? base[key] : undefined;
|
|
88
|
+
if (isPlainMap(value) && node instanceof z.ZodObject) {
|
|
89
|
+
const shape = node.shape as Record<string, z.ZodType>;
|
|
90
|
+
return Object.entries(value).flatMap(([key, v]) =>
|
|
91
|
+
key in shape
|
|
92
|
+
? overrideSchemaErrors(tools, shape[key], v, baseAt(key), [...at, key])
|
|
93
|
+
: [],
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (isPlainMap(value) && node instanceof z.ZodRecord) {
|
|
97
|
+
const inner = node.valueType as z.ZodType;
|
|
98
|
+
return Object.entries(value).flatMap(([key, v]) =>
|
|
99
|
+
overrideSchemaErrors(tools, inner, v, baseAt(key), [...at, key]),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const candidate =
|
|
103
|
+
isPlainMap(value) && isPlainMap(base) ? mergeOverride(base, value) : value;
|
|
104
|
+
const result = node.safeParse(candidate);
|
|
105
|
+
return result.success
|
|
106
|
+
? []
|
|
107
|
+
: tools.issuesToErrors(
|
|
108
|
+
result.error.issues.map((i) => ({ ...i, path: [...at, ...i.path] })),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Validate an override against the schema and against its base's keys. */
|
|
113
|
+
export function validateOverride(
|
|
114
|
+
tools: SchemaTools,
|
|
115
|
+
override: unknown,
|
|
116
|
+
base: ManifestMap,
|
|
117
|
+
): ValidationResult {
|
|
118
|
+
const { version } = tools;
|
|
119
|
+
if (!isPlainMap(override))
|
|
120
|
+
return {
|
|
121
|
+
valid: false,
|
|
122
|
+
version,
|
|
123
|
+
errors: [{ path: "", message: "an override must be a map" }],
|
|
124
|
+
warnings: [],
|
|
125
|
+
};
|
|
126
|
+
const errors = [
|
|
127
|
+
...overrideKeyErrors(base, override),
|
|
128
|
+
...overrideSchemaErrors(tools, tools.schema, override, base, []),
|
|
129
|
+
];
|
|
130
|
+
return { valid: errors.length === 0, version, errors, warnings: [] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function withFile(result: ValidationResult, file: string): ValidationResult {
|
|
134
|
+
const tag = (e: ValidationError): ValidationError => ({ ...e, file });
|
|
135
|
+
return {
|
|
136
|
+
...result,
|
|
137
|
+
errors: result.errors.map(tag),
|
|
138
|
+
warnings: result.warnings.map(tag),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function failure(
|
|
143
|
+
version: number,
|
|
144
|
+
file: string,
|
|
145
|
+
message: string,
|
|
146
|
+
): ValidationResult {
|
|
147
|
+
return {
|
|
148
|
+
valid: false,
|
|
149
|
+
version,
|
|
150
|
+
errors: [{ path: "", message, file }],
|
|
151
|
+
warnings: [],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Read and parse a layer file, or the reason it cannot be used. */
|
|
156
|
+
function readLayer(file: string): { value: unknown } | { error: string } {
|
|
157
|
+
try {
|
|
158
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
159
|
+
// A non-map (scalar, list, empty file) is passed on as is, so the
|
|
160
|
+
// validator reports it the way it reports any other bad manifest.
|
|
161
|
+
let value: unknown;
|
|
162
|
+
try {
|
|
163
|
+
value = parseManifestText(file, raw);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return { error: `could not parse: ${(e as Error).message}` };
|
|
166
|
+
}
|
|
167
|
+
return { value: value ?? (raw.trim() ? raw : null) };
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return { error: `could not read: ${(e as Error).message}` };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Validate every manifest layer of `repoPath`, then the merged result. */
|
|
174
|
+
export function validateLayers(
|
|
175
|
+
tools: SchemaTools,
|
|
176
|
+
repoPath: string,
|
|
177
|
+
): LayeredValidation {
|
|
178
|
+
const { baseFile, baseLayer, overrideFile } = loadManifest(repoPath);
|
|
179
|
+
const layers: LayerValidation[] = [];
|
|
180
|
+
|
|
181
|
+
if (!baseFile || !baseLayer) {
|
|
182
|
+
if (overrideFile)
|
|
183
|
+
layers.push({
|
|
184
|
+
layer: "override",
|
|
185
|
+
file: overrideFile,
|
|
186
|
+
result: failure(tools.version, overrideFile, orphanOverrideMessage()),
|
|
187
|
+
});
|
|
188
|
+
return { valid: false, layers };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const base = readLayer(baseFile);
|
|
192
|
+
const baseResult =
|
|
193
|
+
"error" in base
|
|
194
|
+
? failure(tools.version, baseFile, base.error)
|
|
195
|
+
: withFile(tools.validate(base.value), baseFile);
|
|
196
|
+
layers.push({ layer: baseLayer, file: baseFile, result: baseResult });
|
|
197
|
+
|
|
198
|
+
if (overrideFile) {
|
|
199
|
+
const override = readLayer(overrideFile);
|
|
200
|
+
const baseMap = "value" in base && isPlainMap(base.value) ? base.value : {};
|
|
201
|
+
const overrideResult =
|
|
202
|
+
"error" in override
|
|
203
|
+
? failure(tools.version, overrideFile, override.error)
|
|
204
|
+
: withFile(
|
|
205
|
+
validateOverride(tools, override.value, baseMap),
|
|
206
|
+
overrideFile,
|
|
207
|
+
);
|
|
208
|
+
layers.push({
|
|
209
|
+
layer: "override",
|
|
210
|
+
file: overrideFile,
|
|
211
|
+
result: overrideResult,
|
|
212
|
+
});
|
|
213
|
+
// The merged result is checked once both layers pass on their own, so a
|
|
214
|
+
// layer's error is reported once, against its own file.
|
|
215
|
+
if (
|
|
216
|
+
baseResult.valid &&
|
|
217
|
+
overrideResult.valid &&
|
|
218
|
+
"value" in override &&
|
|
219
|
+
isPlainMap(override.value)
|
|
220
|
+
)
|
|
221
|
+
layers.push({
|
|
222
|
+
layer: "merged",
|
|
223
|
+
file: null,
|
|
224
|
+
result: tools.validate(mergeOverride(baseMap, override.value)),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { valid: layers.every((l) => l.result.valid), layers };
|
|
229
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .den.yml manifest validator.
|
|
3
|
+
*
|
|
4
|
+
* Accepts a parsed object or a raw YAML string.
|
|
5
|
+
* Returns a ValidationResult describing any errors or warnings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as yaml from "yaml";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import {
|
|
11
|
+
CURRENT_SCHEMA_VERSION,
|
|
12
|
+
KNOWN_VERSIONS,
|
|
13
|
+
schemaByVersion,
|
|
14
|
+
} from "./schema.js";
|
|
15
|
+
import type { ValidationError, ValidationResult } from "../types/index.js";
|
|
16
|
+
import { placementErrors } from "./placement.js";
|
|
17
|
+
import { builtinEnvironments, orgEnvironments } from "../den-config.js";
|
|
18
|
+
import {
|
|
19
|
+
validateLayers,
|
|
20
|
+
type LayeredValidation,
|
|
21
|
+
type SchemaTools,
|
|
22
|
+
} from "./validate-layers.js";
|
|
23
|
+
|
|
24
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/** Convert a Zod issue path to a dot-separated string. */
|
|
27
|
+
function zodPathToString(path: (string | number)[]): string {
|
|
28
|
+
return path
|
|
29
|
+
.map((p, i) => (typeof p === "number" ? `[${p}]` : i === 0 ? p : `.${p}`))
|
|
30
|
+
.join("");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Friendly message for common Zod issue codes. */
|
|
34
|
+
function zodIssueMessage(issue: z.ZodIssue): string {
|
|
35
|
+
switch (issue.code) {
|
|
36
|
+
case z.ZodIssueCode.invalid_type:
|
|
37
|
+
if (issue.received === "undefined")
|
|
38
|
+
return `"${issue.path.at(-1)}" is required`;
|
|
39
|
+
return `expected ${issue.expected}, received ${issue.received}`;
|
|
40
|
+
case z.ZodIssueCode.invalid_enum_value:
|
|
41
|
+
return `invalid value "${issue.received}" — must be one of: ${issue.options.join(", ")}`;
|
|
42
|
+
case z.ZodIssueCode.invalid_union:
|
|
43
|
+
return "value does not match any valid shape";
|
|
44
|
+
default:
|
|
45
|
+
return issue.message;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Suggestion text for specific paths / issues. */
|
|
50
|
+
function suggestionFor(issue: z.ZodIssue): string | undefined {
|
|
51
|
+
const pathStr = zodPathToString(issue.path);
|
|
52
|
+
|
|
53
|
+
// auth arrays in smoke / synthetics
|
|
54
|
+
if (pathStr.includes("auth")) {
|
|
55
|
+
return 'Valid roles are: "admin", "user", "beta"';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// health with missing url/cmd
|
|
59
|
+
if (pathStr.includes("health")) {
|
|
60
|
+
return 'HTTP checks need "url"; shell checks need "type: shell" and "cmd"';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Map Zod issues to ValidationErrors. */
|
|
67
|
+
export function issuesToErrors(
|
|
68
|
+
issues: readonly z.ZodIssue[],
|
|
69
|
+
): ValidationError[] {
|
|
70
|
+
return issues.map((issue) => ({
|
|
71
|
+
path: zodPathToString(issue.path as (string | number)[]),
|
|
72
|
+
message: zodIssueMessage(issue),
|
|
73
|
+
suggestion: suggestionFor(issue),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Main export ───────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate a .den.yml manifest.
|
|
81
|
+
*
|
|
82
|
+
* @param input - A parsed object or a raw YAML string.
|
|
83
|
+
* @returns A ValidationResult with errors, warnings, and the detected version.
|
|
84
|
+
*/
|
|
85
|
+
export interface ValidateOptions {
|
|
86
|
+
/** The org's environments; default: the org config's (see orgEnvironments). */
|
|
87
|
+
environments?: readonly string[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function environmentsFor(opts: ValidateOptions): readonly string[] {
|
|
91
|
+
if (opts.environments) return opts.environments;
|
|
92
|
+
try {
|
|
93
|
+
return orgEnvironments();
|
|
94
|
+
} catch {
|
|
95
|
+
// An unreadable org config is reported where it is read; validate the
|
|
96
|
+
// manifest against Den's built-in environment list meanwhile.
|
|
97
|
+
return builtinEnvironments();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function validateManifest(
|
|
102
|
+
input: unknown,
|
|
103
|
+
opts: ValidateOptions = {},
|
|
104
|
+
): ValidationResult {
|
|
105
|
+
// 1. Parse YAML string if needed
|
|
106
|
+
let parsed: unknown = input;
|
|
107
|
+
if (typeof input === "string") {
|
|
108
|
+
try {
|
|
109
|
+
parsed = yaml.parse(input);
|
|
110
|
+
} catch (err: unknown) {
|
|
111
|
+
return {
|
|
112
|
+
valid: false,
|
|
113
|
+
version: 1,
|
|
114
|
+
errors: [
|
|
115
|
+
{
|
|
116
|
+
path: "",
|
|
117
|
+
message: `Could not parse YAML: ${(err as Error).message}`,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
warnings: [],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2. Detect version
|
|
126
|
+
const rawVersion =
|
|
127
|
+
parsed !== null &&
|
|
128
|
+
typeof parsed === "object" &&
|
|
129
|
+
"version" in (parsed as object)
|
|
130
|
+
? (parsed as Record<string, unknown>).version
|
|
131
|
+
: undefined;
|
|
132
|
+
|
|
133
|
+
const detectedVersion = rawVersion === undefined ? 1 : Number(rawVersion);
|
|
134
|
+
|
|
135
|
+
const warnings: ValidationError[] = [];
|
|
136
|
+
|
|
137
|
+
// 3. Warn on unknown future versions but still validate with the latest schema
|
|
138
|
+
const isKnown = KNOWN_VERSIONS.includes(
|
|
139
|
+
detectedVersion as (typeof KNOWN_VERSIONS)[number],
|
|
140
|
+
);
|
|
141
|
+
if (rawVersion !== undefined && !isKnown) {
|
|
142
|
+
warnings.push({
|
|
143
|
+
path: "version",
|
|
144
|
+
message: `Unknown schema version ${detectedVersion}. Validating against v${CURRENT_SCHEMA_VERSION} (latest known).`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 4. Pick schema — fall back to latest for unknown future versions
|
|
149
|
+
const schema =
|
|
150
|
+
schemaByVersion[detectedVersion] ?? schemaByVersion[CURRENT_SCHEMA_VERSION];
|
|
151
|
+
|
|
152
|
+
// 5. Run Zod validation
|
|
153
|
+
const result = schema.safeParse(parsed);
|
|
154
|
+
|
|
155
|
+
if (result.success) {
|
|
156
|
+
// Env names in `runs` / `target.env` must be the org's (R13).
|
|
157
|
+
const placement = placementErrors(parsed, environmentsFor(opts));
|
|
158
|
+
if (placement.length > 0)
|
|
159
|
+
return {
|
|
160
|
+
valid: false,
|
|
161
|
+
version: detectedVersion,
|
|
162
|
+
errors: placement,
|
|
163
|
+
warnings,
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
valid: true,
|
|
167
|
+
version: detectedVersion,
|
|
168
|
+
errors: [],
|
|
169
|
+
warnings,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 6. Map Zod issues → ValidationError[]
|
|
174
|
+
const errors = issuesToErrors(result.error.issues);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
valid: false,
|
|
178
|
+
version: detectedVersion,
|
|
179
|
+
errors,
|
|
180
|
+
warnings,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Layers ────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
const schemaTools: SchemaTools = {
|
|
187
|
+
version: CURRENT_SCHEMA_VERSION,
|
|
188
|
+
schema: schemaByVersion[CURRENT_SCHEMA_VERSION],
|
|
189
|
+
validate: (input) => validateManifest(input),
|
|
190
|
+
issuesToErrors: (issues) => issuesToErrors(issues as z.ZodIssue[]),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Validate each manifest layer of the repo at `repoPath` (its own or the
|
|
195
|
+
* held manifest, then manifests/<repo>.override.yml), then the merge.
|
|
196
|
+
* See validate-layers.ts.
|
|
197
|
+
*/
|
|
198
|
+
export function validateManifestLayers(repoPath: string): LayeredValidation {
|
|
199
|
+
return validateLayers(schemaTools, repoPath);
|
|
200
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every service of the active workspaces, with its repo's manifest: the one
|
|
3
|
+
* list the security scan, the repo suites and the release check walk.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the server's health walk: the focused workspace first, then the
|
|
6
|
+
* other active ones; an address listed twice keeps its first owner.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readCatalog } from "../environment-manager/catalog.js";
|
|
10
|
+
import {
|
|
11
|
+
getActiveWorkspaces,
|
|
12
|
+
getFocusedWorkspaceName,
|
|
13
|
+
getWorkspaceByName,
|
|
14
|
+
listWorkspaceServices,
|
|
15
|
+
readManifestAnyBranch,
|
|
16
|
+
} from "../environment-manager/workspaces.js";
|
|
17
|
+
import type {
|
|
18
|
+
RawManifest,
|
|
19
|
+
ServiceCatalog,
|
|
20
|
+
ServiceManagementConfig,
|
|
21
|
+
} from "../types/index.js";
|
|
22
|
+
import { orgOf } from "./service-links.js";
|
|
23
|
+
|
|
24
|
+
export interface WorkspaceService {
|
|
25
|
+
address: string;
|
|
26
|
+
repo: string;
|
|
27
|
+
service: string;
|
|
28
|
+
repoPath: string;
|
|
29
|
+
org: string;
|
|
30
|
+
manifest: RawManifest;
|
|
31
|
+
config: ServiceManagementConfig | undefined;
|
|
32
|
+
isEntrypoint: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function workspaceNames(workspaceName?: string | null): string[] {
|
|
36
|
+
if (workspaceName)
|
|
37
|
+
return getWorkspaceByName(workspaceName) ? [workspaceName] : [];
|
|
38
|
+
const focus = getFocusedWorkspaceName();
|
|
39
|
+
return [
|
|
40
|
+
...(focus ? [focus] : []),
|
|
41
|
+
...getActiveWorkspaces().filter((n) => n !== focus),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function listServicesWithManifests(
|
|
46
|
+
workspaceName?: string | null,
|
|
47
|
+
catalog: ServiceCatalog = readCatalog(),
|
|
48
|
+
): WorkspaceService[] {
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
const out: WorkspaceService[] = [];
|
|
51
|
+
for (const name of workspaceNames(workspaceName)) {
|
|
52
|
+
const ws = getWorkspaceByName(name);
|
|
53
|
+
if (!ws) continue;
|
|
54
|
+
for (const svc of listWorkspaceServices(ws.config, catalog, name)) {
|
|
55
|
+
if (seen.has(svc.address)) continue;
|
|
56
|
+
seen.add(svc.address);
|
|
57
|
+
const manifest = readManifestAnyBranch(svc.repoName, svc.repoPath);
|
|
58
|
+
if (!manifest) continue;
|
|
59
|
+
out.push({
|
|
60
|
+
address: svc.address,
|
|
61
|
+
repo: svc.repoName,
|
|
62
|
+
service: svc.subsystem,
|
|
63
|
+
repoPath: svc.repoPath,
|
|
64
|
+
org: orgOf(svc.repoName, catalog),
|
|
65
|
+
manifest,
|
|
66
|
+
config: manifest.services?.[svc.subsystem],
|
|
67
|
+
isEntrypoint: manifest.entrypoint === svc.subsystem,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One entry per repo (its first service carries the manifest). */
|
|
75
|
+
export function listReposWithManifests(
|
|
76
|
+
workspaceName?: string | null,
|
|
77
|
+
catalog: ServiceCatalog = readCatalog(),
|
|
78
|
+
): Array<Pick<WorkspaceService, "repo" | "repoPath" | "org" | "manifest">> {
|
|
79
|
+
const byRepo = new Map<
|
|
80
|
+
string,
|
|
81
|
+
Pick<WorkspaceService, "repo" | "repoPath" | "org" | "manifest">
|
|
82
|
+
>();
|
|
83
|
+
for (const s of listServicesWithManifests(workspaceName, catalog))
|
|
84
|
+
if (!byRepo.has(s.repo))
|
|
85
|
+
byRepo.set(s.repo, {
|
|
86
|
+
repo: s.repo,
|
|
87
|
+
repoPath: s.repoPath,
|
|
88
|
+
org: s.org,
|
|
89
|
+
manifest: s.manifest,
|
|
90
|
+
});
|
|
91
|
+
return [...byRepo.values()];
|
|
92
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a service's app opens in a browser, per environment, for the UI's
|
|
3
|
+
* open-in-new-tab links. Health URLs point at /healthz and friends, never the
|
|
4
|
+
* app, so they only ever contribute their origin.
|
|
5
|
+
*
|
|
6
|
+
* Order, first answer wins:
|
|
7
|
+
* 1. manifest `services.<svc>.urls.<env>`
|
|
8
|
+
* 2. local only: the edge route for the service's compose container,
|
|
9
|
+
* `https://<route>.<stack>.<edge domain>`; the stack-wide service (the
|
|
10
|
+
* entrypoint, or one named after the stack) takes the `app` route, else
|
|
11
|
+
* the first route
|
|
12
|
+
* 3. the origin of the env's HTTP health URL
|
|
13
|
+
* Only http(s) URLs are returned.
|
|
14
|
+
*/
|
|
15
|
+
import { manifestStackName } from "./stacks/config.js";
|
|
16
|
+
import { parseTarget } from "./stacks/render.js";
|
|
17
|
+
import type { RawManifest as DenManifest } from "./types/index.js";
|
|
18
|
+
|
|
19
|
+
export interface OpenLink {
|
|
20
|
+
label: string;
|
|
21
|
+
url: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The URL if it is absolute http(s), else null. */
|
|
25
|
+
export function httpUrl(raw: unknown): string | null {
|
|
26
|
+
if (typeof raw !== "string" || !raw) return null;
|
|
27
|
+
try {
|
|
28
|
+
const u = new URL(raw);
|
|
29
|
+
return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function edgeRouteUrl(
|
|
36
|
+
manifest: DenManifest,
|
|
37
|
+
subsystem: string,
|
|
38
|
+
repoName: string,
|
|
39
|
+
edgeDomain: string,
|
|
40
|
+
): string | null {
|
|
41
|
+
const routes = manifest.compose?.routes;
|
|
42
|
+
if (!routes || !Object.keys(routes).length) return null;
|
|
43
|
+
const stack = manifestStackName(manifest, repoName);
|
|
44
|
+
const names = Object.keys(routes).sort();
|
|
45
|
+
let route = names.find((r) => {
|
|
46
|
+
try {
|
|
47
|
+
return parseTarget(routes[r]).service === subsystem;
|
|
48
|
+
} catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
if (!route && (manifest.entrypoint === subsystem || subsystem === stack))
|
|
53
|
+
route = routes.app ? "app" : names[0];
|
|
54
|
+
return route ? `https://${route}.${stack}.${edgeDomain}/` : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function openUrlFor(opts: {
|
|
58
|
+
manifest: DenManifest;
|
|
59
|
+
subsystem: string;
|
|
60
|
+
repoName: string;
|
|
61
|
+
env: string;
|
|
62
|
+
/** The env's HTTP health URL, when the check is HTTP. */
|
|
63
|
+
healthUrl?: string | null;
|
|
64
|
+
/** Primary edge domain; local edge routes are skipped without it. */
|
|
65
|
+
edgeDomain?: string | null;
|
|
66
|
+
}): string | null {
|
|
67
|
+
const svc = opts.manifest.services?.[opts.subsystem];
|
|
68
|
+
const declared = httpUrl(
|
|
69
|
+
(svc?.urls as Record<string, string> | undefined)?.[opts.env],
|
|
70
|
+
);
|
|
71
|
+
if (declared) return declared;
|
|
72
|
+
if (opts.env === "local" && opts.edgeDomain) {
|
|
73
|
+
const routed = edgeRouteUrl(
|
|
74
|
+
opts.manifest,
|
|
75
|
+
opts.subsystem,
|
|
76
|
+
opts.repoName,
|
|
77
|
+
opts.edgeDomain,
|
|
78
|
+
);
|
|
79
|
+
if (routed) return routed;
|
|
80
|
+
}
|
|
81
|
+
const health = httpUrl(opts.healthUrl);
|
|
82
|
+
return health ? `${new URL(health).origin}/` : null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Extra named links (`services.<svc>.links`) that have a URL for the env. */
|
|
86
|
+
export function linksFor(
|
|
87
|
+
manifest: DenManifest,
|
|
88
|
+
subsystem: string,
|
|
89
|
+
env: string,
|
|
90
|
+
): OpenLink[] {
|
|
91
|
+
const out: OpenLink[] = [];
|
|
92
|
+
for (const l of manifest.services?.[subsystem]?.links ?? []) {
|
|
93
|
+
const url = httpUrl((l?.urls as Record<string, string> | undefined)?.[env]);
|
|
94
|
+
if (url && typeof l.label === "string") out.push({ label: l.label, url });
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Swap the primary edge domain for the alias the viewer is on (e.g. the UI was
|
|
101
|
+
* opened as den.<alias> from another machine), so links resolve there too.
|
|
102
|
+
*/
|
|
103
|
+
export function rehost(
|
|
104
|
+
url: string,
|
|
105
|
+
primary: string,
|
|
106
|
+
host: string | undefined,
|
|
107
|
+
aliases: string[],
|
|
108
|
+
): string {
|
|
109
|
+
const h = (host ?? "").split(":")[0].toLowerCase();
|
|
110
|
+
const alias = aliases.find((a) => h === a || h.endsWith(`.${a}`));
|
|
111
|
+
if (!alias) return url;
|
|
112
|
+
try {
|
|
113
|
+
const u = new URL(url);
|
|
114
|
+
if (u.hostname === primary || u.hostname.endsWith(`.${primary}`)) {
|
|
115
|
+
u.hostname = u.hostname.slice(0, -primary.length) + alias;
|
|
116
|
+
return u.href;
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
/* not a URL: leave it */
|
|
120
|
+
}
|
|
121
|
+
return url;
|
|
122
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The org this den instance serves: the root of
|
|
3
|
+
* Org > Application > Repo > Services. One instance = one org.
|
|
4
|
+
*
|
|
5
|
+
* org:
|
|
6
|
+
* id: globex # defaults to edge.org
|
|
7
|
+
* title: Globex # defaults to the id
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as yaml from "yaml";
|
|
12
|
+
import { getDistRoot } from "./paths.js";
|
|
13
|
+
|
|
14
|
+
import { product } from "./product.js";
|
|
15
|
+
export interface OrgConfig {
|
|
16
|
+
id?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Org {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Pure: the org from a parsed config/den.yml, or undefined if unnamed. */
|
|
26
|
+
export function resolveOrg(
|
|
27
|
+
parsed: { org?: OrgConfig; edge?: { org?: string } } | null | undefined,
|
|
28
|
+
): Org | undefined {
|
|
29
|
+
const id = parsed?.org?.id?.trim() || parsed?.edge?.org?.trim();
|
|
30
|
+
if (!id) return undefined;
|
|
31
|
+
return { id, title: parsed?.org?.title?.trim() || id };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readOrg(root: string = getDistRoot()): Org | undefined {
|
|
35
|
+
try {
|
|
36
|
+
return resolveOrg(
|
|
37
|
+
yaml.parse(
|
|
38
|
+
fs.readFileSync(path.join(root, product().configFile), "utf-8"),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|