@monster-orange/den 0.0.0 → 0.1.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 +67 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +61 -0
- package/docs/distributions.md +256 -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 +99 -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 +406 -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 +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -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/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 +566 -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/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 +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -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-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -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/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -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 +178 -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/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 +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -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 +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -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 +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -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/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 +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -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/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -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 +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -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/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-engineer admin 1Password setup — for environments with no shared admin.
|
|
3
|
+
*
|
|
4
|
+
* Environments where the org config declares a shared admin account
|
|
5
|
+
* (`test_accounts.roles.admin`, optionally limited by its `environments`)
|
|
6
|
+
* need no setup.
|
|
7
|
+
*
|
|
8
|
+
* Elsewhere you need a personal account with admin access. This script lets
|
|
9
|
+
* you point the test pillars at your own 1Password item for those envs.
|
|
10
|
+
*
|
|
11
|
+
* Saves config/local/admin-auth[-{env}].json (gitignored — no password stored)
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* make auth-configure env=staging # your personal staging admin account
|
|
15
|
+
* make auth-configure env=production # your personal production admin account
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFile, spawnSync } from "child_process";
|
|
19
|
+
import { promisify } from "util";
|
|
20
|
+
import * as readline from "readline";
|
|
21
|
+
import * as path from "path";
|
|
22
|
+
import * as fs from "fs";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
import { OP_ACCOUNT_DEFAULT } from "../src/shared/constants.js";
|
|
25
|
+
import {
|
|
26
|
+
readTestAccountsConfig,
|
|
27
|
+
testAccountsVault,
|
|
28
|
+
} from "../src/shared/auth/test-accounts.js";
|
|
29
|
+
|
|
30
|
+
// Ensure op invocations hit the den-owned account even when this script is
|
|
31
|
+
// run directly via `npx tsx` (not through make, which already exports OP_ACCOUNT).
|
|
32
|
+
process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT;
|
|
33
|
+
|
|
34
|
+
const execFileAsync = promisify(execFile);
|
|
35
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
36
|
+
|
|
37
|
+
// ─── 1Password types ──────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
interface OpVault {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface OpItem {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
category: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface OpField {
|
|
51
|
+
id: string;
|
|
52
|
+
label: string;
|
|
53
|
+
value?: string;
|
|
54
|
+
type: string;
|
|
55
|
+
purpose?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface OpItemDetail {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
fields?: OpField[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
async function opJson<T>(args: string[]): Promise<T> {
|
|
67
|
+
const { stdout } = await execFileAsync("op", [...args, "--format=json"]);
|
|
68
|
+
return JSON.parse(stdout) as T;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function prompt(rl: readline.Interface, question: string): Promise<string> {
|
|
72
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function pickFromList<T>(
|
|
76
|
+
rl: readline.Interface,
|
|
77
|
+
items: T[],
|
|
78
|
+
getLabel: (item: T) => string,
|
|
79
|
+
question: string,
|
|
80
|
+
): Promise<T> {
|
|
81
|
+
items.forEach((item, i) => {
|
|
82
|
+
console.log(` ${String(i + 1).padStart(2)}. ${getLabel(item)}`);
|
|
83
|
+
});
|
|
84
|
+
console.log();
|
|
85
|
+
let selected: T | undefined;
|
|
86
|
+
do {
|
|
87
|
+
const answer = await prompt(rl, question);
|
|
88
|
+
const n = parseInt(answer.trim(), 10);
|
|
89
|
+
if (n >= 1 && n <= items.length) {
|
|
90
|
+
selected = items[n - 1];
|
|
91
|
+
} else {
|
|
92
|
+
console.log(` Enter a number between 1 and ${items.length}`);
|
|
93
|
+
}
|
|
94
|
+
} while (selected === undefined);
|
|
95
|
+
return selected;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Defaults ────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/** The org's shared admin item (test_accounts.roles.admin), offered first. */
|
|
101
|
+
function sharedAdminDefault():
|
|
102
|
+
| { vault: string; item: string; field: string }
|
|
103
|
+
| undefined {
|
|
104
|
+
const cfg = readTestAccountsConfig();
|
|
105
|
+
const admin = cfg.roles?.admin;
|
|
106
|
+
if (!admin?.item) return undefined;
|
|
107
|
+
return {
|
|
108
|
+
vault: admin.vault ?? testAccountsVault(cfg),
|
|
109
|
+
item: admin.item,
|
|
110
|
+
field: admin.field ?? "password",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async function main() {
|
|
117
|
+
const environment = process.env.E2E_ENVIRONMENT;
|
|
118
|
+
const configFilename = environment
|
|
119
|
+
? `admin-auth-${environment}.json`
|
|
120
|
+
: "admin-auth.json";
|
|
121
|
+
const configScope = environment
|
|
122
|
+
? `environment: ${environment}`
|
|
123
|
+
: "all environments (generic fallback)";
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
console.log("🔐 Admin 1Password Configuration");
|
|
127
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
128
|
+
console.log(`Scope: ${configScope}`);
|
|
129
|
+
console.log(
|
|
130
|
+
`Saves config/local/${configFilename} (gitignored — no password stored).`,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (environment === "production") {
|
|
134
|
+
console.log();
|
|
135
|
+
console.log(
|
|
136
|
+
" ⚠ Production: for auditability you may want to use your own personal",
|
|
137
|
+
);
|
|
138
|
+
console.log(
|
|
139
|
+
" admin credentials rather than a shared account, so that actions in",
|
|
140
|
+
);
|
|
141
|
+
console.log(" the environment are traceable to you specifically.");
|
|
142
|
+
console.log();
|
|
143
|
+
console.log(
|
|
144
|
+
" Select a vault and item that has your personal admin account email",
|
|
145
|
+
);
|
|
146
|
+
console.log(" and password for production saved in 1Password.");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log();
|
|
150
|
+
|
|
151
|
+
// ── Check op is installed ────────────────────────────────────────────────
|
|
152
|
+
try {
|
|
153
|
+
await execFileAsync("op", ["--version"]);
|
|
154
|
+
} catch {
|
|
155
|
+
console.error("❌ 1Password CLI (op) not found.");
|
|
156
|
+
console.error(" Install it: brew install 1password-cli");
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Check signed in (auto-sign-in if not) ───────────────────────────────
|
|
161
|
+
try {
|
|
162
|
+
await execFileAsync("op", ["whoami"]);
|
|
163
|
+
} catch {
|
|
164
|
+
console.log("🔐 Not signed in to 1Password — signing in now...");
|
|
165
|
+
const result = spawnSync("op", ["signin"], { stdio: "inherit" });
|
|
166
|
+
if (result.status !== 0) {
|
|
167
|
+
console.error("❌ 1Password sign-in failed.");
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
// Verify it worked
|
|
171
|
+
try {
|
|
172
|
+
await execFileAsync("op", ["whoami"]);
|
|
173
|
+
} catch {
|
|
174
|
+
console.error("❌ Still not signed in after op signin.");
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const rl = readline.createInterface({
|
|
180
|
+
input: process.stdin,
|
|
181
|
+
output: process.stdout,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
// ── Step 1: Select vault ────────────────────────────────────────────────
|
|
186
|
+
console.log("Fetching vaults...");
|
|
187
|
+
const vaults = await opJson<OpVault[]>(["vault", "list"]);
|
|
188
|
+
if (vaults.length === 0) {
|
|
189
|
+
console.error("❌ No vaults found.");
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Offer the org's shared admin item (if configured) before the full picker
|
|
194
|
+
const SHARED_ADMIN = sharedAdminDefault();
|
|
195
|
+
if (SHARED_ADMIN) {
|
|
196
|
+
const defaultVault = vaults.find((v) => v.name === SHARED_ADMIN.vault);
|
|
197
|
+
if (defaultVault) {
|
|
198
|
+
console.log(` Default: ${SHARED_ADMIN.vault} / ${SHARED_ADMIN.item}`);
|
|
199
|
+
const answer = await prompt(rl, " Use this default? (Y/n): ");
|
|
200
|
+
if (answer.trim().toLowerCase() !== "n") {
|
|
201
|
+
const allItems = await opJson<OpItem[]>([
|
|
202
|
+
"item",
|
|
203
|
+
"list",
|
|
204
|
+
"--vault",
|
|
205
|
+
defaultVault.id,
|
|
206
|
+
]);
|
|
207
|
+
const defaultItem = allItems.find(
|
|
208
|
+
(i) => i.title === SHARED_ADMIN.item,
|
|
209
|
+
);
|
|
210
|
+
if (defaultItem) {
|
|
211
|
+
const detail = await opJson<OpItemDetail>([
|
|
212
|
+
"item",
|
|
213
|
+
"get",
|
|
214
|
+
defaultItem.id,
|
|
215
|
+
"--vault",
|
|
216
|
+
defaultVault.id,
|
|
217
|
+
]);
|
|
218
|
+
const fields = detail.fields ?? [];
|
|
219
|
+
const passwordField = fields.find(
|
|
220
|
+
(f) => f.label === SHARED_ADMIN.field,
|
|
221
|
+
);
|
|
222
|
+
const emailField = fields.find(
|
|
223
|
+
(f) =>
|
|
224
|
+
f.purpose === "USERNAME" ||
|
|
225
|
+
f.type === "EMAIL" ||
|
|
226
|
+
f.label?.toLowerCase() === "username" ||
|
|
227
|
+
f.label?.toLowerCase() === "email",
|
|
228
|
+
);
|
|
229
|
+
if (passwordField && emailField?.value) {
|
|
230
|
+
const opRef = `op://${defaultVault.name}/${defaultItem.title}/${passwordField.label}`;
|
|
231
|
+
const email = emailField.value;
|
|
232
|
+
const configDir = path.resolve(__dirname, "../config/local");
|
|
233
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
234
|
+
fs.writeFileSync(
|
|
235
|
+
path.join(configDir, configFilename),
|
|
236
|
+
JSON.stringify(
|
|
237
|
+
{
|
|
238
|
+
email,
|
|
239
|
+
opRef,
|
|
240
|
+
itemId: defaultItem.id,
|
|
241
|
+
vaultId: defaultVault.id,
|
|
242
|
+
fieldLabel: passwordField.label,
|
|
243
|
+
},
|
|
244
|
+
null,
|
|
245
|
+
2,
|
|
246
|
+
) + "\n",
|
|
247
|
+
);
|
|
248
|
+
console.log();
|
|
249
|
+
console.log(
|
|
250
|
+
`✅ Admin config saved → config/local/${configFilename}`,
|
|
251
|
+
);
|
|
252
|
+
console.log(` scope: ${configScope}`);
|
|
253
|
+
console.log(` email: ${email}`);
|
|
254
|
+
console.log(` opRef: ${opRef}`);
|
|
255
|
+
console.log();
|
|
256
|
+
console.log(`Run tests: make test-syn-basic env=${environment}`);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
console.log(
|
|
261
|
+
" Default item not found — falling back to manual selection.",
|
|
262
|
+
);
|
|
263
|
+
console.log();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log();
|
|
269
|
+
console.log("Select vault:");
|
|
270
|
+
const vault = await pickFromList(
|
|
271
|
+
rl,
|
|
272
|
+
vaults,
|
|
273
|
+
(v) => v.name,
|
|
274
|
+
"Vault number: ",
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// ── Step 2: Select item ─────────────────────────────────────────────────
|
|
278
|
+
console.log();
|
|
279
|
+
console.log(`Fetching items from "${vault.name}"...`);
|
|
280
|
+
const allItems = await opJson<OpItem[]>([
|
|
281
|
+
"item",
|
|
282
|
+
"list",
|
|
283
|
+
"--vault",
|
|
284
|
+
vault.id,
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
// Prefer Login items, but show all if none; sort alphabetically
|
|
288
|
+
const loginItems = allItems.filter((i) => i.category === "LOGIN");
|
|
289
|
+
const items = (loginItems.length > 0 ? loginItems : allItems).sort((a, b) =>
|
|
290
|
+
a.title.localeCompare(b.title),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (items.length === 0) {
|
|
294
|
+
console.error(`❌ No items found in vault "${vault.name}".`);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
console.log();
|
|
298
|
+
console.log("Select item:");
|
|
299
|
+
const item = await pickFromList(rl, items, (i) => i.title, "Item number: ");
|
|
300
|
+
|
|
301
|
+
// ── Step 3: Inspect fields ──────────────────────────────────────────────
|
|
302
|
+
console.log();
|
|
303
|
+
console.log(`Fetching fields for "${item.title}"...`);
|
|
304
|
+
const detail = await opJson<OpItemDetail>([
|
|
305
|
+
"item",
|
|
306
|
+
"get",
|
|
307
|
+
item.id,
|
|
308
|
+
"--vault",
|
|
309
|
+
vault.id,
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
const fields = detail.fields ?? [];
|
|
313
|
+
|
|
314
|
+
// Detect email from USERNAME / EMAIL fields
|
|
315
|
+
const emailField = fields.find(
|
|
316
|
+
(f) =>
|
|
317
|
+
f.purpose === "USERNAME" ||
|
|
318
|
+
f.type === "EMAIL" ||
|
|
319
|
+
f.label?.toLowerCase() === "username" ||
|
|
320
|
+
f.label?.toLowerCase() === "email",
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
// Find secret fields for the op:// reference
|
|
324
|
+
const secretFields = fields.filter(
|
|
325
|
+
(f) => f.type === "CONCEALED" || f.purpose === "PASSWORD",
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
let passwordField: OpField | undefined;
|
|
329
|
+
if (secretFields.length === 1) {
|
|
330
|
+
passwordField = secretFields[0];
|
|
331
|
+
} else if (secretFields.length > 1) {
|
|
332
|
+
console.log();
|
|
333
|
+
console.log("Select password field:");
|
|
334
|
+
passwordField = await pickFromList(
|
|
335
|
+
rl,
|
|
336
|
+
secretFields,
|
|
337
|
+
(f) => f.label,
|
|
338
|
+
"Field number: ",
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
// Fallback: show all fields
|
|
342
|
+
console.log();
|
|
343
|
+
console.log("No password field detected. Select the credential field:");
|
|
344
|
+
const allNamed = fields.filter((f) => f.label);
|
|
345
|
+
passwordField = await pickFromList(
|
|
346
|
+
rl,
|
|
347
|
+
allNamed,
|
|
348
|
+
(f) => `${f.label} (${f.type})`,
|
|
349
|
+
"Field number: ",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (!passwordField) {
|
|
354
|
+
console.error("❌ No field selected.");
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const opRef = `op://${vault.name}/${item.title}/${passwordField.label}`;
|
|
359
|
+
|
|
360
|
+
// ── Step 4: Confirm / enter email ───────────────────────────────────────
|
|
361
|
+
let email: string;
|
|
362
|
+
console.log();
|
|
363
|
+
if (emailField?.value) {
|
|
364
|
+
const answer = await prompt(rl, `Admin email [${emailField.value}]: `);
|
|
365
|
+
email = answer.trim() || emailField.value;
|
|
366
|
+
} else {
|
|
367
|
+
email = (await prompt(rl, "Admin email (@example.com): ")).trim();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!email) {
|
|
371
|
+
console.error("❌ Email is required.");
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ── Step 5: Save ────────────────────────────────────────────────────────
|
|
376
|
+
// Store item ID + field label alongside opRef.
|
|
377
|
+
// headless-login.ts uses the IDs (via `op item get`) which is robust against
|
|
378
|
+
// special characters in vault/item names that can break `op read op://...`.
|
|
379
|
+
const configDir = path.resolve(__dirname, "../config/local");
|
|
380
|
+
const configFile = path.join(configDir, configFilename);
|
|
381
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
382
|
+
fs.writeFileSync(
|
|
383
|
+
configFile,
|
|
384
|
+
JSON.stringify(
|
|
385
|
+
{
|
|
386
|
+
email,
|
|
387
|
+
opRef,
|
|
388
|
+
itemId: item.id,
|
|
389
|
+
vaultId: vault.id,
|
|
390
|
+
fieldLabel: passwordField.label,
|
|
391
|
+
},
|
|
392
|
+
null,
|
|
393
|
+
2,
|
|
394
|
+
) + "\n",
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
console.log();
|
|
398
|
+
console.log(`✅ Admin config saved → config/local/${configFilename}`);
|
|
399
|
+
console.log(` scope: ${configScope}`);
|
|
400
|
+
console.log(` email: ${email}`);
|
|
401
|
+
console.log(` opRef: ${opRef}`);
|
|
402
|
+
console.log();
|
|
403
|
+
const testCmd = environment
|
|
404
|
+
? `make test-syn-basic env=${environment}`
|
|
405
|
+
: "make test-syn-basic";
|
|
406
|
+
console.log(`Run tests: ${testCmd}`);
|
|
407
|
+
} finally {
|
|
408
|
+
rl.close();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
main().catch((err: Error) => {
|
|
413
|
+
console.error("❌", err.message);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent heal for `config/den.yml`.
|
|
3
|
+
*
|
|
4
|
+
* Ensures required keys are populated so users upgrading from older den
|
|
5
|
+
* installs — who may have `github_org` commented out or absent — don't end
|
|
6
|
+
* up with silent empty repo lists. Safe to run repeatedly; exits 0 with a
|
|
7
|
+
* single "already healthy" line when nothing needs doing.
|
|
8
|
+
*
|
|
9
|
+
* Runs during `make setup`; also available as `make den-heal-config`.
|
|
10
|
+
*
|
|
11
|
+
* Keys healed:
|
|
12
|
+
* - github_org — auto-detected from this den checkout's `origin` URL.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { execFileSync } from "child_process";
|
|
18
|
+
|
|
19
|
+
import { OP_ACCOUNT_DEFAULT } from "../src/shared/constants.js";
|
|
20
|
+
|
|
21
|
+
import { product } from "../src/shared/product.js";
|
|
22
|
+
// Ensure op (if used later) hits the right account when this script runs
|
|
23
|
+
// standalone via npx tsx, matching the pattern in setup-1password.ts et al.
|
|
24
|
+
process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT;
|
|
25
|
+
|
|
26
|
+
const ROOT = process.cwd();
|
|
27
|
+
const CONFIG_PATH = path.join(ROOT, product().configFile);
|
|
28
|
+
|
|
29
|
+
function detectOrgFromGitRemote(): string | null {
|
|
30
|
+
try {
|
|
31
|
+
const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
32
|
+
cwd: ROOT,
|
|
33
|
+
encoding: "utf-8",
|
|
34
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
35
|
+
}).trim();
|
|
36
|
+
// SSH: git@github.com:ORG/den.git
|
|
37
|
+
// HTTPS: https://github.com/ORG/den.git
|
|
38
|
+
// https://github.com/ORG/den
|
|
39
|
+
const m = url.match(/[:/]([^/:]+)\/[^/]+?(?:\.git)?$/);
|
|
40
|
+
return m ? m[1] : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main(): void {
|
|
47
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
48
|
+
console.error(
|
|
49
|
+
` ✗ ${CONFIG_PATH} not found — are you in the ${product().cli} repo root?`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const content = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
55
|
+
|
|
56
|
+
// Already set (uncommented line with a non-empty value) → no-op.
|
|
57
|
+
const activeMatch = content.match(/^github_org:\s*(\S+)/m);
|
|
58
|
+
if (activeMatch) {
|
|
59
|
+
console.log(` ✓ github_org already set to ${activeMatch[1]}`);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const detected = detectOrgFromGitRemote();
|
|
64
|
+
if (!detected) {
|
|
65
|
+
console.error(
|
|
66
|
+
` ✗ could not auto-detect github_org from this ${product().cli} checkout's ` +
|
|
67
|
+
`\`remote.origin.url\`. Add \`github_org: <slug>\` to ${product().configFile} ` +
|
|
68
|
+
"manually.",
|
|
69
|
+
);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let newContent: string;
|
|
74
|
+
if (/^#\s*github_org:/m.test(content)) {
|
|
75
|
+
// Uncomment-and-set the existing commented placeholder, preserving
|
|
76
|
+
// surrounding comments so the file stays readable.
|
|
77
|
+
newContent = content.replace(
|
|
78
|
+
/^#\s*github_org:.*$/m,
|
|
79
|
+
`github_org: ${detected}`,
|
|
80
|
+
);
|
|
81
|
+
} else {
|
|
82
|
+
// No placeholder present — append.
|
|
83
|
+
newContent = content.replace(/\n*$/, "\n") + `github_org: ${detected}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(CONFIG_PATH, newContent);
|
|
87
|
+
console.log(
|
|
88
|
+
` ✓ healed ${product().configFile}: github_org: ${detected} (auto-detected from origin)`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
main();
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Playwright Reporter for JSONL
|
|
3
|
+
* Writes test results to JSONL files for historical tracking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn } from "child_process";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import os from "os";
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const writerScript = path.join(__dirname, "write-result.js");
|
|
13
|
+
|
|
14
|
+
class JSONLReporter {
|
|
15
|
+
onBegin(config, suite) {
|
|
16
|
+
console.log("[JSONL] Reporter initialized");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async onTestEnd(test, result) {
|
|
20
|
+
try {
|
|
21
|
+
// Detect pillar from file path
|
|
22
|
+
const filePath = test.location.file;
|
|
23
|
+
let pillar = "synthetic";
|
|
24
|
+
if (filePath.includes("/integration/")) pillar = "integration";
|
|
25
|
+
else if (filePath.includes("/performance/")) pillar = "performance";
|
|
26
|
+
|
|
27
|
+
// Get environment
|
|
28
|
+
const environment = process.env.TEST_ENV || "local";
|
|
29
|
+
|
|
30
|
+
// Build test name
|
|
31
|
+
const testTitle = test.titlePath().slice(1).join(" › ");
|
|
32
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
33
|
+
const testName = `${fileName} › ${testTitle}`;
|
|
34
|
+
|
|
35
|
+
// Determine status
|
|
36
|
+
let status = "passed";
|
|
37
|
+
if (result.status === "failed") status = "failed";
|
|
38
|
+
else if (result.status === "skipped") status = "skipped";
|
|
39
|
+
else if (result.status === "timedOut") status = "failed";
|
|
40
|
+
|
|
41
|
+
// Build result object
|
|
42
|
+
const resultData = {
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
pillar,
|
|
45
|
+
environment,
|
|
46
|
+
test: testName,
|
|
47
|
+
status,
|
|
48
|
+
duration: result.duration,
|
|
49
|
+
user: process.env.USER || os.userInfo().username || "unknown",
|
|
50
|
+
error: result.error?.message || null,
|
|
51
|
+
retries: result.retry || 0,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Write to JSONL asynchronously
|
|
55
|
+
const child = spawn("node", [writerScript, JSON.stringify(resultData)], {
|
|
56
|
+
stdio: "inherit",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Don't wait for completion to avoid slowing down tests
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(`[JSONL] Failed to write result: ${error.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onEnd(result) {
|
|
66
|
+
console.log(`[JSONL] Results written to results/ directory`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export default JSONLReporter;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Makefile helper — reads a .den.yml or .den.json manifest and
|
|
4
|
+
* outputs a value to stdout.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node scripts/manifest-read.cjs <repo-path> <operation>
|
|
8
|
+
*
|
|
9
|
+
* Operations:
|
|
10
|
+
* smoke-keys — space-separated smoke service names
|
|
11
|
+
* smoke-testdir <svc> — testDir for a specific smoke service
|
|
12
|
+
* synth-testdir — synthetics.testDir
|
|
13
|
+
*
|
|
14
|
+
* Returns empty string (exit 0) if the file/key doesn't exist.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
"use strict";
|
|
18
|
+
|
|
19
|
+
const fs = require("fs");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const yaml = require("yaml");
|
|
22
|
+
|
|
23
|
+
const [, , repoPath, operation, ...rest] = process.argv;
|
|
24
|
+
|
|
25
|
+
if (!repoPath || !operation) {
|
|
26
|
+
console.error("Usage: manifest-read.cjs <repo-path> <operation> [arg]");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The manifest file names, from the distribution's den.yml (`product.manifest`,
|
|
32
|
+
* else `.<product.name>.yml`), found by walking up from the cwd.
|
|
33
|
+
*/
|
|
34
|
+
function manifestNames() {
|
|
35
|
+
let dir = process.cwd();
|
|
36
|
+
for (;;) {
|
|
37
|
+
const f = path.join(dir, "den.yml");
|
|
38
|
+
if (fs.existsSync(f)) {
|
|
39
|
+
const p = (yaml.parse(fs.readFileSync(f, "utf-8")) || {}).product || {};
|
|
40
|
+
const name = (typeof p.name === "string" && p.name.trim()) || "den";
|
|
41
|
+
const yml =
|
|
42
|
+
(typeof p.manifest === "string" && p.manifest.trim()) || `.${name}.yml`;
|
|
43
|
+
return [yml, yml.replace(/\.ya?ml$/, "") + ".json"];
|
|
44
|
+
}
|
|
45
|
+
const up = path.dirname(dir);
|
|
46
|
+
if (up === dir) return [".den.yml", ".den.json"];
|
|
47
|
+
dir = up;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readManifest(dir) {
|
|
52
|
+
for (const name of manifestNames()) {
|
|
53
|
+
const p = path.join(dir, name);
|
|
54
|
+
if (!fs.existsSync(p)) continue;
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
57
|
+
return name.endsWith(".yml") ? yaml.parse(raw) : JSON.parse(raw);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const manifest = readManifest(repoPath);
|
|
66
|
+
if (!manifest) {
|
|
67
|
+
process.stdout.write("");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
switch (operation) {
|
|
72
|
+
case "smoke-keys": {
|
|
73
|
+
const keys = Object.keys(manifest.smoke || {});
|
|
74
|
+
process.stdout.write(keys.join(" "));
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "smoke-testdir": {
|
|
78
|
+
const svc = rest[0];
|
|
79
|
+
const dir = manifest.smoke?.[svc]?.testDir ?? "";
|
|
80
|
+
process.stdout.write(dir);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "synth-testdir": {
|
|
84
|
+
const dir = manifest.synthetics?.testDir ?? "(no synthetics key)";
|
|
85
|
+
process.stdout.write(dir);
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
console.error(`Unknown operation: ${operation}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|