@monster-orange/den 0.2.7 → 0.3.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 +1655 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +619 -3
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +9 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/agent-actor.ts +38 -3
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +63 -0
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +185 -7
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/environment-manager/catalog.ts +52 -1
- package/src/shared/environment-manager/default-workspace.ts +26 -3
- package/src/shared/environment-manager/git-remote.ts +49 -0
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/environment-manager/workspace-manager.ts +67 -25
- package/src/shared/environment-manager/workspace-org.ts +26 -11
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +328 -0
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +24 -5
- package/src/ui/cli/clone-plan.ts +154 -23
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +131 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/flag-parser.ts +62 -3
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +649 -72
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/org-command.ts +12 -2
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/up-args.ts +15 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +298 -0
- package/src/ui/mcp/server.ts +70 -8
- package/src/ui/mcp/tools.ts +151 -24
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +90 -3
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +29 -45
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +590 -45
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +15 -3
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +24 -4
- package/src/ui/server/release-service.ts +25 -3
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +55 -9
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `none` strategy: no secrets backend. What a distribution gets when it
|
|
3
|
+
* sets no `secrets.provider`. It resolves nothing, never spawns a CLI, and
|
|
4
|
+
* every secret-backed feature reports "not configured" instead of failing.
|
|
5
|
+
*/
|
|
6
|
+
import { product } from "../product.js";
|
|
7
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* "not configured: set secrets.provider and secrets.refs.<name> in
|
|
11
|
+
* config/<cli>.yml" — the one message every surface shows for a secret Den
|
|
12
|
+
* has no configured place for.
|
|
13
|
+
*/
|
|
14
|
+
export function notConfiguredMessage(refName = "<name>"): string {
|
|
15
|
+
return `not configured: set secrets.provider and secrets.refs.${refName} in ${product().configFile}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Thrown when a secret is asked of a backend or ref that is not configured. */
|
|
19
|
+
export class SecretsNotConfiguredError extends Error {
|
|
20
|
+
readonly code = "not-configured";
|
|
21
|
+
constructor(message = notConfiguredMessage()) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "SecretsNotConfiguredError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NoneProvider implements SecretsProvider {
|
|
28
|
+
readonly name = "none" as const;
|
|
29
|
+
|
|
30
|
+
async isReady(): Promise<boolean> {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async get(ref: SecretRef): Promise<string> {
|
|
35
|
+
throw new SecretsNotConfiguredError(
|
|
36
|
+
`${ref.field || "secret"}: no secrets backend is configured (${notConfiguredMessage()})`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async tryGet(): Promise<string | null> {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async set(): Promise<void> {
|
|
45
|
+
throw new SecretsNotConfiguredError();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async preflight(): Promise<PreflightResult> {
|
|
49
|
+
// Nothing to sign in to: `up` and `web` run as they are.
|
|
50
|
+
return { ok: true, message: "no secrets backend configured" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
runPrefix(): string[] {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in secrets strategies, registered through the generic strategy
|
|
3
|
+
* registry (src/shared/strategies/registry.ts) exactly as a plugin would
|
|
4
|
+
* register one. Each entry is `{ id, kind: "secrets", detect(), capabilities }`.
|
|
5
|
+
*
|
|
6
|
+
* Capabilities:
|
|
7
|
+
* - `create(cfg)`: the SecretsProvider for this backend;
|
|
8
|
+
* - `label`: the name people see ("Save to 1Password");
|
|
9
|
+
* - `cli`: the CLI it needs, for `make setup`'s install step;
|
|
10
|
+
* - `refComplete(ref)`: whether a configured `secrets.refs.<name>` gives
|
|
11
|
+
* this backend enough to address a secret;
|
|
12
|
+
* - `env(cfg, env)`: optional vars this backend gives the processes Den
|
|
13
|
+
* starts (only 1password: OP_ACCOUNT, from the distribution's config);
|
|
14
|
+
* - `setup(ctx)`: optional interactive setup (`make <cli>-setup-secrets`).
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { OP_CLI_TIMEOUT_MS } from "../constants.js";
|
|
18
|
+
import {
|
|
19
|
+
getStrategy,
|
|
20
|
+
onPath,
|
|
21
|
+
registerStrategy,
|
|
22
|
+
strategyIds,
|
|
23
|
+
type DetectResult,
|
|
24
|
+
type StrategyEntry,
|
|
25
|
+
} from "../strategies/registry.js";
|
|
26
|
+
import { DopplerProvider } from "./doppler.js";
|
|
27
|
+
import { EnvProvider } from "./env.js";
|
|
28
|
+
import { NoneProvider } from "./none.js";
|
|
29
|
+
import { OnePasswordProvider } from "./onepassword.js";
|
|
30
|
+
import type { SecretsConfig } from "./index.js";
|
|
31
|
+
import type { SecretRef, SecretsProvider } from "./types.js";
|
|
32
|
+
|
|
33
|
+
export const SECRETS_KIND = "secrets";
|
|
34
|
+
|
|
35
|
+
export interface SecretsSetupContext {
|
|
36
|
+
cfg: SecretsConfig;
|
|
37
|
+
provider: SecretsProvider;
|
|
38
|
+
/** Den's package root (its scripts live there). */
|
|
39
|
+
engineRoot: string;
|
|
40
|
+
/** The distribution's CLI name, for remediation lines. */
|
|
41
|
+
cli: string;
|
|
42
|
+
/** Run a command with inherited stdio; its exit code. */
|
|
43
|
+
run(cmd: string, args: string[]): number;
|
|
44
|
+
log(line: string): void;
|
|
45
|
+
error(line: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SecretsCapabilities {
|
|
49
|
+
label: string;
|
|
50
|
+
create(cfg: SecretsConfig): SecretsProvider;
|
|
51
|
+
cli?: { bin: string; formula: string };
|
|
52
|
+
refComplete(ref: Partial<SecretRef>): boolean;
|
|
53
|
+
env?(cfg: SecretsConfig, env: NodeJS.ProcessEnv): Record<string, string>;
|
|
54
|
+
setup?(ctx: SecretsSetupContext): Promise<number>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type SecretsStrategy = StrategyEntry<
|
|
58
|
+
typeof SECRETS_KIND,
|
|
59
|
+
SecretsCapabilities
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
const nonEmpty = (v: unknown): boolean =>
|
|
63
|
+
typeof v === "string" && v.trim().length > 0;
|
|
64
|
+
|
|
65
|
+
/** Flat-key backends address a secret by its key, or an env-shaped field. */
|
|
66
|
+
const flatRefComplete = (ref: Partial<SecretRef>): boolean =>
|
|
67
|
+
nonEmpty(ref.key) || nonEmpty(ref.field);
|
|
68
|
+
|
|
69
|
+
function cliDetect(bin: string, label: string) {
|
|
70
|
+
return (env: NodeJS.ProcessEnv = process.env): DetectResult =>
|
|
71
|
+
onPath(bin, env)
|
|
72
|
+
? { available: true }
|
|
73
|
+
: { available: false, reason: `${label} CLI (${bin}) is not on PATH` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function otherBackends(): string {
|
|
77
|
+
return (
|
|
78
|
+
"Or pick another backend in the org config (secrets.provider: env | doppler | 1password),\n" +
|
|
79
|
+
" or for just you in config/local/user.yml."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const none: SecretsStrategy = {
|
|
84
|
+
id: "none",
|
|
85
|
+
kind: SECRETS_KIND,
|
|
86
|
+
detect: () => ({
|
|
87
|
+
available: true,
|
|
88
|
+
reason: "no secrets backend configured",
|
|
89
|
+
}),
|
|
90
|
+
capabilities: {
|
|
91
|
+
label: "none",
|
|
92
|
+
create: () => new NoneProvider(),
|
|
93
|
+
refComplete: () => false,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const env: SecretsStrategy = {
|
|
98
|
+
id: "env",
|
|
99
|
+
kind: SECRETS_KIND,
|
|
100
|
+
detect: () => ({ available: true }),
|
|
101
|
+
capabilities: {
|
|
102
|
+
label: "environment",
|
|
103
|
+
create: (c) => new EnvProvider(c.keys),
|
|
104
|
+
refComplete: flatRefComplete,
|
|
105
|
+
async setup(ctx) {
|
|
106
|
+
ctx.log(
|
|
107
|
+
"The env backend reads secrets from the process environment.\n" +
|
|
108
|
+
`Export them yourself (direnv, CI secrets, \`doppler run -- ${ctx.cli} up\`).`,
|
|
109
|
+
);
|
|
110
|
+
return 0;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const doppler: SecretsStrategy = {
|
|
116
|
+
id: "doppler",
|
|
117
|
+
kind: SECRETS_KIND,
|
|
118
|
+
detect: cliDetect("doppler", "The Doppler"),
|
|
119
|
+
capabilities: {
|
|
120
|
+
label: "Doppler",
|
|
121
|
+
create: (c) => new DopplerProvider({ ...c.doppler, keys: c.keys }),
|
|
122
|
+
cli: { bin: "doppler", formula: "dopplerhq/cli/doppler" },
|
|
123
|
+
refComplete: flatRefComplete,
|
|
124
|
+
async setup(ctx) {
|
|
125
|
+
if (!doppler.detect().available) {
|
|
126
|
+
ctx.error(
|
|
127
|
+
"✗ The Doppler CLI is not installed.\n" +
|
|
128
|
+
` Install it: brew install dopplerhq/cli/doppler\n then re-run: make ${ctx.cli}-setup-secrets\n\n ` +
|
|
129
|
+
otherBackends(),
|
|
130
|
+
);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
if (!(await ctx.provider.isReady())) {
|
|
134
|
+
ctx.log("→ doppler login");
|
|
135
|
+
if (ctx.run("doppler", ["login"]) !== 0) return 1;
|
|
136
|
+
}
|
|
137
|
+
const scope: string[] = [];
|
|
138
|
+
if (ctx.cfg.doppler?.project)
|
|
139
|
+
scope.push("--project", ctx.cfg.doppler.project);
|
|
140
|
+
if (ctx.cfg.doppler?.config)
|
|
141
|
+
scope.push("--config", ctx.cfg.doppler.config);
|
|
142
|
+
ctx.log(`→ doppler setup ${scope.join(" ")}`.trim());
|
|
143
|
+
if (
|
|
144
|
+
ctx.run("doppler", ["setup", "--no-interactive", ...scope]) !== 0 &&
|
|
145
|
+
scope.length === 0
|
|
146
|
+
) {
|
|
147
|
+
ctx.run("doppler", ["setup"]);
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const onepassword: SecretsStrategy = {
|
|
155
|
+
id: "1password",
|
|
156
|
+
kind: SECRETS_KIND,
|
|
157
|
+
detect: cliDetect("op", "The 1Password"),
|
|
158
|
+
capabilities: {
|
|
159
|
+
label: "1Password",
|
|
160
|
+
create: (c) =>
|
|
161
|
+
new OnePasswordProvider({
|
|
162
|
+
// From the distribution's config or the engineer's shell; never a
|
|
163
|
+
// built-in account.
|
|
164
|
+
account: c.onepassword?.account,
|
|
165
|
+
timeoutMs: OP_CLI_TIMEOUT_MS,
|
|
166
|
+
}),
|
|
167
|
+
cli: { bin: "op", formula: "1password-cli" },
|
|
168
|
+
refComplete: (ref) => nonEmpty(ref.item) && nonEmpty(ref.field),
|
|
169
|
+
env(c, e): Record<string, string> {
|
|
170
|
+
const account = c.onepassword?.account?.trim() || e.OP_ACCOUNT?.trim();
|
|
171
|
+
return account ? { OP_ACCOUNT: account } : {};
|
|
172
|
+
},
|
|
173
|
+
async setup(ctx) {
|
|
174
|
+
if (!onepassword.detect().available) {
|
|
175
|
+
ctx.error(
|
|
176
|
+
"✗ The 1Password CLI (op) is not installed.\n" +
|
|
177
|
+
` Install it: brew install 1password-cli\n\n ` +
|
|
178
|
+
otherBackends(),
|
|
179
|
+
);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
return ctx.run(path.join(ctx.engineRoot, "node_modules", ".bin", "tsx"), [
|
|
183
|
+
path.join(ctx.engineRoot, "scripts", "setup-1password.ts"),
|
|
184
|
+
]);
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const s of [none, env, doppler, onepassword]) registerStrategy(s);
|
|
190
|
+
|
|
191
|
+
/** The registered secrets strategy `id`, or undefined. */
|
|
192
|
+
export function secretsStrategy(id: string): SecretsStrategy | undefined {
|
|
193
|
+
return getStrategy<typeof SECRETS_KIND, SecretsCapabilities>(
|
|
194
|
+
SECRETS_KIND,
|
|
195
|
+
id,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Every registered secrets strategy id. */
|
|
200
|
+
export function secretsStrategyIds(): string[] {
|
|
201
|
+
return strategyIds(SECRETS_KIND);
|
|
202
|
+
}
|
|
@@ -34,7 +34,7 @@ export interface SecretRef {
|
|
|
34
34
|
scoped?: true;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export type SecretsProviderName = "doppler" | "1password" | "env";
|
|
37
|
+
export type SecretsProviderName = "none" | "doppler" | "1password" | "env";
|
|
38
38
|
|
|
39
39
|
export interface PreflightResult {
|
|
40
40
|
ok: boolean;
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { execSync } from "child_process";
|
|
15
|
+
import * as fs from "fs";
|
|
16
|
+
import { isPlaceholderGithubOrg } from "../den-config.js";
|
|
15
17
|
|
|
16
18
|
export interface GitHubCoords {
|
|
17
19
|
owner: string;
|
|
@@ -68,3 +70,41 @@ export function getGitHubCoords(repoPath: string): GitHubCoords | null {
|
|
|
68
70
|
if (!origin) return null;
|
|
69
71
|
return parseGitHubOwnerRepo(origin);
|
|
70
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The GitHub owner of the distribution's OWN checkout: `distRoot`'s origin,
|
|
76
|
+
* and only when `distRoot` is the top of its own git repo. A distribution
|
|
77
|
+
* with no repo of its own (one nested in another checkout, such as Den's)
|
|
78
|
+
* has no owner here: the engine's (or any parent's) remote is never used to
|
|
79
|
+
* pick the org whose repos Den lists. Null when there is none.
|
|
80
|
+
*/
|
|
81
|
+
export function distributionGitHubOwner(distRoot: string): string | null {
|
|
82
|
+
try {
|
|
83
|
+
const top = execSync("git rev-parse --show-toplevel", {
|
|
84
|
+
cwd: distRoot,
|
|
85
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
}).trim();
|
|
88
|
+
if (!top || fs.realpathSync(top) !== fs.realpathSync(distRoot)) return null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return getGitHubCoords(distRoot)?.owner ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The GitHub org whose repos Den lists for "register a managed repo": the
|
|
97
|
+
* org config's `github_org`, else the distribution's own checkout's owner.
|
|
98
|
+
* Never the engine's. Null (the caller asks for github_org) when neither
|
|
99
|
+
* names a valid owner.
|
|
100
|
+
*/
|
|
101
|
+
export function managedReposOrg(
|
|
102
|
+
githubOrg: string | undefined,
|
|
103
|
+
distRoot: string,
|
|
104
|
+
): string | null {
|
|
105
|
+
const set = githubOrg?.trim();
|
|
106
|
+
// A placeholder (`your-github-org`) is not an org: never list it.
|
|
107
|
+
const configured = set && !isPlaceholderGithubOrg(set) ? set : "";
|
|
108
|
+
const org = configured || distributionGitHubOwner(distRoot);
|
|
109
|
+
return org && /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(org) ? org : null;
|
|
110
|
+
}
|
|
@@ -14,7 +14,7 @@ import { product } from "../product.js";
|
|
|
14
14
|
type CommandDef = string | { cmd: string; detachable?: boolean };
|
|
15
15
|
|
|
16
16
|
/** Whether `subsystem` is the repo's primary service (the one the stack belongs to). */
|
|
17
|
-
function isPrimary(manifest: RawManifest, subsystem: string): boolean {
|
|
17
|
+
export function isPrimary(manifest: RawManifest, subsystem: string): boolean {
|
|
18
18
|
const keys = Object.keys(manifest.services ?? {});
|
|
19
19
|
if (manifest.entrypoint) return manifest.entrypoint === subsystem;
|
|
20
20
|
return keys.length <= 1;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A workspace's stack namespace: the suffix its stacks' Compose projects run
|
|
3
|
+
* under (`<stack>-<namespace>`), and so the names of their containers,
|
|
4
|
+
* volumes and networks.
|
|
5
|
+
*
|
|
6
|
+
* By default it is the workspace's own name (`default`: `main`, see
|
|
7
|
+
* stackWorkspace). A workspace can set `stack_namespace` to opt out: several
|
|
8
|
+
* workspaces may then share one namespace, and a stack keeps its data when
|
|
9
|
+
* its repo moves between workspaces. First match wins:
|
|
10
|
+
*
|
|
11
|
+
* 1. `stack_namespace:` on the workspace in workspaces.yml (per machine)
|
|
12
|
+
* 2. `stack_namespace:` on the same-named blueprint workspace
|
|
13
|
+
* 3. stackWorkspace(name)
|
|
14
|
+
*
|
|
15
|
+
* Every stack code path turns a den workspace into its suffix through
|
|
16
|
+
* resolveStackWorkspaces (status.ts), which calls this; DEN_STACK_WORKSPACE
|
|
17
|
+
* still wins there, for one process.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
checkStackNamespace,
|
|
21
|
+
readBlueprintStackNamespaces,
|
|
22
|
+
readDenConfig,
|
|
23
|
+
type DenConfig,
|
|
24
|
+
} from "../den-config.js";
|
|
25
|
+
import { readWorkspacesFile } from "../environment-manager/workspaces.js";
|
|
26
|
+
import { cachedRead } from "../read-cache.js";
|
|
27
|
+
import type { WorkspacesFile } from "../types/index.js";
|
|
28
|
+
import { stackWorkspace } from "./render.js";
|
|
29
|
+
|
|
30
|
+
/** Which step gave the namespace. */
|
|
31
|
+
export type StackNamespaceSource = "workspace" | "blueprint" | "default";
|
|
32
|
+
|
|
33
|
+
export interface StackNamespace {
|
|
34
|
+
workspace: string;
|
|
35
|
+
/** The Compose project suffix. */
|
|
36
|
+
namespace: string;
|
|
37
|
+
source: StackNamespaceSource;
|
|
38
|
+
/** What it would be with no `stack_namespace` anywhere. */
|
|
39
|
+
defaultNamespace: string;
|
|
40
|
+
/** workspaces.yml overriding a different blueprint value, as a sentence. */
|
|
41
|
+
warnings: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StackNamespaceContext {
|
|
45
|
+
file: Pick<WorkspacesFile, "workspaces">;
|
|
46
|
+
/** Blueprint `stack_namespace` by workspace name. */
|
|
47
|
+
blueprint: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* workspaces.yml and the blueprint's namespaces, as they are now. A
|
|
52
|
+
* workspaces file that cannot be read sets no namespace (its other readers
|
|
53
|
+
* report it), and so does an unreadable org config; a bad
|
|
54
|
+
* `stack_namespace` value still throws.
|
|
55
|
+
*/
|
|
56
|
+
export function readStackNamespaceContext(): StackNamespaceContext {
|
|
57
|
+
return cachedRead("stack-namespace-context", () => {
|
|
58
|
+
let file: StackNamespaceContext["file"] = { workspaces: {} };
|
|
59
|
+
try {
|
|
60
|
+
file = readWorkspacesFile() ?? file;
|
|
61
|
+
} catch {
|
|
62
|
+
/* unreadable: no workspace sets one */
|
|
63
|
+
}
|
|
64
|
+
let config: DenConfig | null = null;
|
|
65
|
+
try {
|
|
66
|
+
config = readDenConfig();
|
|
67
|
+
} catch {
|
|
68
|
+
/* unreadable: no blueprint sets one */
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
file,
|
|
72
|
+
blueprint: config ? readBlueprintStackNamespaces(config) : {},
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve one workspace's stack namespace. A malformed `stack_namespace`
|
|
79
|
+
* (workspaces.yml or blueprint) throws, naming the key: falling back to
|
|
80
|
+
* another name would run the stacks against other volumes.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveStackNamespace(
|
|
83
|
+
name: string,
|
|
84
|
+
ctx: StackNamespaceContext = readStackNamespaceContext(),
|
|
85
|
+
): StackNamespace {
|
|
86
|
+
const defaultNamespace = stackWorkspace(name);
|
|
87
|
+
const own = checkStackNamespace(
|
|
88
|
+
`workspaces.${name}.stack_namespace (config/local/workspaces.yml)`,
|
|
89
|
+
ctx.file.workspaces?.[name]?.stack_namespace,
|
|
90
|
+
);
|
|
91
|
+
const bp = ctx.blueprint[name];
|
|
92
|
+
const warnings: string[] = [];
|
|
93
|
+
if (own) {
|
|
94
|
+
if (bp && bp !== own)
|
|
95
|
+
warnings.push(
|
|
96
|
+
`workspace ${name}: stack_namespace ${own} in workspaces.yml overrides the blueprint's stack_namespace ${bp}`,
|
|
97
|
+
);
|
|
98
|
+
return {
|
|
99
|
+
workspace: name,
|
|
100
|
+
namespace: own,
|
|
101
|
+
source: "workspace",
|
|
102
|
+
defaultNamespace,
|
|
103
|
+
warnings,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (bp)
|
|
107
|
+
return {
|
|
108
|
+
workspace: name,
|
|
109
|
+
namespace: bp,
|
|
110
|
+
source: "blueprint",
|
|
111
|
+
defaultNamespace,
|
|
112
|
+
warnings,
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
workspace: name,
|
|
116
|
+
namespace: defaultNamespace,
|
|
117
|
+
source: "default",
|
|
118
|
+
defaultNamespace,
|
|
119
|
+
warnings,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** One workspace's namespace as the lists show it: resolved, or the error. */
|
|
124
|
+
export interface StackNamespaceRow {
|
|
125
|
+
workspace: string;
|
|
126
|
+
/** The resolved namespace; null when its config is invalid. */
|
|
127
|
+
stack_namespace: string | null;
|
|
128
|
+
source: StackNamespaceSource | null;
|
|
129
|
+
/** The suffix with no `stack_namespace` anywhere. */
|
|
130
|
+
default: string;
|
|
131
|
+
/** True when the namespace is not the default suffix. */
|
|
132
|
+
differs: boolean;
|
|
133
|
+
warnings: string[];
|
|
134
|
+
error?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Every named workspace's stack namespace, for `workspace list` and
|
|
139
|
+
* `/api/workspaces`. A bad value is that row's error, never the list's.
|
|
140
|
+
*/
|
|
141
|
+
export function stackNamespaceRows(
|
|
142
|
+
names: string[],
|
|
143
|
+
ctx?: StackNamespaceContext,
|
|
144
|
+
): StackNamespaceRow[] {
|
|
145
|
+
let c: StackNamespaceContext | null = ctx ?? null;
|
|
146
|
+
let ctxError: string | null = null;
|
|
147
|
+
if (!c)
|
|
148
|
+
try {
|
|
149
|
+
c = readStackNamespaceContext();
|
|
150
|
+
} catch (e) {
|
|
151
|
+
ctxError = (e as Error).message;
|
|
152
|
+
}
|
|
153
|
+
return names.map((workspace) => {
|
|
154
|
+
const fallback = stackWorkspace(workspace);
|
|
155
|
+
try {
|
|
156
|
+
if (!c) throw new Error(ctxError ?? "unreadable");
|
|
157
|
+
const ns = resolveStackNamespace(workspace, c);
|
|
158
|
+
return {
|
|
159
|
+
workspace,
|
|
160
|
+
stack_namespace: ns.namespace,
|
|
161
|
+
source: ns.source,
|
|
162
|
+
default: ns.defaultNamespace,
|
|
163
|
+
differs: ns.namespace !== ns.defaultNamespace,
|
|
164
|
+
warnings: ns.warnings,
|
|
165
|
+
};
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return {
|
|
168
|
+
workspace,
|
|
169
|
+
stack_namespace: null,
|
|
170
|
+
source: null,
|
|
171
|
+
default: fallback,
|
|
172
|
+
differs: false,
|
|
173
|
+
warnings: [],
|
|
174
|
+
error: (e as Error).message,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|