@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,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed marker the web server adds to every HTML page it serves, so the CLI
|
|
3
|
+
* can tell its own UI is up without matching any product or brand text
|
|
4
|
+
* (a brand may differ from the product title, even in case).
|
|
5
|
+
*/
|
|
6
|
+
export const DEN_UI_MARKER = '<meta name="den-ui" content="1">';
|
|
7
|
+
|
|
8
|
+
/** Insert the marker right after <head> (or at the start when there is none). */
|
|
9
|
+
export function addDenUiMarker(html: string): string {
|
|
10
|
+
if (html.includes(DEN_UI_MARKER)) return html;
|
|
11
|
+
const head = /<head(\s[^>]*)?>/i.exec(html);
|
|
12
|
+
if (!head) return DEN_UI_MARKER + html;
|
|
13
|
+
const at = head.index + head[0].length;
|
|
14
|
+
return html.slice(0, at) + DEN_UI_MARKER + html.slice(at);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasDenUiMarker(html: string): boolean {
|
|
18
|
+
return html.includes(DEN_UI_MARKER);
|
|
19
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `den apps` (alias `applications`): Org > Application > Repo > Services
|
|
3
|
+
* for the focused workspace, and the `-A/--app <name>` flag for up/down/
|
|
4
|
+
* restart/status.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import {
|
|
8
|
+
loadApplications,
|
|
9
|
+
loadApplicationTree,
|
|
10
|
+
} from "../../shared/environment-manager/load-applications.js";
|
|
11
|
+
import type {
|
|
12
|
+
Application,
|
|
13
|
+
ApplicationContributor,
|
|
14
|
+
} from "../../shared/applications.js";
|
|
15
|
+
|
|
16
|
+
import { product } from "../../shared/product.js";
|
|
17
|
+
import type {
|
|
18
|
+
Placement,
|
|
19
|
+
RollupPlacement,
|
|
20
|
+
} from "../../shared/manifest/placement.js";
|
|
21
|
+
|
|
22
|
+
/** An application's placement line: "⇄ hybrid: <summary>", "☁ remote", or "". */
|
|
23
|
+
export function describeAppPlacement(a: {
|
|
24
|
+
placement: RollupPlacement;
|
|
25
|
+
summary: string;
|
|
26
|
+
}): string {
|
|
27
|
+
if (a.placement === "hybrid") return `⇄ hybrid: ${a.summary}`;
|
|
28
|
+
if (a.placement === "remote") return "☁ remote";
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A repo's suffix over its services: "☁ remote (dev)", "⇄ hybrid", or "". */
|
|
33
|
+
export function describeRepoPlacement(services: Placement[]): string {
|
|
34
|
+
const runs = new Set(services.map((s) => s.runs).filter((r) => r !== "none"));
|
|
35
|
+
if (runs.size > 1) return "⇄ hybrid";
|
|
36
|
+
if (!runs.has("remote")) return "";
|
|
37
|
+
const envs = [
|
|
38
|
+
...new Set(services.flatMap((s) => (s.target ? [s.target.env] : []))),
|
|
39
|
+
].sort();
|
|
40
|
+
return `☁ remote (${envs.join(", ")})`;
|
|
41
|
+
}
|
|
42
|
+
/** "feature-items (module) → ships in core-api" */
|
|
43
|
+
export function describeContributor(c: ApplicationContributor): string {
|
|
44
|
+
return `${c.repo} (${c.kind}) → ${c.contributesTo.length ? `ships in ${c.contributesTo.join(", ")}` : "no consumer known"}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Pull `-A <name>` / `--app <name>` / `--app=<name>` out of args. */
|
|
48
|
+
export function takeAppFlag(args: string[]): {
|
|
49
|
+
app?: string;
|
|
50
|
+
rest: string[];
|
|
51
|
+
} {
|
|
52
|
+
const rest: string[] = [];
|
|
53
|
+
let app: string | undefined;
|
|
54
|
+
for (let i = 0; i < args.length; i++) {
|
|
55
|
+
const a = args[i];
|
|
56
|
+
if (a === "-A" || a === "--app") {
|
|
57
|
+
app = args[++i];
|
|
58
|
+
if (!app || app.startsWith("-"))
|
|
59
|
+
throw new Error(
|
|
60
|
+
`${a} needs an application name (see \`${product().cli} apps\`)`,
|
|
61
|
+
);
|
|
62
|
+
} else if (a.startsWith("--app=")) app = a.slice("--app=".length);
|
|
63
|
+
else rest.push(a);
|
|
64
|
+
}
|
|
65
|
+
return { app, rest };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The runtime repos of application `name`; throws if unknown or empty. */
|
|
69
|
+
export function appRepos(apps: Application[], name: string): Set<string> {
|
|
70
|
+
const a = apps.find((x) => x.name === name);
|
|
71
|
+
if (!a)
|
|
72
|
+
throw new Error(
|
|
73
|
+
`No application "${name}". Applications here: ${apps.map((x) => x.name).join(", ") || "none"}`,
|
|
74
|
+
);
|
|
75
|
+
if (a.repos.length === 0)
|
|
76
|
+
throw new Error(
|
|
77
|
+
a.contributors.length
|
|
78
|
+
? `Application "${name}" has no runtime here, only modules/libraries: ${a.contributors.map((c) => c.repo).join(", ")}`
|
|
79
|
+
: `Application "${name}" has no repos in this workspace`,
|
|
80
|
+
);
|
|
81
|
+
return new Set(a.repos);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** appRepos over the focused workspace. */
|
|
85
|
+
export function focusedAppRepos(name: string): Set<string> {
|
|
86
|
+
return appRepos(loadApplications(), name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function cmdApps(args: string[]): void {
|
|
90
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
91
|
+
console.log(
|
|
92
|
+
`\n ${product().cli} apps [--json] (alias: applications)\n\n` +
|
|
93
|
+
" Org > Application > Repo > Services for the focused workspace.\n" +
|
|
94
|
+
` Applications come from \`applications:\` in ${product().configFile} and\n` +
|
|
95
|
+
` \`application:\` in a repo's ${product().manifestYml} (the repo wins). Any other\n` +
|
|
96
|
+
" repo is its own application, named after the repo.\n" +
|
|
97
|
+
` Act on one with -A: ${product().cli} up -A <app>.\n`,
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const tree = loadApplicationTree();
|
|
102
|
+
if (args.includes("--json")) {
|
|
103
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log();
|
|
107
|
+
console.log(
|
|
108
|
+
` ${chalk.bold(tree.org?.title ?? "Org")}${tree.org && tree.org.id !== tree.org.title ? chalk.gray(` (${tree.org.id})`) : ""}`,
|
|
109
|
+
);
|
|
110
|
+
if (tree.applications.length === 0) {
|
|
111
|
+
console.log(chalk.yellow(" No repos in this workspace.\n"));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const a of tree.applications) {
|
|
115
|
+
console.log();
|
|
116
|
+
const title = a.title !== a.name ? ` ${a.title}` : "";
|
|
117
|
+
const owner = a.owner ? chalk.gray(` · ${a.owner}`) : "";
|
|
118
|
+
const kind =
|
|
119
|
+
a.source === "repo" ? chalk.gray(" (repo is the application)") : "";
|
|
120
|
+
const placed = describeAppPlacement(a);
|
|
121
|
+
console.log(
|
|
122
|
+
` ${chalk.bold.cyan(a.name)}${title}${owner}${kind}${placed ? chalk.cyan(` ${placed}`) : ""}`,
|
|
123
|
+
);
|
|
124
|
+
if (a.description) console.log(chalk.gray(` ${a.description}`));
|
|
125
|
+
const width = Math.max(0, ...a.repos.map((r) => r.length));
|
|
126
|
+
for (const repo of a.repos) {
|
|
127
|
+
const subs = a.services[repo]
|
|
128
|
+
.map((addr) => (addr.includes("/") ? addr.split("/")[1] : addr))
|
|
129
|
+
.filter((s) => s !== repo);
|
|
130
|
+
const where = describeRepoPlacement(
|
|
131
|
+
a.services[repo].flatMap((addr) => a.placements[addr] ?? []),
|
|
132
|
+
);
|
|
133
|
+
const suffix = where ? chalk.cyan(` ${where}`) : "";
|
|
134
|
+
console.log(
|
|
135
|
+
` ${subs.length ? `${repo.padEnd(width)}${chalk.gray(` ${subs.join(", ")}`)}` : repo}${suffix}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
for (const c of a.contributors)
|
|
139
|
+
console.log(chalk.gray(` ${describeContributor(c)}`));
|
|
140
|
+
if (a.absent.length)
|
|
141
|
+
console.log(
|
|
142
|
+
chalk.gray(` not in this workspace: ${a.absent.join(", ")}`),
|
|
143
|
+
);
|
|
144
|
+
if (!a.repos.length && !a.contributors.length && !a.absent.length)
|
|
145
|
+
console.log(chalk.gray(" (no repos in this workspace)"));
|
|
146
|
+
}
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as yaml from "yaml";
|
|
3
|
+
import {
|
|
4
|
+
mergeUserConfig,
|
|
5
|
+
readUserConfig,
|
|
6
|
+
type UserConfig,
|
|
7
|
+
} from "../../shared/den-config.js";
|
|
8
|
+
|
|
9
|
+
import { product } from "../../shared/product.js";
|
|
10
|
+
type ConfigKey = keyof UserConfig;
|
|
11
|
+
|
|
12
|
+
const KEY_ALIASES: Record<string, ConfigKey> = {
|
|
13
|
+
tabs: "cli_tabs",
|
|
14
|
+
cli_tabs: "cli_tabs",
|
|
15
|
+
"local-dev": "local_dev",
|
|
16
|
+
local_dev: "local_dev",
|
|
17
|
+
editor: "editor",
|
|
18
|
+
terminal: "terminal",
|
|
19
|
+
editor_overrides: "editor_overrides",
|
|
20
|
+
log_window_lines: "log_window_lines",
|
|
21
|
+
log_window: "log_window_lines",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function canonicalKey(input: string): ConfigKey {
|
|
25
|
+
const key = KEY_ALIASES[input];
|
|
26
|
+
if (!key) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Unknown config key "${input}". Known keys: ${Object.keys(KEY_ALIASES).join(", ")}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return key;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseValue(raw: string): unknown {
|
|
35
|
+
const parsed = yaml.parse(raw);
|
|
36
|
+
return parsed === null ? "" : parsed;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function cmdConfig(args: string[]): Promise<void> {
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
const current = readUserConfig();
|
|
42
|
+
const output = yaml.stringify(current).trim();
|
|
43
|
+
console.log(output || "{}");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (args[0] === "rm") {
|
|
48
|
+
const keyArg = args[1];
|
|
49
|
+
if (!keyArg) throw new Error(`Usage: ${product().cli} config rm <key>`);
|
|
50
|
+
const key = canonicalKey(keyArg);
|
|
51
|
+
mergeUserConfig({ [key]: undefined });
|
|
52
|
+
console.log(chalk.green(` ✓ Removed ${key}`));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const assignment = args[0];
|
|
57
|
+
const eq = assignment.indexOf("=");
|
|
58
|
+
if (eq <= 0) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Usage: ${product().cli} config <key>=<value> or ${product().cli} config rm <key>`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const key = canonicalKey(assignment.slice(0, eq));
|
|
65
|
+
const value = parseValue(assignment.slice(eq + 1));
|
|
66
|
+
mergeUserConfig({ [key]: value } as Partial<UserConfig>);
|
|
67
|
+
console.log(chalk.green(` ✓ Set ${key}`));
|
|
68
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Entry point for `den create` (bin/den routes `create` here). Kept apart
|
|
3
|
+
* from the main CLI, which needs a distribution root at import time: create
|
|
4
|
+
* runs before one exists, e.g. `npx @monster-orange/den create acme`.
|
|
5
|
+
*/
|
|
6
|
+
import { runCreate } from "./create.js";
|
|
7
|
+
|
|
8
|
+
const cwd = process.env.DEN_INVOKE_CWD?.trim() || process.cwd();
|
|
9
|
+
process.exit(runCreate(process.argv.slice(2), cwd));
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `den create <name> [dir]`: scaffold a new distribution.
|
|
3
|
+
*
|
|
4
|
+
* A distribution is a small repo that installs Den as a package and holds
|
|
5
|
+
* everything that makes it yours: den.yml (the product profile), config/
|
|
6
|
+
* (brand, secrets, edge), manifests/, tests/pillars/, a Makefile that
|
|
7
|
+
* includes Den's make fragment, and a bin/<cli> shim. See
|
|
8
|
+
* docs/distributions.md.
|
|
9
|
+
*
|
|
10
|
+
* This module imports nothing that needs a distribution root: it runs before
|
|
11
|
+
* one exists. examples/distribution/ is exactly `den create acme`; a unit
|
|
12
|
+
* test regenerates it, so the two cannot drift.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as path from "node:path";
|
|
16
|
+
import { ENGINE_PACKAGE, getEngineRoot } from "../../shared/paths.js";
|
|
17
|
+
import { profileFrom } from "../../shared/profile.js";
|
|
18
|
+
|
|
19
|
+
export interface CreateOptions {
|
|
20
|
+
/** Product name: lower-case `[a-z][a-z0-9-]*`. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Display name for the brand; defaults to the name in Title Case. */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** The command people type; defaults to the name. */
|
|
25
|
+
cli?: string;
|
|
26
|
+
/** Upper-case env-var prefix; defaults to the name upper-cased. */
|
|
27
|
+
envPrefix?: string;
|
|
28
|
+
/** The Den version the distribution pins (exact). */
|
|
29
|
+
denVersion: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NAME = /^[a-z][a-z0-9-]*$/;
|
|
33
|
+
|
|
34
|
+
/** "acme-dev" → "Acme Dev". */
|
|
35
|
+
export function titleFromName(name: string): string {
|
|
36
|
+
return name
|
|
37
|
+
.split("-")
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
40
|
+
.join(" ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Validate options; throws with a message a person can act on. */
|
|
44
|
+
export function validateCreateOptions(opts: CreateOptions): void {
|
|
45
|
+
if (!NAME.test(opts.name))
|
|
46
|
+
throw new Error(
|
|
47
|
+
`name "${opts.name}" must start with a lower-case letter and use only a-z, 0-9 and "-"`,
|
|
48
|
+
);
|
|
49
|
+
if (opts.cli !== undefined && !NAME.test(opts.cli))
|
|
50
|
+
throw new Error(
|
|
51
|
+
`--cli "${opts.cli}" must start with a lower-case letter and use only a-z, 0-9 and "-"`,
|
|
52
|
+
);
|
|
53
|
+
if (opts.title !== undefined && !opts.title.trim())
|
|
54
|
+
throw new Error("--title must not be empty");
|
|
55
|
+
if (opts.title !== undefined && /[\n"\\]/.test(opts.title))
|
|
56
|
+
throw new Error("--title must be one line without quotes or backslashes");
|
|
57
|
+
// The profile validates the env prefix the same way den.yml would.
|
|
58
|
+
profileFrom({
|
|
59
|
+
name: opts.name,
|
|
60
|
+
cli: opts.cli,
|
|
61
|
+
env_prefix: opts.envPrefix,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function bannerLines(title: string): string[] {
|
|
66
|
+
const spelled = title.toUpperCase().split("").join(" ");
|
|
67
|
+
const bar = "─".repeat(spelled.length + 4);
|
|
68
|
+
return [` ┌${bar}┐`, ` │ ${spelled} │`, ` └${bar}┘`];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every file `den create` writes, keyed by path relative to the new root. */
|
|
72
|
+
export function renderDistribution(
|
|
73
|
+
opts: CreateOptions,
|
|
74
|
+
): Record<string, string> {
|
|
75
|
+
validateCreateOptions(opts);
|
|
76
|
+
const { name, denVersion } = opts;
|
|
77
|
+
const title = opts.title?.trim() || titleFromName(name);
|
|
78
|
+
const profile = profileFrom({
|
|
79
|
+
name,
|
|
80
|
+
cli: opts.cli,
|
|
81
|
+
env_prefix: opts.envPrefix,
|
|
82
|
+
});
|
|
83
|
+
const cli = profile.cli;
|
|
84
|
+
const prefix = profile.envPrefix;
|
|
85
|
+
const overrides: string[] = [];
|
|
86
|
+
if (opts.cli !== undefined) overrides.push(` cli: ${cli}`);
|
|
87
|
+
if (opts.envPrefix !== undefined) overrides.push(` env_prefix: ${prefix}`);
|
|
88
|
+
|
|
89
|
+
const denYml = [
|
|
90
|
+
`# ${title}: a Den distribution. Every name below derives from product.name;`,
|
|
91
|
+
"# uncomment a line to override it. See docs/distributions.md in Den.",
|
|
92
|
+
"product:",
|
|
93
|
+
` name: ${name}`,
|
|
94
|
+
...overrides,
|
|
95
|
+
...(opts.cli === undefined ? [` # cli: ${cli}`] : []),
|
|
96
|
+
...(opts.envPrefix === undefined ? [` # env_prefix: ${prefix}`] : []),
|
|
97
|
+
` # config: config/${name}.yml`,
|
|
98
|
+
` # manifest: .${name}.yml`,
|
|
99
|
+
` # state_dir: ~/.${name}`,
|
|
100
|
+
` # services_dir: .${name}-services`,
|
|
101
|
+
` # workspaces_dir: ${name}-workspaces`,
|
|
102
|
+
"",
|
|
103
|
+
].join("\n");
|
|
104
|
+
|
|
105
|
+
const packageJson =
|
|
106
|
+
JSON.stringify(
|
|
107
|
+
{
|
|
108
|
+
name,
|
|
109
|
+
version: "0.0.0",
|
|
110
|
+
private: true,
|
|
111
|
+
description: `${title}: local dev and E2E harness, built on Den`,
|
|
112
|
+
type: "module",
|
|
113
|
+
bin: { [cli]: `./bin/${cli}` },
|
|
114
|
+
scripts: { [cli]: `./bin/${cli}` },
|
|
115
|
+
dependencies: { [ENGINE_PACKAGE]: denVersion },
|
|
116
|
+
},
|
|
117
|
+
null,
|
|
118
|
+
2,
|
|
119
|
+
) + "\n";
|
|
120
|
+
|
|
121
|
+
const shim = [
|
|
122
|
+
"#!/bin/sh",
|
|
123
|
+
`# ${cli}: runs Den from node_modules as this distribution.`,
|
|
124
|
+
'DEN_ROOT="$(cd "$(dirname "$0")/.." && pwd)"',
|
|
125
|
+
`export DEN_ROOT; exec "$DEN_ROOT/node_modules/${ENGINE_PACKAGE}/bin/den" "$@"`,
|
|
126
|
+
"",
|
|
127
|
+
].join("\n");
|
|
128
|
+
|
|
129
|
+
const makefile = [
|
|
130
|
+
`# ${title}: Den's make targets, run from this distribution's root.`,
|
|
131
|
+
"# `make help` lists them. Add your own targets below the include.",
|
|
132
|
+
`DEN_TARGET_PREFIX := ${cli}`,
|
|
133
|
+
`include node_modules/${ENGINE_PACKAGE}/make/den.mk`,
|
|
134
|
+
"",
|
|
135
|
+
].join("\n");
|
|
136
|
+
|
|
137
|
+
const config = [
|
|
138
|
+
`# ${title}: org config. Den reads it from config/${name}.yml (den.yml product.config).`,
|
|
139
|
+
"",
|
|
140
|
+
"# Shown to people: CLI banner, help header and web page titles.",
|
|
141
|
+
"brand:",
|
|
142
|
+
` name: ${title}`,
|
|
143
|
+
` tagline: ${title} local dev and E2E harness`,
|
|
144
|
+
" banner:",
|
|
145
|
+
...bannerLines(title).map((l) => ` - "${l}"`),
|
|
146
|
+
"",
|
|
147
|
+
"# The GitHub org (or user) that owns the repos Den clones and lists.",
|
|
148
|
+
"github_org: your-github-org",
|
|
149
|
+
"",
|
|
150
|
+
"# Secrets backend: env reads plain environment variables; doppler and",
|
|
151
|
+
"# 1password are the others.",
|
|
152
|
+
"secrets:",
|
|
153
|
+
" provider: env",
|
|
154
|
+
"",
|
|
155
|
+
"# This instance's HTTPS edge: https://<service>.<domain>.",
|
|
156
|
+
"edge:",
|
|
157
|
+
` org: ${name}`,
|
|
158
|
+
` domain: ${name}.localhost`,
|
|
159
|
+
"",
|
|
160
|
+
"environments: [local]",
|
|
161
|
+
"",
|
|
162
|
+
].join("\n");
|
|
163
|
+
|
|
164
|
+
const localEnv =
|
|
165
|
+
JSON.stringify(
|
|
166
|
+
{
|
|
167
|
+
name: "local",
|
|
168
|
+
baseUrls: {
|
|
169
|
+
web: "http://localhost:3000",
|
|
170
|
+
api: "http://localhost:3001",
|
|
171
|
+
services: {},
|
|
172
|
+
},
|
|
173
|
+
timeouts: { api: 30000, pageLoad: 60000 },
|
|
174
|
+
},
|
|
175
|
+
null,
|
|
176
|
+
2,
|
|
177
|
+
) + "\n";
|
|
178
|
+
|
|
179
|
+
const gitignore = [
|
|
180
|
+
"node_modules/",
|
|
181
|
+
"config/local/",
|
|
182
|
+
`.${name}-services/`,
|
|
183
|
+
`${name}-workspaces/`,
|
|
184
|
+
".smoke-run/",
|
|
185
|
+
".smoke-clones/",
|
|
186
|
+
".synth-run/",
|
|
187
|
+
".journeys-run/",
|
|
188
|
+
"reports/",
|
|
189
|
+
"results/",
|
|
190
|
+
"test-results/",
|
|
191
|
+
"playwright-report/",
|
|
192
|
+
".env",
|
|
193
|
+
"",
|
|
194
|
+
].join("\n");
|
|
195
|
+
|
|
196
|
+
const readme = [
|
|
197
|
+
`# ${title}`,
|
|
198
|
+
"",
|
|
199
|
+
`${title} is a Den distribution: Den (\`${ENGINE_PACKAGE}\`) is the engine,`,
|
|
200
|
+
"this repo holds what makes it yours.",
|
|
201
|
+
"",
|
|
202
|
+
"## Run",
|
|
203
|
+
"",
|
|
204
|
+
"```sh",
|
|
205
|
+
"npm install",
|
|
206
|
+
`./bin/${cli} --help`,
|
|
207
|
+
`./bin/${cli} status`,
|
|
208
|
+
`./bin/${cli} web # the web UI`,
|
|
209
|
+
"make help",
|
|
210
|
+
"```",
|
|
211
|
+
"",
|
|
212
|
+
"## Layout",
|
|
213
|
+
"",
|
|
214
|
+
"| Path | What |",
|
|
215
|
+
"| --- | --- |",
|
|
216
|
+
"| `den.yml` | The product profile: name, CLI, env prefix, file and directory names |",
|
|
217
|
+
`| \`config/${name}.yml\` | Brand, GitHub org, secrets backend, edge |`,
|
|
218
|
+
"| `config/environments/` | One JSON file per environment (base URLs, auth, timeouts) |",
|
|
219
|
+
"| `manifests/` | Manifests for repos that do not carry their own |",
|
|
220
|
+
"| `tests/pillars/` | This distribution's own tests, by pillar |",
|
|
221
|
+
`| \`bin/${cli}\` | Runs Den from \`node_modules\` with this repo as its root |`,
|
|
222
|
+
"| `Makefile` | Includes Den's make targets (`make/den.mk`) |",
|
|
223
|
+
"",
|
|
224
|
+
`Environment variables use the \`${prefix}_\` prefix: for example`,
|
|
225
|
+
`\`${prefix}_HOME\` moves the state directory (default \`~/.${name}\`).`,
|
|
226
|
+
"",
|
|
227
|
+
"## Upgrade Den",
|
|
228
|
+
"",
|
|
229
|
+
`\`package.json\` pins an exact Den version. To upgrade, read the`,
|
|
230
|
+
"Migrations section of Den's CHANGELOG for each version in between, then:",
|
|
231
|
+
"",
|
|
232
|
+
"```sh",
|
|
233
|
+
`npm install ${ENGINE_PACKAGE}@<version> --save-exact`,
|
|
234
|
+
"```",
|
|
235
|
+
"",
|
|
236
|
+
].join("\n");
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"den.yml": denYml,
|
|
240
|
+
"package.json": packageJson,
|
|
241
|
+
[`bin/${cli}`]: shim,
|
|
242
|
+
Makefile: makefile,
|
|
243
|
+
[`config/${name}.yml`]: config,
|
|
244
|
+
"config/environments/local.json": localEnv,
|
|
245
|
+
"manifests/.gitkeep": "",
|
|
246
|
+
"tests/pillars/.gitkeep": "",
|
|
247
|
+
".gitignore": gitignore,
|
|
248
|
+
"README.md": readme,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** The version of the Den that is running (its package.json). */
|
|
253
|
+
export function runningDenVersion(
|
|
254
|
+
engineRoot: string = getEngineRoot(),
|
|
255
|
+
): string {
|
|
256
|
+
const raw = fs.readFileSync(path.join(engineRoot, "package.json"), "utf-8");
|
|
257
|
+
return (JSON.parse(raw) as { version: string }).version;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Write a distribution into `dir`. Refuses a directory that exists and is not
|
|
262
|
+
* empty; never overwrites a file. Returns the files written (relative).
|
|
263
|
+
*/
|
|
264
|
+
export function writeDistribution(dir: string, opts: CreateOptions): string[] {
|
|
265
|
+
const files = renderDistribution(opts);
|
|
266
|
+
const root = path.resolve(dir);
|
|
267
|
+
if (fs.existsSync(root)) {
|
|
268
|
+
if (!fs.statSync(root).isDirectory())
|
|
269
|
+
throw new Error(`${root} exists and is not a directory`);
|
|
270
|
+
if (fs.readdirSync(root).length > 0)
|
|
271
|
+
throw new Error(
|
|
272
|
+
`${root} is not empty: create needs a new or empty directory`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const written: string[] = [];
|
|
276
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
277
|
+
const file = path.join(root, rel);
|
|
278
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
279
|
+
// "wx": fail rather than overwrite, even if something appeared meanwhile.
|
|
280
|
+
fs.writeFileSync(file, content, { encoding: "utf-8", flag: "wx" });
|
|
281
|
+
if (rel.startsWith("bin/")) fs.chmodSync(file, 0o755);
|
|
282
|
+
written.push(rel);
|
|
283
|
+
}
|
|
284
|
+
return written;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export interface ParsedCreateArgs extends Omit<CreateOptions, "denVersion"> {
|
|
288
|
+
dir: string;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const CREATE_USAGE =
|
|
292
|
+
"usage: den create <name> [dir] [--title <title>] [--cli <cli>] [--env-prefix <PREFIX>]";
|
|
293
|
+
|
|
294
|
+
/** Parse `create` argv (after the word `create`). `cwd` resolves the dir. */
|
|
295
|
+
export function parseCreateArgs(argv: string[], cwd: string): ParsedCreateArgs {
|
|
296
|
+
const positional: string[] = [];
|
|
297
|
+
const flags: Record<string, string> = {};
|
|
298
|
+
for (let i = 0; i < argv.length; i++) {
|
|
299
|
+
const a = argv[i];
|
|
300
|
+
const m = /^--(title|cli|env-prefix)(?:=(.*))?$/.exec(a);
|
|
301
|
+
if (m) {
|
|
302
|
+
const value = m[2] ?? argv[++i];
|
|
303
|
+
if (value === undefined)
|
|
304
|
+
throw new Error(`--${m[1]} needs a value\n${CREATE_USAGE}`);
|
|
305
|
+
flags[m[1]] = value;
|
|
306
|
+
} else if (a.startsWith("-")) {
|
|
307
|
+
throw new Error(`unknown flag ${a}\n${CREATE_USAGE}`);
|
|
308
|
+
} else positional.push(a);
|
|
309
|
+
}
|
|
310
|
+
if (positional.length < 1 || positional.length > 2)
|
|
311
|
+
throw new Error(CREATE_USAGE);
|
|
312
|
+
const [name, dir] = positional;
|
|
313
|
+
return {
|
|
314
|
+
name,
|
|
315
|
+
dir: path.resolve(cwd, dir ?? name),
|
|
316
|
+
title: flags.title,
|
|
317
|
+
cli: flags.cli,
|
|
318
|
+
envPrefix: flags["env-prefix"],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const CREATE_HELP = `${CREATE_USAGE}
|
|
323
|
+
|
|
324
|
+
Scaffold a new Den distribution in [dir] (default ./<name>):
|
|
325
|
+
den.yml, package.json (pinning this Den's exact version), bin/<cli>,
|
|
326
|
+
a Makefile including Den's make targets, config/<name>.yml,
|
|
327
|
+
config/environments/local.json, manifests/ and tests/pillars/.
|
|
328
|
+
|
|
329
|
+
--title <title> brand name shown to people (default: <Name>)
|
|
330
|
+
--cli <cli> the command people type (default: <name>)
|
|
331
|
+
--env-prefix <PREFIX> env-var prefix (default: <NAME>)`;
|
|
332
|
+
|
|
333
|
+
/** Run `create` end to end; returns the process exit code. */
|
|
334
|
+
export function runCreate(argv: string[], cwd: string): number {
|
|
335
|
+
if (argv.includes("-h") || argv.includes("--help")) {
|
|
336
|
+
console.log(CREATE_HELP);
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
const args = parseCreateArgs(argv, cwd);
|
|
341
|
+
const written = writeDistribution(args.dir, {
|
|
342
|
+
...args,
|
|
343
|
+
denVersion: runningDenVersion(),
|
|
344
|
+
});
|
|
345
|
+
const cli = args.cli ?? args.name;
|
|
346
|
+
const rel = path.relative(cwd, args.dir) || ".";
|
|
347
|
+
console.log(
|
|
348
|
+
`Created ${args.name} in ${args.dir} (${written.length} files).`,
|
|
349
|
+
);
|
|
350
|
+
console.log("");
|
|
351
|
+
console.log("Next:");
|
|
352
|
+
console.log(` cd ${rel}`);
|
|
353
|
+
console.log(" npm install");
|
|
354
|
+
console.log(` ./bin/${cli} --help`);
|
|
355
|
+
return 0;
|
|
356
|
+
} catch (e) {
|
|
357
|
+
console.error(`create: ${(e as Error).message}`);
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `edge up | down | status | trust | untrust | routes`
|
|
3
|
+
*
|
|
4
|
+
* This org's HTTPS edge: own CA + cert, routes at `<name>.<edge.domain>`.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { readBrand } from "../../shared/brand.js";
|
|
8
|
+
import { readEdgeConfig } from "../../shared/edge/config.js";
|
|
9
|
+
import { edgeDomains, edgeNames, routeUrl } from "../../shared/edge/render.js";
|
|
10
|
+
import {
|
|
11
|
+
caDir,
|
|
12
|
+
caTrusted,
|
|
13
|
+
edgeDown,
|
|
14
|
+
edgeUp,
|
|
15
|
+
trustCa,
|
|
16
|
+
untrustCa,
|
|
17
|
+
} from "../../shared/edge/runtime.js";
|
|
18
|
+
import { edgeStatus } from "../../shared/edge/status.js";
|
|
19
|
+
|
|
20
|
+
export async function cmdEdge(args: string[]): Promise<void> {
|
|
21
|
+
const sub = args[0] ?? "status";
|
|
22
|
+
const cfg = readEdgeConfig();
|
|
23
|
+
const n = edgeNames(cfg.org);
|
|
24
|
+
const published = cfg.ports
|
|
25
|
+
? `host :${cfg.ports.http} / :${cfg.ports.https}`
|
|
26
|
+
: `not published (reach it on Docker network ${n.network})`;
|
|
27
|
+
switch (sub) {
|
|
28
|
+
case "up": {
|
|
29
|
+
const { certs } = await edgeUp(cfg);
|
|
30
|
+
console.log(
|
|
31
|
+
chalk.green(
|
|
32
|
+
`✓ ${n.container} up — ${edgeDomains(cfg)
|
|
33
|
+
.map((d) => `*.${d}`)
|
|
34
|
+
.join(", ")}`,
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
console.log(` ports ${published}`);
|
|
38
|
+
if (certs.issued)
|
|
39
|
+
console.log(` cert issued for ${certs.names.join(", ")}`);
|
|
40
|
+
if (caTrusted(cfg) === false)
|
|
41
|
+
console.log(
|
|
42
|
+
chalk.yellow(
|
|
43
|
+
" CA not trusted yet — run `edge trust` (asks for your password once)",
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
for (const name of ["edge", ...Object.keys(cfg.routes)])
|
|
47
|
+
console.log(` ${routeUrl(name, cfg)}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
case "down":
|
|
51
|
+
edgeDown(cfg);
|
|
52
|
+
console.log(chalk.green(`✓ ${n.container} down`));
|
|
53
|
+
return;
|
|
54
|
+
case "trust":
|
|
55
|
+
process.exitCode = trustCa(cfg);
|
|
56
|
+
return;
|
|
57
|
+
case "untrust":
|
|
58
|
+
process.exitCode = untrustCa(cfg);
|
|
59
|
+
return;
|
|
60
|
+
case "routes":
|
|
61
|
+
for (const r of edgeStatus(cfg).routes)
|
|
62
|
+
console.log(`${r.url.padEnd(52)} → ${r.upstream}`);
|
|
63
|
+
return;
|
|
64
|
+
case "status": {
|
|
65
|
+
const st = edgeStatus(cfg);
|
|
66
|
+
console.log(chalk.bold(`${readBrand().name} edge — ${st.org}`));
|
|
67
|
+
console.log(` container ${st.container}: ${st.state}`);
|
|
68
|
+
console.log(` ports ${published}`);
|
|
69
|
+
console.log(` domains ${st.domains.join(", ")} (+ every subdomain)`);
|
|
70
|
+
console.log(
|
|
71
|
+
` CA ${caDir(cfg)} — ${st.caTrusted === null ? "unknown" : st.caTrusted ? "trusted" : "NOT trusted"}`,
|
|
72
|
+
);
|
|
73
|
+
if (st.certIssued) console.log(` cert ${st.certNames.join(", ")}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Unknown edge command "${sub}". Use: up | down | status | trust | untrust | routes`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|