@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,436 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Results Viewer
|
|
3
|
+
* Display test results in CLI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Table from "cli-table3";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { resultReader } from "../../shared/results/result-reader.js";
|
|
9
|
+
import type {
|
|
10
|
+
Pillar,
|
|
11
|
+
Environment,
|
|
12
|
+
TestResult,
|
|
13
|
+
} from "../../shared/types/index.js";
|
|
14
|
+
|
|
15
|
+
/** Journeys row metadata extracted from the JSONL row (all optional on the wire). */
|
|
16
|
+
interface JourneyRowMetadata {
|
|
17
|
+
failureKind: string | null;
|
|
18
|
+
frictionCount: number;
|
|
19
|
+
snapshotCount: number;
|
|
20
|
+
snapshotsDir: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ResultsViewer {
|
|
24
|
+
/**
|
|
25
|
+
* Display summary of recent results
|
|
26
|
+
*/
|
|
27
|
+
async displaySummary(days: number = 7): Promise<void> {
|
|
28
|
+
console.log(chalk.bold(`\n📊 Test Results Summary (Last ${days} days)`));
|
|
29
|
+
console.log(chalk.gray("━".repeat(80)));
|
|
30
|
+
|
|
31
|
+
const pillars: Pillar[] = [
|
|
32
|
+
"synthetic",
|
|
33
|
+
"integration",
|
|
34
|
+
"performance",
|
|
35
|
+
"journeys",
|
|
36
|
+
"security",
|
|
37
|
+
];
|
|
38
|
+
const environments: Environment[] = ["local", "dev", "sandbox", "staging"];
|
|
39
|
+
|
|
40
|
+
for (const pillar of pillars) {
|
|
41
|
+
console.log(chalk.bold(`\n${this.formatPillarName(pillar)}:`));
|
|
42
|
+
|
|
43
|
+
const table = new Table({
|
|
44
|
+
head: [
|
|
45
|
+
chalk.cyan("Environment"),
|
|
46
|
+
chalk.cyan("Total"),
|
|
47
|
+
chalk.green("Passed"),
|
|
48
|
+
chalk.red("Failed"),
|
|
49
|
+
chalk.yellow("Skipped"),
|
|
50
|
+
chalk.blue("Pass Rate"),
|
|
51
|
+
],
|
|
52
|
+
style: {
|
|
53
|
+
head: [],
|
|
54
|
+
border: ["gray"],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
for (const env of environments) {
|
|
59
|
+
const summary = await resultReader.getSummary(pillar, env, days);
|
|
60
|
+
|
|
61
|
+
if (summary.total === 0) {
|
|
62
|
+
continue; // Skip environments with no results
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
table.push([
|
|
66
|
+
this.formatEnvironmentName(env),
|
|
67
|
+
summary.total.toString(),
|
|
68
|
+
chalk.green(summary.passed.toString()),
|
|
69
|
+
summary.failed > 0
|
|
70
|
+
? chalk.red(summary.failed.toString())
|
|
71
|
+
: chalk.gray("0"),
|
|
72
|
+
summary.skipped > 0
|
|
73
|
+
? chalk.yellow(summary.skipped.toString())
|
|
74
|
+
: chalk.gray("0"),
|
|
75
|
+
this.formatPassRate(summary.passRate),
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (table.length === 0) {
|
|
80
|
+
console.log(chalk.gray(" No results found"));
|
|
81
|
+
} else {
|
|
82
|
+
console.log(table.toString());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log(); // Empty line at end
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Display detailed results for specific pillar/environment
|
|
91
|
+
*/
|
|
92
|
+
async displayDetailed(
|
|
93
|
+
pillar: Pillar,
|
|
94
|
+
environment: Environment,
|
|
95
|
+
days: number = 7,
|
|
96
|
+
): Promise<void> {
|
|
97
|
+
if (pillar === "journeys") {
|
|
98
|
+
return this.displayJourneysDetailed(environment, days);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(
|
|
102
|
+
chalk.bold(
|
|
103
|
+
`\n📋 Detailed Results: ${this.formatPillarName(pillar)} / ${this.formatEnvironmentName(environment)}`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
console.log(chalk.gray("━".repeat(80)));
|
|
107
|
+
|
|
108
|
+
const dateFrom = new Date();
|
|
109
|
+
dateFrom.setDate(dateFrom.getDate() - days);
|
|
110
|
+
|
|
111
|
+
const results = await resultReader.query({ pillar, environment, dateFrom });
|
|
112
|
+
|
|
113
|
+
if (results.length === 0) {
|
|
114
|
+
console.log(chalk.yellow("No results found"));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const table = new Table({
|
|
119
|
+
head: [
|
|
120
|
+
chalk.cyan("Test"),
|
|
121
|
+
chalk.cyan("Status"),
|
|
122
|
+
chalk.cyan("Duration"),
|
|
123
|
+
chalk.cyan("User"),
|
|
124
|
+
chalk.cyan("Time"),
|
|
125
|
+
],
|
|
126
|
+
colWidths: [40, 10, 12, 20, 20],
|
|
127
|
+
style: {
|
|
128
|
+
head: [],
|
|
129
|
+
border: ["gray"],
|
|
130
|
+
},
|
|
131
|
+
wordWrap: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Show only recent 20 results
|
|
135
|
+
const recentResults = results.slice(0, 20);
|
|
136
|
+
|
|
137
|
+
for (const result of recentResults) {
|
|
138
|
+
table.push([
|
|
139
|
+
this.truncate(result.test, 38),
|
|
140
|
+
this.formatStatus(result.status),
|
|
141
|
+
this.formatDuration(result.duration),
|
|
142
|
+
this.truncate(result.user, 18),
|
|
143
|
+
this.formatTimestamp(result.timestamp),
|
|
144
|
+
]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(table.toString());
|
|
148
|
+
|
|
149
|
+
if (results.length > 20) {
|
|
150
|
+
console.log(chalk.gray(`\n... and ${results.length - 20} more results`));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Show summary
|
|
154
|
+
const summary = await resultReader.getSummary(pillar, environment, days);
|
|
155
|
+
console.log(chalk.bold("\nSummary:"));
|
|
156
|
+
console.log(` Total: ${summary.total}`);
|
|
157
|
+
console.log(` ${chalk.green("Passed:")} ${summary.passed}`);
|
|
158
|
+
console.log(` ${chalk.red("Failed:")} ${summary.failed}`);
|
|
159
|
+
console.log(` ${chalk.yellow("Skipped:")} ${summary.skipped}`);
|
|
160
|
+
console.log(` Pass Rate: ${this.formatPassRate(summary.passRate)}`);
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Display journeys results — one row per journey run from the JSONL
|
|
166
|
+
* rows, surfacing the deterministic verdict, failure_kind, friction
|
|
167
|
+
* count, and the snapshots directory for the run.
|
|
168
|
+
*/
|
|
169
|
+
async displayJourneysDetailed(
|
|
170
|
+
environment: Environment,
|
|
171
|
+
days: number = 7,
|
|
172
|
+
): Promise<void> {
|
|
173
|
+
console.log(
|
|
174
|
+
chalk.bold(
|
|
175
|
+
`\n🧭 Journey Results: ${this.formatEnvironmentName(environment)}`,
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
console.log(chalk.gray("━".repeat(80)));
|
|
179
|
+
|
|
180
|
+
const dateFrom = new Date();
|
|
181
|
+
dateFrom.setDate(dateFrom.getDate() - days);
|
|
182
|
+
|
|
183
|
+
const results = await resultReader.query({
|
|
184
|
+
pillar: "journeys",
|
|
185
|
+
environment,
|
|
186
|
+
dateFrom,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
if (results.length === 0) {
|
|
190
|
+
console.log(chalk.yellow("No results found"));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const table = new Table({
|
|
195
|
+
head: [
|
|
196
|
+
chalk.cyan("Journey"),
|
|
197
|
+
chalk.cyan("Verdict"),
|
|
198
|
+
chalk.cyan("Failure kind"),
|
|
199
|
+
chalk.cyan("Friction"),
|
|
200
|
+
chalk.cyan("Snapshots"),
|
|
201
|
+
chalk.cyan("Time"),
|
|
202
|
+
],
|
|
203
|
+
style: {
|
|
204
|
+
head: [],
|
|
205
|
+
border: ["gray"],
|
|
206
|
+
},
|
|
207
|
+
wordWrap: true,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const recentResults = results.slice(0, 20);
|
|
211
|
+
for (const result of recentResults) {
|
|
212
|
+
const meta = this.journeyRowMetadata(result);
|
|
213
|
+
table.push([
|
|
214
|
+
this.truncate(result.test, 38),
|
|
215
|
+
this.formatStatus(result.status),
|
|
216
|
+
meta.failureKind !== null
|
|
217
|
+
? chalk.red(meta.failureKind)
|
|
218
|
+
: chalk.gray("—"),
|
|
219
|
+
meta.frictionCount > 0
|
|
220
|
+
? chalk.yellow(meta.frictionCount.toString())
|
|
221
|
+
: chalk.gray("0"),
|
|
222
|
+
meta.snapshotsDir !== null
|
|
223
|
+
? `${meta.snapshotCount} · ${meta.snapshotsDir}`
|
|
224
|
+
: meta.snapshotCount.toString(),
|
|
225
|
+
this.formatTimestamp(result.timestamp),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(table.toString());
|
|
230
|
+
|
|
231
|
+
if (results.length > 20) {
|
|
232
|
+
console.log(chalk.gray(`\n... and ${results.length - 20} more results`));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const summary = await resultReader.getSummary(
|
|
236
|
+
"journeys",
|
|
237
|
+
environment,
|
|
238
|
+
days,
|
|
239
|
+
);
|
|
240
|
+
console.log(chalk.bold("\nSummary:"));
|
|
241
|
+
console.log(` Total: ${summary.total}`);
|
|
242
|
+
console.log(` ${chalk.green("Passed:")} ${summary.passed}`);
|
|
243
|
+
console.log(` ${chalk.red("Failed:")} ${summary.failed}`);
|
|
244
|
+
console.log(` Pass Rate: ${this.formatPassRate(summary.passRate)}`);
|
|
245
|
+
console.log();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Extract the journeys-specific metadata block from a JSONL row,
|
|
250
|
+
* tolerating rows written by older den versions (missing fields
|
|
251
|
+
* degrade to zero/absent — never a crash while browsing results).
|
|
252
|
+
*/
|
|
253
|
+
private journeyRowMetadata(result: TestResult): JourneyRowMetadata {
|
|
254
|
+
const metadata = result.metadata ?? {};
|
|
255
|
+
const failureKind = metadata.failure_kind;
|
|
256
|
+
const frictionPoints = metadata.friction_points;
|
|
257
|
+
const snapshotCount = metadata.snapshot_count;
|
|
258
|
+
const snapshotsDir = metadata.snapshots_dir;
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
failureKind: typeof failureKind === "string" ? failureKind : null,
|
|
262
|
+
frictionCount: Array.isArray(frictionPoints) ? frictionPoints.length : 0,
|
|
263
|
+
snapshotCount: typeof snapshotCount === "number" ? snapshotCount : 0,
|
|
264
|
+
snapshotsDir: typeof snapshotsDir === "string" ? snapshotsDir : null,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Display flaky tests
|
|
270
|
+
*/
|
|
271
|
+
async displayFlakyTests(pillar: Pillar, days: number = 30): Promise<void> {
|
|
272
|
+
console.log(
|
|
273
|
+
chalk.bold(`\n⚠️ Flaky Tests: ${this.formatPillarName(pillar)}`),
|
|
274
|
+
);
|
|
275
|
+
console.log(chalk.gray("━".repeat(80)));
|
|
276
|
+
|
|
277
|
+
const flakyTests = await resultReader.getFlakyTests(pillar, days);
|
|
278
|
+
|
|
279
|
+
if (flakyTests.length === 0) {
|
|
280
|
+
console.log(chalk.green("✓ No flaky tests found!"));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const table = new Table({
|
|
285
|
+
head: [
|
|
286
|
+
chalk.cyan("Test"),
|
|
287
|
+
chalk.cyan("Runs"),
|
|
288
|
+
chalk.cyan("Failures"),
|
|
289
|
+
chalk.cyan("Flaky Rate"),
|
|
290
|
+
],
|
|
291
|
+
colWidths: [50, 10, 12, 15],
|
|
292
|
+
style: {
|
|
293
|
+
head: [],
|
|
294
|
+
border: ["gray"],
|
|
295
|
+
},
|
|
296
|
+
wordWrap: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
for (const flaky of flakyTests) {
|
|
300
|
+
table.push([
|
|
301
|
+
this.truncate(flaky.test, 48),
|
|
302
|
+
flaky.runs.toString(),
|
|
303
|
+
chalk.red(flaky.failures.toString()),
|
|
304
|
+
this.formatFlakyRate(flaky.flakyRate),
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(table.toString());
|
|
309
|
+
console.log();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Format pillar name for display
|
|
314
|
+
*/
|
|
315
|
+
private formatPillarName(pillar: Pillar): string {
|
|
316
|
+
const names: Record<Pillar, string> = {
|
|
317
|
+
synthetic: "🌐 Synthetic",
|
|
318
|
+
integration: "🔗 Integration",
|
|
319
|
+
performance: "⚡ Performance",
|
|
320
|
+
smoke: "🔥 Smoke",
|
|
321
|
+
journeys: "🧭 Journeys",
|
|
322
|
+
security: "🛡 Security",
|
|
323
|
+
suites: "🧪 Suites",
|
|
324
|
+
};
|
|
325
|
+
return names[pillar];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Format environment name for display
|
|
330
|
+
*/
|
|
331
|
+
private formatEnvironmentName(env: Environment): string {
|
|
332
|
+
const names: Record<Environment, string> = {
|
|
333
|
+
local: "🏠 Local",
|
|
334
|
+
dev: "🔧 Dev",
|
|
335
|
+
sandbox: "🏖️ Sandbox",
|
|
336
|
+
staging: "🎭 Staging",
|
|
337
|
+
beta: "🧪 Beta",
|
|
338
|
+
prod: "🚀 Prod",
|
|
339
|
+
production: "🚀 Production",
|
|
340
|
+
};
|
|
341
|
+
return names[env];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Format test status with color
|
|
346
|
+
*/
|
|
347
|
+
private formatStatus(status: "passed" | "failed" | "skipped"): string {
|
|
348
|
+
switch (status) {
|
|
349
|
+
case "passed":
|
|
350
|
+
return chalk.green("✓ Passed");
|
|
351
|
+
case "failed":
|
|
352
|
+
return chalk.red("✗ Failed");
|
|
353
|
+
case "skipped":
|
|
354
|
+
return chalk.yellow("○ Skipped");
|
|
355
|
+
default:
|
|
356
|
+
return status;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Format duration in human-readable format
|
|
362
|
+
*/
|
|
363
|
+
private formatDuration(ms: number): string {
|
|
364
|
+
if (ms < 1000) {
|
|
365
|
+
return `${ms}ms`;
|
|
366
|
+
}
|
|
367
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Format timestamp in relative time
|
|
372
|
+
*/
|
|
373
|
+
private formatTimestamp(timestamp: string): string {
|
|
374
|
+
const date = new Date(timestamp);
|
|
375
|
+
const now = new Date();
|
|
376
|
+
const diffMs = now.getTime() - date.getTime();
|
|
377
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
378
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
379
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
380
|
+
|
|
381
|
+
if (diffMins < 1) {
|
|
382
|
+
return "just now";
|
|
383
|
+
} else if (diffMins < 60) {
|
|
384
|
+
return `${diffMins}m ago`;
|
|
385
|
+
} else if (diffHours < 24) {
|
|
386
|
+
return `${diffHours}h ago`;
|
|
387
|
+
} else if (diffDays < 7) {
|
|
388
|
+
return `${diffDays}d ago`;
|
|
389
|
+
} else {
|
|
390
|
+
return date.toLocaleDateString();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Format pass rate with color
|
|
396
|
+
*/
|
|
397
|
+
private formatPassRate(rate: number): string {
|
|
398
|
+
const rateStr = `${rate.toFixed(1)}%`;
|
|
399
|
+
|
|
400
|
+
if (rate >= 95) {
|
|
401
|
+
return chalk.green(rateStr);
|
|
402
|
+
} else if (rate >= 80) {
|
|
403
|
+
return chalk.yellow(rateStr);
|
|
404
|
+
} else {
|
|
405
|
+
return chalk.red(rateStr);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Format flaky rate with color
|
|
411
|
+
*/
|
|
412
|
+
private formatFlakyRate(rate: number): string {
|
|
413
|
+
const rateStr = `${rate.toFixed(1)}%`;
|
|
414
|
+
|
|
415
|
+
if (rate >= 50) {
|
|
416
|
+
return chalk.red(rateStr);
|
|
417
|
+
} else if (rate >= 20) {
|
|
418
|
+
return chalk.yellow(rateStr);
|
|
419
|
+
} else {
|
|
420
|
+
return chalk.gray(rateStr);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Truncate string with ellipsis
|
|
426
|
+
*/
|
|
427
|
+
private truncate(str: string, maxLength: number): string {
|
|
428
|
+
if (str.length <= maxLength) {
|
|
429
|
+
return str;
|
|
430
|
+
}
|
|
431
|
+
return str.substring(0, maxLength - 3) + "...";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Export singleton instance
|
|
436
|
+
export const resultsViewer = new ResultsViewer();
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets preflight for `den up` / `restart` / `logs`.
|
|
3
|
+
*
|
|
4
|
+
* Backend-agnostic: shells out to `scripts/setup-secrets.ts --gate`, which
|
|
5
|
+
* dispatches to the configured strategy (Doppler, 1Password or env — see
|
|
6
|
+
* src/shared/secrets). A backend that is not ready only warns unless
|
|
7
|
+
* `secrets.required` is true. Synchronous so the CLI can gate before spawning work.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
spawnSync,
|
|
11
|
+
type SpawnSyncOptionsWithStringEncoding,
|
|
12
|
+
type SpawnSyncReturns,
|
|
13
|
+
} from "child_process";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import { getDistRoot, getEngineRoot } from "../../shared/paths.js";
|
|
16
|
+
import { engineBin } from "../../shared/engine.js";
|
|
17
|
+
|
|
18
|
+
import { product } from "../../shared/product.js";
|
|
19
|
+
type SpawnSyncLike = (
|
|
20
|
+
command: string,
|
|
21
|
+
args: string[],
|
|
22
|
+
options: SpawnSyncOptionsWithStringEncoding,
|
|
23
|
+
) => SpawnSyncReturns<string>;
|
|
24
|
+
|
|
25
|
+
export interface SecretsPreflightInvocation {
|
|
26
|
+
command: string;
|
|
27
|
+
args: string[];
|
|
28
|
+
cwd: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SecretsPreflightOptions {
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
root?: string;
|
|
34
|
+
platform?: NodeJS.Platform;
|
|
35
|
+
spawnSyncImpl?: SpawnSyncLike;
|
|
36
|
+
stdio?: SpawnSyncOptionsWithStringEncoding["stdio"];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const truthy = (v: string | undefined) => v === "1" || v === "true";
|
|
40
|
+
|
|
41
|
+
export function shouldSkipSecretsPreflight(
|
|
42
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
43
|
+
): boolean {
|
|
44
|
+
return truthy(env.DEN_SKIP_SECRETS);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The gate runs Den's own script (engine root) against the distribution
|
|
49
|
+
* (cwd = its root, where the org config lives).
|
|
50
|
+
*/
|
|
51
|
+
export function buildSecretsPreflightInvocation(
|
|
52
|
+
root = getDistRoot(),
|
|
53
|
+
platform: NodeJS.Platform = process.platform,
|
|
54
|
+
engine = getEngineRoot(),
|
|
55
|
+
): SecretsPreflightInvocation {
|
|
56
|
+
return {
|
|
57
|
+
command: engineBin(platform === "win32" ? "tsx.cmd" : "tsx", engine),
|
|
58
|
+
args: [path.join(engine, "scripts", "setup-secrets.ts"), "--gate"],
|
|
59
|
+
cwd: root,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function preflightSecrets(options: SecretsPreflightOptions = {}): void {
|
|
64
|
+
const env = options.env ?? process.env;
|
|
65
|
+
if (shouldSkipSecretsPreflight(env)) return;
|
|
66
|
+
|
|
67
|
+
const invocation = buildSecretsPreflightInvocation(
|
|
68
|
+
options.root,
|
|
69
|
+
options.platform,
|
|
70
|
+
);
|
|
71
|
+
const result = (options.spawnSyncImpl ?? spawnSync)(
|
|
72
|
+
invocation.command,
|
|
73
|
+
invocation.args,
|
|
74
|
+
{
|
|
75
|
+
cwd: invocation.cwd,
|
|
76
|
+
env: { ...process.env, ...env },
|
|
77
|
+
stdio: options.stdio ?? "inherit",
|
|
78
|
+
encoding: "utf-8",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (result.status === 0) return;
|
|
83
|
+
|
|
84
|
+
const detail = result.error ? `\n${result.error.message}` : "";
|
|
85
|
+
throw new Error(
|
|
86
|
+
"Developer secrets are not configured or the secrets backend is not signed in.\n" +
|
|
87
|
+
`Run: make ${product().cli}-setup-secrets\n` +
|
|
88
|
+
`Bypass once: ${product().cli} up --skip-secrets or ${product().cli} restart --skip-secrets` +
|
|
89
|
+
detail,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `stack ls | up <name…> [--build] | down <name…> | restart <name…> | health <name…> | ps <name> | logs <name> [args]`
|
|
3
|
+
*
|
|
4
|
+
* Runs repos' Docker Compose projects with no host ports, each as its own
|
|
5
|
+
* project (`<stack>-<workspace>`), served by the edge at
|
|
6
|
+
* https://<route>.<stack>.<edge.domain>.
|
|
7
|
+
*/
|
|
8
|
+
import chalk from "chalk";
|
|
9
|
+
import { routeUrl } from "../../shared/edge/render.js";
|
|
10
|
+
import { edgeUp } from "../../shared/edge/runtime.js";
|
|
11
|
+
import { projectName, startOrder } from "../../shared/stacks/render.js";
|
|
12
|
+
import {
|
|
13
|
+
stackDown,
|
|
14
|
+
stackHealth,
|
|
15
|
+
stackLogs,
|
|
16
|
+
stackPs,
|
|
17
|
+
stackUp,
|
|
18
|
+
} from "../../shared/stacks/runtime.js";
|
|
19
|
+
import { loadStackContext, stackStatuses } from "../../shared/stacks/status.js";
|
|
20
|
+
|
|
21
|
+
function serviceFlag(args: string[]): { service?: string; rest: string[] } {
|
|
22
|
+
const rest: string[] = [];
|
|
23
|
+
let service: string | undefined;
|
|
24
|
+
for (let i = 0; i < args.length; i++) {
|
|
25
|
+
if (args[i] === "--service") service = args[++i];
|
|
26
|
+
else rest.push(args[i]);
|
|
27
|
+
}
|
|
28
|
+
return { service, rest };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function cmdStack(argv: string[]): Promise<void> {
|
|
32
|
+
const { service, rest } = serviceFlag(argv);
|
|
33
|
+
const [sub = "ls", ...args] = rest;
|
|
34
|
+
// The global -w/--workspace is parsed (and checked) before we run and
|
|
35
|
+
// arrives as DEN_WORKSPACE_OVERRIDE; loadStackContext reads it.
|
|
36
|
+
const ctx = loadStackContext();
|
|
37
|
+
const { edge, stacks } = ctx;
|
|
38
|
+
const names = args.filter((a) => !a.startsWith("-"));
|
|
39
|
+
const pick = () => {
|
|
40
|
+
if (!names.length)
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Usage: stack ${sub} <name…> (known: ${Object.keys(stacks).join(", ")})`,
|
|
43
|
+
);
|
|
44
|
+
return names;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
switch (sub) {
|
|
48
|
+
case "ls":
|
|
49
|
+
for (const s of stackStatuses(ctx)) {
|
|
50
|
+
console.log(
|
|
51
|
+
chalk.bold(`${s.name}`) +
|
|
52
|
+
chalk.gray(
|
|
53
|
+
` ${s.repo}${s.dir ? `/${s.dir}` : ""} project ${s.project}`,
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
for (const route of s.routes)
|
|
57
|
+
console.log(` ${route.url} → ${route.target}`);
|
|
58
|
+
if (s.uses.length)
|
|
59
|
+
console.log(chalk.gray(` uses ${s.uses.join(", ")}`));
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
case "up": {
|
|
63
|
+
// Re-render the edge so its cert covers every stack, then start the stacks.
|
|
64
|
+
await edgeUp(edge);
|
|
65
|
+
for (const n of startOrder(pick(), stacks)) {
|
|
66
|
+
console.log(chalk.cyan(`\n▶ ${n} (${projectName(n, ctx.workspace)})`));
|
|
67
|
+
stackUp(ctx, stacks[n], { build: args.includes("--build") });
|
|
68
|
+
for (const route of Object.keys(stacks[n].routes))
|
|
69
|
+
console.log(chalk.green(` ✓ ${routeUrl(`${route}.${n}`, edge)}`));
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case "restart":
|
|
74
|
+
await edgeUp(edge);
|
|
75
|
+
for (const n of pick()) {
|
|
76
|
+
console.log(chalk.cyan(`↻ ${n}`));
|
|
77
|
+
stackDown(ctx, stacks[n]);
|
|
78
|
+
stackUp(ctx, stacks[n], { build: args.includes("--build") });
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
case "health": {
|
|
82
|
+
let ok = true;
|
|
83
|
+
for (const n of pick()) {
|
|
84
|
+
const h = stackHealth(ctx, stacks[n], service);
|
|
85
|
+
ok &&= h.ok;
|
|
86
|
+
console.log(
|
|
87
|
+
`${h.ok ? chalk.green("✓") : chalk.red("✗")} ${n} ${h.detail}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
process.exitCode = ok ? 0 : 1;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
case "down":
|
|
94
|
+
for (const n of startOrder(pick(), stacks)
|
|
95
|
+
.reverse()
|
|
96
|
+
.filter((n) => names.includes(n))) {
|
|
97
|
+
console.log(chalk.cyan(`▼ ${n}`));
|
|
98
|
+
stackDown(ctx, stacks[n]);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
case "ps":
|
|
102
|
+
for (const n of pick())
|
|
103
|
+
console.log(chalk.bold(n) + "\n" + stackPs(ctx, stacks[n]));
|
|
104
|
+
return;
|
|
105
|
+
case "logs":
|
|
106
|
+
process.exitCode = stackLogs(
|
|
107
|
+
ctx,
|
|
108
|
+
stacks[pick()[0]],
|
|
109
|
+
args.filter((a) => a !== names[0]),
|
|
110
|
+
);
|
|
111
|
+
return;
|
|
112
|
+
default:
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Unknown stack command "${sub}". Use: ls | up | down | restart | health | ps | logs`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|