@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,570 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1Password Developer API Keys setup.
|
|
3
|
+
*
|
|
4
|
+
* Creates and populates the developer-keys item (OP_DEV_KEYS_ITEM) inside the
|
|
5
|
+
* personal vault (OP_API_KEYS_VAULT), which services can use to inject
|
|
6
|
+
* secrets at `make up` time via `op run --env-file=.env.op`.
|
|
7
|
+
*
|
|
8
|
+
* Which keys to collect is org data: `secrets.developer_keys.fields` in the
|
|
9
|
+
* org config (default: only the keys the engine itself uses), and
|
|
10
|
+
* `secrets.developer_keys.env_file` names a dotenv file whose values are
|
|
11
|
+
* offered as defaults.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* make den-setup-1password — full interactive setup
|
|
15
|
+
* npx tsx scripts/setup-1password.ts --check — non-interactive vault check
|
|
16
|
+
* exits 0 if ready, 1 if not
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execFile, spawnSync } from "child_process";
|
|
20
|
+
import { promisify } from "util";
|
|
21
|
+
import * as fs from "fs";
|
|
22
|
+
import * as path from "path";
|
|
23
|
+
import { fileURLToPath } from "url";
|
|
24
|
+
import chalk from "chalk";
|
|
25
|
+
import {
|
|
26
|
+
OP_ACCOUNT_DEFAULT,
|
|
27
|
+
OP_API_KEYS_VAULT,
|
|
28
|
+
OP_DEV_KEYS_ITEM,
|
|
29
|
+
} from "../src/shared/constants.js";
|
|
30
|
+
import {
|
|
31
|
+
readSecretsConfig,
|
|
32
|
+
type DeveloperKeyDef,
|
|
33
|
+
} from "../src/shared/secrets/index.js";
|
|
34
|
+
|
|
35
|
+
import { product } from "../src/shared/product.js";
|
|
36
|
+
// Ensure op invocations hit the den-owned account even when this script is
|
|
37
|
+
// run directly via `npx tsx` (not through make, which already exports OP_ACCOUNT).
|
|
38
|
+
process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT;
|
|
39
|
+
|
|
40
|
+
const execFileAsync = promisify(execFile);
|
|
41
|
+
|
|
42
|
+
async function opJson<T>(args: string[]): Promise<T> {
|
|
43
|
+
const { stdout } = await execFileAsync("op", [...args, "--format=json"]);
|
|
44
|
+
return JSON.parse(stdout) as T;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
const DEN_ROOT = path.resolve(__dirname, "..");
|
|
51
|
+
|
|
52
|
+
const VAULT_NAME = OP_API_KEYS_VAULT;
|
|
53
|
+
const ITEM_NAME = OP_DEV_KEYS_ITEM;
|
|
54
|
+
|
|
55
|
+
// ─── .env reader ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse a .env file and return key→value pairs.
|
|
59
|
+
* Handles quoted values, inline comments, and blank/comment lines.
|
|
60
|
+
*/
|
|
61
|
+
function parseEnvFile(filePath: string): Record<string, string> {
|
|
62
|
+
let raw: string;
|
|
63
|
+
try {
|
|
64
|
+
raw = fs.readFileSync(filePath, "utf-8");
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
const result: Record<string, string> = {};
|
|
69
|
+
for (const line of raw.split("\n")) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
72
|
+
const eq = trimmed.indexOf("=");
|
|
73
|
+
if (eq === -1) continue;
|
|
74
|
+
const key = trimmed.slice(0, eq).trim();
|
|
75
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
76
|
+
// Strip inline comments (unquoted)
|
|
77
|
+
if (!val.startsWith('"') && !val.startsWith("'")) {
|
|
78
|
+
val = val.split(/\s+#/)[0].trim();
|
|
79
|
+
}
|
|
80
|
+
// Strip surrounding quotes
|
|
81
|
+
if (
|
|
82
|
+
(val.startsWith('"') && val.endsWith('"')) ||
|
|
83
|
+
(val.startsWith("'") && val.endsWith("'"))
|
|
84
|
+
) {
|
|
85
|
+
val = val.slice(1, -1);
|
|
86
|
+
}
|
|
87
|
+
if (key && val) result[key] = val;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const DEVELOPER_KEYS = readSecretsConfig().developer_keys ?? {};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The dotenv file `secrets.developer_keys.env_file` names (relative to the
|
|
96
|
+
* den root), or undefined when none is configured.
|
|
97
|
+
*/
|
|
98
|
+
const DOTENV_PATH: string | undefined = DEVELOPER_KEYS.env_file
|
|
99
|
+
? path.resolve(DEN_ROOT, DEVELOPER_KEYS.env_file)
|
|
100
|
+
: undefined;
|
|
101
|
+
|
|
102
|
+
/** Parsed key→value map of DOTENV_PATH, or {} when absent. */
|
|
103
|
+
function loadSourceDotEnv(): Record<string, string> {
|
|
104
|
+
if (!DOTENV_PATH || !fs.existsSync(DOTENV_PATH)) return {};
|
|
105
|
+
return parseEnvFile(DOTENV_PATH);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface SecretDef {
|
|
109
|
+
field: string;
|
|
110
|
+
description: string;
|
|
111
|
+
url: string;
|
|
112
|
+
required: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The keys the engine itself uses, when the org config lists none. */
|
|
116
|
+
const DEFAULT_SECRETS: DeveloperKeyDef[] = [
|
|
117
|
+
{
|
|
118
|
+
field: "CURSOR_API_KEY",
|
|
119
|
+
description: "Cursor API key (journeys pillar agent)",
|
|
120
|
+
url: "https://cursor.com/dashboard",
|
|
121
|
+
required: false,
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
const SECRETS: SecretDef[] = (
|
|
126
|
+
DEVELOPER_KEYS.fields?.length ? DEVELOPER_KEYS.fields : DEFAULT_SECRETS
|
|
127
|
+
).map((d) => ({
|
|
128
|
+
field: d.field,
|
|
129
|
+
description: d.description ?? "",
|
|
130
|
+
url: d.url ?? "",
|
|
131
|
+
required: d.required === true,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// ─── 1Password types ──────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
interface OpVault {
|
|
137
|
+
id: string;
|
|
138
|
+
name: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface OpField {
|
|
142
|
+
id: string;
|
|
143
|
+
label: string;
|
|
144
|
+
value?: string;
|
|
145
|
+
type: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface OpItemDetail {
|
|
149
|
+
id: string;
|
|
150
|
+
title: string;
|
|
151
|
+
fields?: OpField[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Input helpers ────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
157
|
+
const stdin = process.stdin as any;
|
|
158
|
+
|
|
159
|
+
/** Read a plain echoed line using Node's TTY directly — no subprocess. */
|
|
160
|
+
function readLine(question: string): Promise<string> {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
process.stdout.write(question);
|
|
163
|
+
let buf = "";
|
|
164
|
+
stdin.setEncoding("utf8");
|
|
165
|
+
stdin.resume();
|
|
166
|
+
const onData = (chunk: string) => {
|
|
167
|
+
buf += chunk;
|
|
168
|
+
const nl = buf.indexOf("\n");
|
|
169
|
+
if (nl !== -1) {
|
|
170
|
+
stdin.pause();
|
|
171
|
+
stdin.removeListener("data", onData);
|
|
172
|
+
resolve(buf.slice(0, nl).replace(/\r$/, "").trim());
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
stdin.on("data", onData);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Read a secret without echoing characters.
|
|
181
|
+
* Uses Node TTY raw mode — no subprocess, no stty, works through make/npx/tsx.
|
|
182
|
+
* CTRL-C is caught explicitly in raw mode and exits cleanly.
|
|
183
|
+
*/
|
|
184
|
+
function readSecret(promptText: string): Promise<string> {
|
|
185
|
+
return new Promise((resolve) => {
|
|
186
|
+
process.stdout.write(promptText);
|
|
187
|
+
|
|
188
|
+
// Non-TTY fallback (piped input): read normally
|
|
189
|
+
if (!stdin.isTTY) {
|
|
190
|
+
let buf = "";
|
|
191
|
+
stdin.setEncoding("utf8");
|
|
192
|
+
stdin.resume();
|
|
193
|
+
const onData = (chunk: string) => {
|
|
194
|
+
buf += chunk;
|
|
195
|
+
const nl = buf.indexOf("\n");
|
|
196
|
+
if (nl !== -1) {
|
|
197
|
+
stdin.pause();
|
|
198
|
+
stdin.removeListener("data", onData);
|
|
199
|
+
process.stdout.write("\n");
|
|
200
|
+
resolve(buf.slice(0, nl).replace(/\r$/, ""));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
stdin.on("data", onData);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
stdin.setRawMode(true);
|
|
208
|
+
stdin.setEncoding("utf8");
|
|
209
|
+
stdin.resume();
|
|
210
|
+
let secret = "";
|
|
211
|
+
|
|
212
|
+
const onData = (char: string) => {
|
|
213
|
+
switch (char) {
|
|
214
|
+
case "\r":
|
|
215
|
+
case "\n":
|
|
216
|
+
stdin.setRawMode(false);
|
|
217
|
+
stdin.pause();
|
|
218
|
+
stdin.removeListener("data", onData);
|
|
219
|
+
process.stdout.write("\n");
|
|
220
|
+
resolve(secret);
|
|
221
|
+
break;
|
|
222
|
+
case "\u0003": // CTRL-C
|
|
223
|
+
stdin.setRawMode(false);
|
|
224
|
+
stdin.pause();
|
|
225
|
+
process.stdout.write("\n");
|
|
226
|
+
process.exit(130);
|
|
227
|
+
break;
|
|
228
|
+
case "\u007f": // backspace
|
|
229
|
+
case "\b":
|
|
230
|
+
secret = secret.slice(0, -1);
|
|
231
|
+
break;
|
|
232
|
+
default:
|
|
233
|
+
if (char >= " ") secret += char;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
stdin.on("data", onData);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── 1Password helpers ────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
async function ensureOpInstalled(): Promise<void> {
|
|
245
|
+
try {
|
|
246
|
+
await execFileAsync("op", ["--version"]);
|
|
247
|
+
} catch {
|
|
248
|
+
console.error("❌ 1Password CLI (op) not found.");
|
|
249
|
+
console.error(" Install it: brew install 1password-cli");
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function ensureSignedIn(): Promise<boolean> {
|
|
255
|
+
try {
|
|
256
|
+
await execFileAsync("op", ["whoami"]);
|
|
257
|
+
return true;
|
|
258
|
+
} catch {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function signIn(): Promise<void> {
|
|
264
|
+
console.log("🔐 Not signed in to 1Password — signing in now...");
|
|
265
|
+
const result = spawnSync("op", ["signin"], { stdio: "inherit" });
|
|
266
|
+
if (result.status !== 0) {
|
|
267
|
+
console.error("❌ 1Password sign-in failed.");
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
if (!(await ensureSignedIn())) {
|
|
271
|
+
console.error("❌ Still not signed in after op signin.");
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function getVault(name: string): Promise<OpVault | null> {
|
|
277
|
+
try {
|
|
278
|
+
return await opJson<OpVault>(["vault", "get", name]);
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function getItem(vaultName: string): Promise<OpItemDetail | null> {
|
|
285
|
+
try {
|
|
286
|
+
return await opJson<OpItemDetail>([
|
|
287
|
+
"item",
|
|
288
|
+
"get",
|
|
289
|
+
ITEM_NAME,
|
|
290
|
+
"--vault",
|
|
291
|
+
vaultName,
|
|
292
|
+
]);
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function fieldValue(item: OpItemDetail, fieldName: string): string {
|
|
299
|
+
return (
|
|
300
|
+
item.fields?.find(
|
|
301
|
+
(f) => f.label === fieldName || f.id === fieldName.toLowerCase(),
|
|
302
|
+
)?.value ?? ""
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Check mode ───────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Non-interactive check used by make web / make den-up.
|
|
310
|
+
*
|
|
311
|
+
* Signs in automatically if the CLI session has expired — that is NOT a reason
|
|
312
|
+
* to run full setup. Only exits 1 (triggering setup) when the vault, item, or
|
|
313
|
+
* required fields are genuinely missing.
|
|
314
|
+
*/
|
|
315
|
+
async function checkMode(): Promise<void> {
|
|
316
|
+
try {
|
|
317
|
+
await execFileAsync("op", ["--version"]);
|
|
318
|
+
} catch {
|
|
319
|
+
console.error("1password-cli not installed");
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Session expired → sign in interactively, then proceed with the check.
|
|
324
|
+
// This is NOT a setup failure — the vault may already be fully configured.
|
|
325
|
+
if (!(await ensureSignedIn())) {
|
|
326
|
+
console.log("🔐 1Password session expired — signing in...");
|
|
327
|
+
const result = spawnSync("op", ["signin"], { stdio: "inherit" });
|
|
328
|
+
if (result.status !== 0 || !(await ensureSignedIn())) {
|
|
329
|
+
console.error("1Password sign-in failed");
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!(await getVault(VAULT_NAME))) {
|
|
335
|
+
console.error(
|
|
336
|
+
`vault "${VAULT_NAME}" not found — run: make ${product().cli}-setup-1password`,
|
|
337
|
+
);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const item = await getItem(VAULT_NAME);
|
|
342
|
+
if (!item) {
|
|
343
|
+
console.error(
|
|
344
|
+
`item "${ITEM_NAME}" not found — run: make ${product().cli}-setup-1password`,
|
|
345
|
+
);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const missing = SECRETS.filter(
|
|
350
|
+
(s) => s.required && !fieldValue(item, s.field),
|
|
351
|
+
);
|
|
352
|
+
if (missing.length > 0) {
|
|
353
|
+
console.error(
|
|
354
|
+
`required fields not set (${missing.map((s) => s.field).join(", ")}) — run: make ${product().cli}-setup-1password`,
|
|
355
|
+
);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ─── Setup mode ───────────────────────────────────────────────────────────────
|
|
363
|
+
|
|
364
|
+
async function setupMode(): Promise<void> {
|
|
365
|
+
console.log();
|
|
366
|
+
console.log("🔑 1Password Developer API Keys Setup");
|
|
367
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
368
|
+
console.log(
|
|
369
|
+
`Creates item "${ITEM_NAME}" in vault "${VAULT_NAME}" with your API keys.`,
|
|
370
|
+
);
|
|
371
|
+
console.log(
|
|
372
|
+
"These are resolved at `make up` time via `op run` — keys never touch disk.",
|
|
373
|
+
);
|
|
374
|
+
console.log();
|
|
375
|
+
|
|
376
|
+
await ensureOpInstalled();
|
|
377
|
+
|
|
378
|
+
if (!(await ensureSignedIn())) {
|
|
379
|
+
await signIn();
|
|
380
|
+
} else {
|
|
381
|
+
const { stdout } = await execFileAsync("op", ["whoami"]);
|
|
382
|
+
console.log(`✓ Signed in as ${stdout.trim().split("\n")[0]}`);
|
|
383
|
+
}
|
|
384
|
+
console.log();
|
|
385
|
+
|
|
386
|
+
// ── Vault ───────────────────────────────────────────────────────────────
|
|
387
|
+
const vault = await getVault(VAULT_NAME);
|
|
388
|
+
if (!vault) {
|
|
389
|
+
console.error(`❌ Vault "${VAULT_NAME}" not found in 1Password.`);
|
|
390
|
+
console.error(
|
|
391
|
+
" Ask your team admin to ensure the vault exists and you have access.",
|
|
392
|
+
);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
console.log(`✓ Vault "${VAULT_NAME}" found`);
|
|
396
|
+
console.log();
|
|
397
|
+
|
|
398
|
+
// ── Item ─────────────────────────────────────────────────────────────────
|
|
399
|
+
let item = await getItem(VAULT_NAME);
|
|
400
|
+
if (item) {
|
|
401
|
+
console.log(`✓ Item "${ITEM_NAME}" already exists`);
|
|
402
|
+
} else {
|
|
403
|
+
console.log(` Creating item "${ITEM_NAME}"...`);
|
|
404
|
+
const fieldArgs = SECRETS.map((s) => `${s.field}[password]=`);
|
|
405
|
+
item = await opJson<OpItemDetail>([
|
|
406
|
+
"item",
|
|
407
|
+
"create",
|
|
408
|
+
"--category=login",
|
|
409
|
+
`--title=${ITEM_NAME}`,
|
|
410
|
+
`--vault=${VAULT_NAME}`,
|
|
411
|
+
...fieldArgs,
|
|
412
|
+
]);
|
|
413
|
+
console.log(`✓ Item "${ITEM_NAME}" created`);
|
|
414
|
+
}
|
|
415
|
+
console.log();
|
|
416
|
+
|
|
417
|
+
// ── Fill fields ──────────────────────────────────────────────────────────
|
|
418
|
+
const sourceEnv = loadSourceDotEnv();
|
|
419
|
+
const sourceEnvLabel = DOTENV_PATH
|
|
420
|
+
? path.relative(DEN_ROOT, DOTENV_PATH)
|
|
421
|
+
: "";
|
|
422
|
+
const sourceEnvExists = DOTENV_PATH ? fs.existsSync(DOTENV_PATH) : false;
|
|
423
|
+
|
|
424
|
+
console.log("Fill in your API keys. Press Enter to skip a field.");
|
|
425
|
+
console.log();
|
|
426
|
+
|
|
427
|
+
// Show .env source check once up front
|
|
428
|
+
if (sourceEnvExists) {
|
|
429
|
+
console.log(
|
|
430
|
+
` ${chalk.green("✓")} ${sourceEnvLabel} found — will offer matching values`,
|
|
431
|
+
);
|
|
432
|
+
} else if (DOTENV_PATH) {
|
|
433
|
+
console.log(
|
|
434
|
+
` ${chalk.red("✗")} ${sourceEnvLabel} not found — enter keys manually`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
console.log();
|
|
438
|
+
|
|
439
|
+
let anyUpdated = false;
|
|
440
|
+
for (const secret of SECRETS) {
|
|
441
|
+
const current = fieldValue(item, secret.field);
|
|
442
|
+
const sourceVal = sourceEnv[secret.field];
|
|
443
|
+
const envVal = process.env[secret.field];
|
|
444
|
+
const tag = secret.required ? " (required)" : " (optional)";
|
|
445
|
+
const status = current ? " " + chalk.green("✓") + " set" : "";
|
|
446
|
+
|
|
447
|
+
console.log(` ${chalk.bold(secret.field)}${chalk.dim(tag)}${status}`);
|
|
448
|
+
console.log(` ${chalk.dim(secret.description)}`);
|
|
449
|
+
if (secret.url) console.log(` ${chalk.dim(secret.url)}`);
|
|
450
|
+
|
|
451
|
+
// Per-field source checks
|
|
452
|
+
console.log(
|
|
453
|
+
(DOTENV_PATH
|
|
454
|
+
? ` ${sourceEnvLabel}: ${sourceVal ? chalk.green("✓ found") : chalk.dim("✗ not set")} `
|
|
455
|
+
: " ") +
|
|
456
|
+
`shell env: ${envVal ? chalk.green("✓ found") : chalk.dim("✗ not set")}`,
|
|
457
|
+
);
|
|
458
|
+
|
|
459
|
+
// Track the value we will save (null = nothing to save, keep current).
|
|
460
|
+
let valueToSave: string | null = null;
|
|
461
|
+
|
|
462
|
+
// Priority 1: the configured dotenv file
|
|
463
|
+
if (sourceVal) {
|
|
464
|
+
const answer = (
|
|
465
|
+
await readLine(` Use value from ${sourceEnvLabel}? (Y/n): `)
|
|
466
|
+
)
|
|
467
|
+
.trim()
|
|
468
|
+
.toLowerCase();
|
|
469
|
+
if (!answer || answer === "y") {
|
|
470
|
+
valueToSave = sourceVal;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Priority 2: host environment variable
|
|
475
|
+
if (valueToSave === null && envVal) {
|
|
476
|
+
const answer = (
|
|
477
|
+
await readLine(" Use value from shell environment? (Y/n): ")
|
|
478
|
+
)
|
|
479
|
+
.trim()
|
|
480
|
+
.toLowerCase();
|
|
481
|
+
if (!answer || answer === "y") {
|
|
482
|
+
valueToSave = envVal;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Priority 3: manual entry
|
|
487
|
+
if (valueToSave === null) {
|
|
488
|
+
const raw = (await readSecret(" Value (Enter to skip): ")).trim();
|
|
489
|
+
if (raw) valueToSave = raw;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (valueToSave === null) {
|
|
493
|
+
if (!current && secret.required) {
|
|
494
|
+
console.log(
|
|
495
|
+
" ⚠️ Skipped — fill this in before `make up` will work.\n",
|
|
496
|
+
);
|
|
497
|
+
} else {
|
|
498
|
+
console.log(" → keeping existing value\n");
|
|
499
|
+
}
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
await execFileAsync("op", [
|
|
504
|
+
"item",
|
|
505
|
+
"edit",
|
|
506
|
+
ITEM_NAME,
|
|
507
|
+
"--vault",
|
|
508
|
+
VAULT_NAME,
|
|
509
|
+
`${secret.field}[password]=${valueToSave}`,
|
|
510
|
+
]);
|
|
511
|
+
console.log(" ✓ saved\n");
|
|
512
|
+
anyUpdated = true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── Verify ───────────────────────────────────────────────────────────────
|
|
516
|
+
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
517
|
+
console.log("Verifying...");
|
|
518
|
+
console.log();
|
|
519
|
+
|
|
520
|
+
const updatedItem = (await getItem(VAULT_NAME))!;
|
|
521
|
+
let allRequired = true;
|
|
522
|
+
|
|
523
|
+
for (const secret of SECRETS) {
|
|
524
|
+
const val = fieldValue(updatedItem, secret.field);
|
|
525
|
+
if (val) {
|
|
526
|
+
console.log(` ✓ ${secret.field}`);
|
|
527
|
+
} else if (secret.required) {
|
|
528
|
+
console.log(` ✗ ${secret.field} ← required, not set`);
|
|
529
|
+
allRequired = false;
|
|
530
|
+
} else {
|
|
531
|
+
console.log(` – ${secret.field} (optional, not set)`);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
console.log();
|
|
536
|
+
if (allRequired) {
|
|
537
|
+
console.log(
|
|
538
|
+
`✅ Setup complete. \`make ${product().cli}-up\` will inject secrets automatically.`,
|
|
539
|
+
);
|
|
540
|
+
} else {
|
|
541
|
+
console.log("⚠️ Setup incomplete — required fields are missing.");
|
|
542
|
+
console.log(
|
|
543
|
+
` Run \`make ${product().cli}-setup-1password\` again to fill them in.`,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (anyUpdated) {
|
|
548
|
+
console.log();
|
|
549
|
+
console.log(
|
|
550
|
+
"💡 Reminder: ensure 1Password CLI integration is enabled in the app:",
|
|
551
|
+
);
|
|
552
|
+
console.log(
|
|
553
|
+
" 1Password.app → Settings → Developer → Integrate with 1Password CLI",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
console.log();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
560
|
+
|
|
561
|
+
const isCheck = process.argv.includes("--check");
|
|
562
|
+
|
|
563
|
+
if (isCheck) {
|
|
564
|
+
checkMode().catch(() => process.exit(1));
|
|
565
|
+
} else {
|
|
566
|
+
setupMode().catch((err: Error) => {
|
|
567
|
+
console.error("❌ ", err.message);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
});
|
|
570
|
+
}
|