@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,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `depends_on` entry normalization.
|
|
3
|
+
*
|
|
4
|
+
* A dep can be:
|
|
5
|
+
* - a string: "shop/db"
|
|
6
|
+
* - an object: { service: "shop/db", wait: true }
|
|
7
|
+
*
|
|
8
|
+
* The object form lets a service opt into blocking startup until the dep's
|
|
9
|
+
* health check is green (default behavior is the existing 5-second detach
|
|
10
|
+
* window — no change for unflagged deps).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { DependsOnEntry } from "../types/index.js";
|
|
14
|
+
|
|
15
|
+
export interface NormalizedDep {
|
|
16
|
+
service: string;
|
|
17
|
+
wait: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function depService(dep: DependsOnEntry): string {
|
|
21
|
+
return typeof dep === "string" ? dep : dep.service;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeDep(dep: DependsOnEntry): NormalizedDep {
|
|
25
|
+
if (typeof dep === "string") return { service: dep, wait: false };
|
|
26
|
+
return { service: dep.service, wait: dep.wait ?? false };
|
|
27
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The manifest override layer: `manifests/<repo>.override.yml` deep-merged
|
|
3
|
+
* onto whichever base manifest won (the repo's own, or the held one).
|
|
4
|
+
*
|
|
5
|
+
* Merge rules:
|
|
6
|
+
* - maps merge by key, recursively;
|
|
7
|
+
* - lists and scalars replace the base value whole;
|
|
8
|
+
* - an explicit `null` deletes the key from the result.
|
|
9
|
+
*
|
|
10
|
+
* An override only changes what the base already has: a key (or service)
|
|
11
|
+
* the base lacks is an error (see overrideKeyErrors), so a typo can never
|
|
12
|
+
* add a silent dead key. The one exception is a service's placement (`runs`,
|
|
13
|
+
* `target`), which an override may add to a service the base has.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ValidationError } from "../types/index.js";
|
|
17
|
+
|
|
18
|
+
export type ManifestMap = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
/** A plain YAML/JSON map (not a list, not null). */
|
|
21
|
+
export function isPlainMap(v: unknown): v is ManifestMap {
|
|
22
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `override` deep-merged onto `base`. Neither input is changed. */
|
|
26
|
+
export function mergeOverride(
|
|
27
|
+
base: ManifestMap,
|
|
28
|
+
override: ManifestMap,
|
|
29
|
+
): ManifestMap {
|
|
30
|
+
const out: ManifestMap = { ...base };
|
|
31
|
+
for (const [key, value] of Object.entries(override)) {
|
|
32
|
+
if (value === null) delete out[key];
|
|
33
|
+
else if (isPlainMap(value) && isPlainMap(out[key]))
|
|
34
|
+
out[key] = mergeOverride(out[key] as ManifestMap, value);
|
|
35
|
+
else out[key] = value;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Every key in `override` the base lacks, by its dotted path. A whole
|
|
42
|
+
* service the base lacks is reported as one error at `services.<name>`.
|
|
43
|
+
*/
|
|
44
|
+
/**
|
|
45
|
+
* Keys an override may add to a service the base has: placement (R13) is
|
|
46
|
+
* what a distribution most often needs to differ, and is never a dead key.
|
|
47
|
+
*/
|
|
48
|
+
const ADDABLE_SERVICE_KEYS = new Set(["runs", "target"]);
|
|
49
|
+
|
|
50
|
+
export function overrideKeyErrors(
|
|
51
|
+
base: ManifestMap,
|
|
52
|
+
override: ManifestMap,
|
|
53
|
+
at: string[] = [],
|
|
54
|
+
): ValidationError[] {
|
|
55
|
+
const errors: ValidationError[] = [];
|
|
56
|
+
const inService = at.length === 2 && at[0] === "services";
|
|
57
|
+
for (const [key, value] of Object.entries(override)) {
|
|
58
|
+
const here = [...at, key];
|
|
59
|
+
if (!(key in base) && inService && ADDABLE_SERVICE_KEYS.has(key)) continue;
|
|
60
|
+
if (!(key in base)) {
|
|
61
|
+
const isService = at.length === 1 && at[0] === "services";
|
|
62
|
+
errors.push({
|
|
63
|
+
path: here.join("."),
|
|
64
|
+
message: isService
|
|
65
|
+
? `unknown service "${key}": the base manifest has no such service`
|
|
66
|
+
: `unknown key "${key}": not in the base manifest`,
|
|
67
|
+
suggestion:
|
|
68
|
+
"An override can only change or delete what the base manifest has.",
|
|
69
|
+
});
|
|
70
|
+
} else if (isPlainMap(value) && isPlainMap(base[key])) {
|
|
71
|
+
errors.push(...overrideKeyErrors(base[key] as ManifestMap, value, here));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return errors;
|
|
75
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .den.yml schema migration engine.
|
|
3
|
+
*
|
|
4
|
+
* Walks the migration chain from the manifest's current version up to
|
|
5
|
+
* the requested target version (default: CURRENT_SCHEMA_VERSION).
|
|
6
|
+
*
|
|
7
|
+
* Each step is a pure function: migrate_N_to_M(manifest) → manifest
|
|
8
|
+
* Adding a new version means:
|
|
9
|
+
* 1. Bump CURRENT_SCHEMA_VERSION
|
|
10
|
+
* 2. Add migrate_<old>_to_<new> to the MIGRATION_STEPS map
|
|
11
|
+
* 3. Add the new version to schema.ts KNOWN_VERSIONS + schemaByVersion
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RawManifest, MigrationResult } from "../types/index.js";
|
|
15
|
+
import { CURRENT_SCHEMA_VERSION } from "./schema.js";
|
|
16
|
+
|
|
17
|
+
// ── Migration step registry ───────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
type MigrationFn = (manifest: RawManifest) => {
|
|
20
|
+
manifest: RawManifest;
|
|
21
|
+
change: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Migration steps keyed by "fromVersion".
|
|
26
|
+
* migrate_1_to_2 lives at key 1, migrate_2_to_3 at key 2, etc.
|
|
27
|
+
*
|
|
28
|
+
* Currently empty — v1 is the only version.
|
|
29
|
+
* When v2 is introduced, add:
|
|
30
|
+
* [1]: migrate_1_to_2,
|
|
31
|
+
*/
|
|
32
|
+
const MIGRATION_STEPS: Record<number, MigrationFn> = {
|
|
33
|
+
// Future migrations go here, e.g.:
|
|
34
|
+
// [1]: migrate_1_to_2,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Migrate a manifest to the target schema version.
|
|
41
|
+
*
|
|
42
|
+
* @param input - A parsed manifest object (or any plain object).
|
|
43
|
+
* @param toVersion - Target version. Defaults to CURRENT_SCHEMA_VERSION.
|
|
44
|
+
* @returns MigrationResult with the migrated manifest and a log of changes.
|
|
45
|
+
* @throws When toVersion is unknown / unsupported.
|
|
46
|
+
*/
|
|
47
|
+
export function migrateManifest(
|
|
48
|
+
input: Record<string, unknown> | RawManifest,
|
|
49
|
+
toVersion: number = CURRENT_SCHEMA_VERSION,
|
|
50
|
+
): MigrationResult {
|
|
51
|
+
// Validate target version
|
|
52
|
+
if (toVersion > CURRENT_SCHEMA_VERSION) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Cannot migrate to unsupported schema version ${toVersion}. ` +
|
|
55
|
+
`Latest known version is ${CURRENT_SCHEMA_VERSION}.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fromVersion =
|
|
60
|
+
typeof (input as Record<string, unknown>).version === "number"
|
|
61
|
+
? ((input as Record<string, unknown>).version as number)
|
|
62
|
+
: 1;
|
|
63
|
+
|
|
64
|
+
// Never silently downgrade: re-stamping a lower version onto newer-schema
|
|
65
|
+
// content corrupts the manifest. Fail fast, mirroring the forward guard above.
|
|
66
|
+
if (fromVersion > toVersion) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Cannot downgrade manifest from v${fromVersion} to v${toVersion}.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const changes: string[] = [];
|
|
73
|
+
let current: RawManifest = { ...(input as RawManifest) };
|
|
74
|
+
|
|
75
|
+
// Walk the chain: fromVersion → fromVersion+1 → … → toVersion
|
|
76
|
+
for (let v = fromVersion; v < toVersion; v++) {
|
|
77
|
+
const step = MIGRATION_STEPS[v];
|
|
78
|
+
if (!step) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`No migration step found from v${v} to v${v + 1}. ` +
|
|
81
|
+
`This is a bug — please file an issue.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const { manifest: next, change } = step(current);
|
|
85
|
+
current = next;
|
|
86
|
+
changes.push(change);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Stamp the output with the target version. RawManifest has no `version`
|
|
90
|
+
// field (absence means v1), so the stamp is applied as an extra property.
|
|
91
|
+
current = { ...current, version: toVersion } as RawManifest;
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
manifest: current,
|
|
95
|
+
fromVersion,
|
|
96
|
+
toVersion,
|
|
97
|
+
changes,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { CURRENT_SCHEMA_VERSION };
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement (R13): where each service runs for the env being viewed.
|
|
3
|
+
*
|
|
4
|
+
* services:
|
|
5
|
+
* core:
|
|
6
|
+
* runs: { local: remote, dev: remote } # per env: local | remote
|
|
7
|
+
* target: { env: dev, account: AWS, url: https://…/health, expect: 401 }
|
|
8
|
+
*
|
|
9
|
+
* `runs` is "local" (on this machine), "remote" (deployed elsewhere: Den never
|
|
10
|
+
* starts, stops, scans or execs it) or "none" (no runtime Den knows about).
|
|
11
|
+
* When the manifest does not say, for the viewed env:
|
|
12
|
+
* - local if the service has commands there (locally: or the repo is a
|
|
13
|
+
* compose stack);
|
|
14
|
+
* - remote if it has an HTTP health URL there and no commands;
|
|
15
|
+
* - none otherwise.
|
|
16
|
+
*
|
|
17
|
+
* `target` is set only for a remote service. The manifest's target applies
|
|
18
|
+
* when viewing locally or its own env; any other env stands for itself.
|
|
19
|
+
*
|
|
20
|
+
* Pure: callers pass the manifest, and the production test where it matters.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
RawManifest,
|
|
25
|
+
ServiceHealthConfig,
|
|
26
|
+
ServiceManagementConfig,
|
|
27
|
+
ValidationError,
|
|
28
|
+
} from "../types/index.js";
|
|
29
|
+
|
|
30
|
+
export type Runs = "local" | "remote" | "none";
|
|
31
|
+
|
|
32
|
+
/** What /api/services, /api/health and /api/applications serve. */
|
|
33
|
+
export interface PlacementTarget {
|
|
34
|
+
env: string;
|
|
35
|
+
account?: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Placement {
|
|
40
|
+
runs: Runs;
|
|
41
|
+
target: PlacementTarget | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Status codes that count as up for a target.url probe. */
|
|
45
|
+
export type Expect = readonly number[] | "2xx";
|
|
46
|
+
|
|
47
|
+
/** Is this env treated as production by the org? */
|
|
48
|
+
export type IsProduction = (env: string) => boolean;
|
|
49
|
+
|
|
50
|
+
const LOCAL = "local";
|
|
51
|
+
|
|
52
|
+
function isHttp(h: ServiceHealthConfig | undefined): h is { url: string } {
|
|
53
|
+
return !!h && h.type !== "shell" && typeof h.url === "string";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasCommands(
|
|
57
|
+
manifest: RawManifest,
|
|
58
|
+
svc: ServiceManagementConfig,
|
|
59
|
+
env: string,
|
|
60
|
+
): boolean {
|
|
61
|
+
if (ownCommands(svc, env)) return true;
|
|
62
|
+
if (env !== LOCAL) return false;
|
|
63
|
+
// Every service of a compose repo runs in the local stack, and so does
|
|
64
|
+
// every service of a repo whose own up runs here: one repo-level up starts
|
|
65
|
+
// services that declare no commands of their own.
|
|
66
|
+
return (
|
|
67
|
+
!!manifest.compose ||
|
|
68
|
+
Object.values(manifest.services ?? {}).some((s) => ownCommands(s, env))
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function ownCommands(svc: ServiceManagementConfig, env: string): boolean {
|
|
73
|
+
const cmds = svc.commands?.[env as keyof typeof svc.commands];
|
|
74
|
+
return !!cmds && Object.keys(cmds).length > 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function targetFor(svc: ServiceManagementConfig, env: string): PlacementTarget {
|
|
78
|
+
const t = svc.target;
|
|
79
|
+
const own = t && (env === LOCAL || t.env === env) ? t : undefined;
|
|
80
|
+
const tEnv = own?.env ?? env;
|
|
81
|
+
const health = svc.health?.[tEnv as keyof typeof svc.health];
|
|
82
|
+
const url = own?.url ?? (isHttp(health) ? health.url : undefined);
|
|
83
|
+
return {
|
|
84
|
+
env: tEnv,
|
|
85
|
+
...(own?.account ? { account: own.account } : {}),
|
|
86
|
+
...(url ? { url } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Where `service` of `manifest` runs when viewing `env`. */
|
|
91
|
+
export function servicePlacement(
|
|
92
|
+
manifest: RawManifest | null | undefined,
|
|
93
|
+
service: string,
|
|
94
|
+
env: string,
|
|
95
|
+
): Placement {
|
|
96
|
+
const svc = manifest?.services?.[service];
|
|
97
|
+
if (!manifest || !svc) return { runs: "none", target: null };
|
|
98
|
+
const explicit = svc.runs?.[env as keyof typeof svc.runs];
|
|
99
|
+
if (explicit === "local") return { runs: "local", target: null };
|
|
100
|
+
if (explicit === "remote")
|
|
101
|
+
return { runs: "remote", target: targetFor(svc, env) };
|
|
102
|
+
if (hasCommands(manifest, svc, env)) return { runs: "local", target: null };
|
|
103
|
+
if (isHttp(svc.health?.[env as keyof typeof svc.health]))
|
|
104
|
+
return { runs: "remote", target: targetFor(svc, env) };
|
|
105
|
+
return { runs: "none", target: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** "dev · AWS": where a remote service is, for labels. */
|
|
109
|
+
export function targetLabel(t: PlacementTarget): string {
|
|
110
|
+
return t.account ? `${t.env} · ${t.account}` : t.env;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** "☁ remote (dev)" for a remote service; "" otherwise. */
|
|
114
|
+
export function placementSuffix(p: Placement): string {
|
|
115
|
+
return p.runs === "remote" && p.target ? `☁ remote (${p.target.env})` : "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Health ──────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export interface HealthCheckPlan {
|
|
121
|
+
placement: Placement;
|
|
122
|
+
/** The check to run, or undefined for none. */
|
|
123
|
+
check?: ServiceHealthConfig;
|
|
124
|
+
/** Set when probing target.url: the codes that count as up. */
|
|
125
|
+
expect?: Expect;
|
|
126
|
+
/** A remote service's row label: its target ("dev · AWS"). */
|
|
127
|
+
label?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expectOf(e: number | number[] | undefined): Expect {
|
|
131
|
+
if (e === undefined) return "2xx";
|
|
132
|
+
return Array.isArray(e) ? e : [e];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The health check for `service` viewed in `env`. The one resolution path for
|
|
137
|
+
* the web health row and `<cli> status`.
|
|
138
|
+
* - local / none: the env's declared check, else `composeShell` (locally);
|
|
139
|
+
* - remote: the check declared for target.env, else a GET of target.url
|
|
140
|
+
* (up when the status is in target.expect, default any 2xx).
|
|
141
|
+
*/
|
|
142
|
+
export function resolveHealthCheck(
|
|
143
|
+
manifest: RawManifest | null | undefined,
|
|
144
|
+
service: string,
|
|
145
|
+
env: string,
|
|
146
|
+
composeShell: string | null,
|
|
147
|
+
): HealthCheckPlan {
|
|
148
|
+
const placement = servicePlacement(manifest, service, env);
|
|
149
|
+
const svc = manifest?.services?.[service];
|
|
150
|
+
if (placement.runs === "remote" && placement.target) {
|
|
151
|
+
const t = placement.target;
|
|
152
|
+
const label = targetLabel(t);
|
|
153
|
+
const declared = svc?.health?.[t.env as keyof typeof svc.health];
|
|
154
|
+
if (declared) return { placement, check: declared, label };
|
|
155
|
+
if (svc?.target?.url && t.url === svc.target.url)
|
|
156
|
+
return {
|
|
157
|
+
placement,
|
|
158
|
+
check: { type: "http", url: t.url },
|
|
159
|
+
expect: expectOf(svc.target.expect),
|
|
160
|
+
label,
|
|
161
|
+
};
|
|
162
|
+
return { placement, label };
|
|
163
|
+
}
|
|
164
|
+
const declared = svc?.health?.[env as keyof typeof svc.health];
|
|
165
|
+
if (declared) return { placement, check: declared };
|
|
166
|
+
if (composeShell)
|
|
167
|
+
return { placement, check: { type: "shell", cmd: composeShell } };
|
|
168
|
+
return { placement };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Whether `status` counts as up under `expect`. */
|
|
172
|
+
export function expectMatches(
|
|
173
|
+
status: number | undefined,
|
|
174
|
+
expect: Expect,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (status === undefined) return false;
|
|
177
|
+
return expect === "2xx"
|
|
178
|
+
? status >= 200 && status < 300
|
|
179
|
+
: expect.includes(status);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Guards ──────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/** A remote service whose target env counts as production. */
|
|
185
|
+
export function isProtectedRemote(p: Placement, isProduction: IsProduction) {
|
|
186
|
+
return p.runs === "remote" && !!p.target && isProduction(p.target.env);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Why Den must not run a command on `address`, or null. Every command on a
|
|
191
|
+
* remote service is refused ("runs in dev (AWS); nothing to start here").
|
|
192
|
+
*/
|
|
193
|
+
export function placementRefusal(
|
|
194
|
+
address: string,
|
|
195
|
+
p: Placement,
|
|
196
|
+
isProduction: IsProduction,
|
|
197
|
+
verb: "start" | "stop" = "start",
|
|
198
|
+
): string | null {
|
|
199
|
+
if (p.runs !== "remote") return null;
|
|
200
|
+
const where = p.target
|
|
201
|
+
? `${p.target.env}${p.target.account ? ` (${p.target.account})` : ""}`
|
|
202
|
+
: "a remote env";
|
|
203
|
+
const prod = isProtectedRemote(p, isProduction)
|
|
204
|
+
? ", which counts as production: it is never acted on from here"
|
|
205
|
+
: "";
|
|
206
|
+
return `${address} runs in ${where}; nothing to ${verb} here${prod}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Rollups ─────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export type RollupPlacement = Runs | "hybrid";
|
|
212
|
+
|
|
213
|
+
export interface PlacementRow extends Placement {
|
|
214
|
+
repo: string;
|
|
215
|
+
service: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function rollup(runs: Runs[]): RollupPlacement {
|
|
219
|
+
const set = new Set(runs.filter((r) => r !== "none"));
|
|
220
|
+
if (set.size === 0) return "none";
|
|
221
|
+
return set.size > 1 ? "hybrid" : [...set][0];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function envsOf(rows: Placement[]): string[] {
|
|
225
|
+
return [
|
|
226
|
+
...new Set(
|
|
227
|
+
rows.flatMap((r) =>
|
|
228
|
+
r.runs === "remote" && r.target ? [r.target.env] : [],
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
].sort();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* An application's placement and a summary per repo, e.g.
|
|
236
|
+
* "shop-frontend local · shop-core remote (dev)".
|
|
237
|
+
*/
|
|
238
|
+
export function rollupPlacement(rows: readonly PlacementRow[]): {
|
|
239
|
+
placement: RollupPlacement;
|
|
240
|
+
summary: string;
|
|
241
|
+
} {
|
|
242
|
+
const byRepo = new Map<string, PlacementRow[]>();
|
|
243
|
+
for (const r of rows) {
|
|
244
|
+
if (!byRepo.has(r.repo)) byRepo.set(r.repo, []);
|
|
245
|
+
byRepo.get(r.repo)!.push(r);
|
|
246
|
+
}
|
|
247
|
+
const parts: string[] = [];
|
|
248
|
+
for (const [repo, list] of byRepo) {
|
|
249
|
+
const p = rollup(list.map((r) => r.runs));
|
|
250
|
+
if (p === "none") continue;
|
|
251
|
+
if (p === "local") parts.push(`${repo} local`);
|
|
252
|
+
else if (p === "remote")
|
|
253
|
+
parts.push(`${repo} remote (${envsOf(list).join(", ")})`);
|
|
254
|
+
else
|
|
255
|
+
parts.push(
|
|
256
|
+
`${repo} hybrid (${list
|
|
257
|
+
.filter((r) => r.runs === "remote")
|
|
258
|
+
.map((r) => `${r.service} remote (${r.target?.env ?? "?"})`)
|
|
259
|
+
.join(", ")})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
placement: rollup(rows.map((r) => r.runs)),
|
|
264
|
+
summary: parts.join(" · "),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** A repo's placement over its manifest's services, and its remote envs. */
|
|
269
|
+
export function repoPlacement(
|
|
270
|
+
manifest: RawManifest | null | undefined,
|
|
271
|
+
env: string,
|
|
272
|
+
): { placement: RollupPlacement; envs: string[] } {
|
|
273
|
+
const list = Object.keys(manifest?.services ?? {}).map((s) =>
|
|
274
|
+
servicePlacement(manifest, s, env),
|
|
275
|
+
);
|
|
276
|
+
return { placement: rollup(list.map((p) => p.runs)), envs: envsOf(list) };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Validation ──────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function isMap(v: unknown): v is Record<string, unknown> {
|
|
282
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Placement errors the schema cannot see: env names in `runs` and
|
|
287
|
+
* `target.env` must be among `environments` (the org's), and a service run
|
|
288
|
+
* remote locally must say where (`target`).
|
|
289
|
+
*/
|
|
290
|
+
export function placementErrors(
|
|
291
|
+
manifest: unknown,
|
|
292
|
+
environments: readonly string[],
|
|
293
|
+
): ValidationError[] {
|
|
294
|
+
if (!isMap(manifest) || !isMap(manifest.services)) return [];
|
|
295
|
+
const known = environments.join(", ");
|
|
296
|
+
const unknownEnv = (path: string, env: string): ValidationError => ({
|
|
297
|
+
path,
|
|
298
|
+
message: `unknown environment "${env}" (known: ${known})`,
|
|
299
|
+
suggestion: "Use one of the org's environments (config `environments:`).",
|
|
300
|
+
});
|
|
301
|
+
const errors: ValidationError[] = [];
|
|
302
|
+
for (const [name, svc] of Object.entries(manifest.services)) {
|
|
303
|
+
if (!isMap(svc)) continue;
|
|
304
|
+
const at = `services.${name}`;
|
|
305
|
+
const runs = isMap(svc.runs) ? svc.runs : {};
|
|
306
|
+
for (const env of Object.keys(runs))
|
|
307
|
+
if (!environments.includes(env))
|
|
308
|
+
errors.push(unknownEnv(`${at}.runs.${env}`, env));
|
|
309
|
+
const target = isMap(svc.target) ? svc.target : null;
|
|
310
|
+
if (target && typeof target.env === "string")
|
|
311
|
+
if (!environments.includes(target.env))
|
|
312
|
+
errors.push(unknownEnv(`${at}.target.env`, target.env));
|
|
313
|
+
if (runs[LOCAL] === "remote" && !target)
|
|
314
|
+
errors.push({
|
|
315
|
+
path: `${at}.target`,
|
|
316
|
+
message: "runs.local is remote: say where it runs with target.env",
|
|
317
|
+
suggestion: "e.g. target: { env: dev, url: https://…/health }",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return errors;
|
|
321
|
+
}
|