@monster-orange/den 0.2.8 → 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 +1547 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +507 -4
- 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/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 +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +141 -5
- 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/default-workspace.ts +16 -1
- 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/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- 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 +12 -3
- 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 +7 -2
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +548 -60
- package/src/ui/cli/notice.ts +20 -0
- 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/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +22 -1
- package/src/ui/mcp/tools.ts +50 -7
- package/src/ui/server/appearance-prefs.ts +95 -0
- 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 +23 -44
- 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 +532 -51
- 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 +14 -2
- 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 +12 -1
- 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 +45 -3
- 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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What starting a repo runs (0.3.0-dev.35; backported to 0.2.8): the one
|
|
3
|
+
* decision `up <repo>` (its dependency closure) and `up -a` / `-A` share.
|
|
4
|
+
*
|
|
5
|
+
* - the repo's own `up` → the repo
|
|
6
|
+
* - none, but its services have one → those services: all of them, or,
|
|
7
|
+
* for a repo started only as a
|
|
8
|
+
* dependency, the ones its
|
|
9
|
+
* dependents name (`depends_on:
|
|
10
|
+
* [acme/api]`); every one when any
|
|
11
|
+
* dependent needs the repo as a
|
|
12
|
+
* whole
|
|
13
|
+
* - no up at all → skipped ("no local up command")
|
|
14
|
+
*
|
|
15
|
+
* A repo that can't be resolved in the workspace is passed through
|
|
16
|
+
* unchanged, so the runner reports it as before. Reads manifests from disk:
|
|
17
|
+
* call it once the start set's clones exist.
|
|
18
|
+
*/
|
|
19
|
+
import { readManifest } from "../manifest/read-manifest.js";
|
|
20
|
+
import { depService } from "../manifest/deps.js";
|
|
21
|
+
import type { DerivedGraph } from "./derivation/types.js";
|
|
22
|
+
import {
|
|
23
|
+
getWorkspaceServices,
|
|
24
|
+
previewServiceCommand,
|
|
25
|
+
resolveService,
|
|
26
|
+
} from "./service-runner.js";
|
|
27
|
+
|
|
28
|
+
export interface UpTargets {
|
|
29
|
+
/** The start order, a repo replaced by its services where it has no up. */
|
|
30
|
+
order: string[];
|
|
31
|
+
/** Repos with no up at all (said and skipped, never fatal). */
|
|
32
|
+
skippedNoUp: string[];
|
|
33
|
+
/** repo → the service addresses that start in its place. */
|
|
34
|
+
expanded: Map<string, string[]>;
|
|
35
|
+
/**
|
|
36
|
+
* A list of dependency repos as the start set has them: an expanded repo
|
|
37
|
+
* becomes its services, a skipped one is dropped (nothing to wait on).
|
|
38
|
+
*/
|
|
39
|
+
depsAs(deps: string[]): string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The repo part of an address ("acme/api" → "acme"). */
|
|
43
|
+
export const repoOfAddress = (address: string): string => address.split("/")[0];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param order repos in start order
|
|
47
|
+
* @param opts.targets repos started for their own sake (the seed, or every
|
|
48
|
+
* `-a` candidate): all their services start
|
|
49
|
+
* @param opts.graph the derived graph: who depends on whom
|
|
50
|
+
*/
|
|
51
|
+
export function resolveUpTargets(
|
|
52
|
+
order: string[],
|
|
53
|
+
workspace: string,
|
|
54
|
+
opts: { targets: ReadonlySet<string>; graph?: DerivedGraph },
|
|
55
|
+
): UpTargets {
|
|
56
|
+
const services = getWorkspaceServices(workspace);
|
|
57
|
+
const expanded = new Map<string, string[]>();
|
|
58
|
+
const skippedNoUp: string[] = [];
|
|
59
|
+
const out: string[] = [];
|
|
60
|
+
const runs = (address: string): boolean =>
|
|
61
|
+
previewServiceCommand(address, "up", workspace).kind === "run";
|
|
62
|
+
for (const repo of order) {
|
|
63
|
+
const own = previewServiceCommand(repo, "up", workspace).kind;
|
|
64
|
+
if (
|
|
65
|
+
own === "run" ||
|
|
66
|
+
own === "via-repo" ||
|
|
67
|
+
!resolveService(repo, workspace)
|
|
68
|
+
) {
|
|
69
|
+
out.push(repo);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const withUp = services
|
|
73
|
+
.filter((s) => s.repoName === repo && s.address !== repo)
|
|
74
|
+
.map((s) => s.address)
|
|
75
|
+
.filter(runs);
|
|
76
|
+
if (withUp.length === 0) {
|
|
77
|
+
skippedNoUp.push(repo);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const wanted = opts.targets.has(repo)
|
|
81
|
+
? null
|
|
82
|
+
: namedByDependents(repo, order, workspace, opts.graph);
|
|
83
|
+
const start = wanted ? withUp.filter((a) => wanted.has(a)) : withUp;
|
|
84
|
+
const pick = start.length ? start : withUp;
|
|
85
|
+
expanded.set(repo, pick);
|
|
86
|
+
out.push(...pick);
|
|
87
|
+
}
|
|
88
|
+
const skipped = new Set(skippedNoUp);
|
|
89
|
+
return {
|
|
90
|
+
order: out,
|
|
91
|
+
skippedNoUp,
|
|
92
|
+
expanded,
|
|
93
|
+
depsAs: (deps) =>
|
|
94
|
+
deps.flatMap((d) => (skipped.has(d) ? [] : (expanded.get(d) ?? [d]))),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The services of `repo` its dependents in `order` name, or null when one
|
|
100
|
+
* of them needs the repo as a whole (names it bare, or depends on it by a
|
|
101
|
+
* derived edge with no `depends_on` naming a service of it).
|
|
102
|
+
*/
|
|
103
|
+
function namedByDependents(
|
|
104
|
+
repo: string,
|
|
105
|
+
order: string[],
|
|
106
|
+
workspace: string,
|
|
107
|
+
graph?: DerivedGraph,
|
|
108
|
+
): Set<string> | null {
|
|
109
|
+
const named = new Set<string>();
|
|
110
|
+
const dependents = new Set(
|
|
111
|
+
(graph?.edges ?? [])
|
|
112
|
+
.filter((e) => e.to === repo && e.from !== repo)
|
|
113
|
+
.map((e) => e.from),
|
|
114
|
+
);
|
|
115
|
+
for (const dependent of order) {
|
|
116
|
+
if (dependent === repo) continue;
|
|
117
|
+
const resolved = resolveService(dependent, workspace);
|
|
118
|
+
const manifest = resolved ? readManifest(resolved.repoPath) : null;
|
|
119
|
+
let mine = 0;
|
|
120
|
+
for (const s of Object.values(manifest?.services ?? {}))
|
|
121
|
+
for (const dep of s?.depends_on ?? []) {
|
|
122
|
+
const d = depService(dep);
|
|
123
|
+
if (d === repo) return null;
|
|
124
|
+
if (repoOfAddress(d) === repo) {
|
|
125
|
+
named.add(d);
|
|
126
|
+
mine++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (dependents.has(dependent) && mine === 0) return null;
|
|
130
|
+
}
|
|
131
|
+
return named.size ? named : null;
|
|
132
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repo health rollup, typed for TypeScript callers (`<cli> status`).
|
|
3
|
+
*
|
|
4
|
+
* The rule itself is one plain ES module, src/ui/server/public/health-rollup.js,
|
|
5
|
+
* because the browser runs it too (the header row and the graph): one
|
|
6
|
+
* function everywhere, so the row, its chip, the graph and `status` never
|
|
7
|
+
* disagree. See that file for the rule.
|
|
8
|
+
*/
|
|
9
|
+
import * as rollup from "../../ui/server/public/health-rollup.js";
|
|
10
|
+
|
|
11
|
+
export type RepoState = "down" | "degraded" | "ok" | "unknown";
|
|
12
|
+
|
|
13
|
+
export interface HealthSignal {
|
|
14
|
+
service: string;
|
|
15
|
+
/** true up, false down or failing, null not checked (or not yet). */
|
|
16
|
+
ok: boolean | null | undefined;
|
|
17
|
+
isEntrypoint?: boolean;
|
|
18
|
+
/** Not answered yet this cycle: the repo stays unknown until it has. */
|
|
19
|
+
awaiting?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RepoRollup {
|
|
23
|
+
state: RepoState;
|
|
24
|
+
/** The entry service's name, or null when none can be told. */
|
|
25
|
+
entry: string | null;
|
|
26
|
+
/** Checked services that are down, by name. */
|
|
27
|
+
down: string[];
|
|
28
|
+
checked: number;
|
|
29
|
+
unchecked: number;
|
|
30
|
+
/** Checks that haven't answered yet; any holds the state at unknown. */
|
|
31
|
+
awaiting: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const STATE_RANK: Record<RepoState, number> = rollup.STATE_RANK;
|
|
35
|
+
|
|
36
|
+
export const rollupRepo = rollup.rollupRepo as (
|
|
37
|
+
repo: string,
|
|
38
|
+
signals: HealthSignal[],
|
|
39
|
+
) => RepoRollup;
|
|
40
|
+
|
|
41
|
+
export const describeRollup = rollup.describeRollup as (
|
|
42
|
+
r: RepoRollup,
|
|
43
|
+
) => string;
|
|
44
|
+
|
|
45
|
+
export const compareRepos = rollup.compareRepos as (
|
|
46
|
+
a: { state: RepoState; repo: string; focused?: boolean },
|
|
47
|
+
b: { state: RepoState; repo: string; focused?: boolean },
|
|
48
|
+
) => number;
|
package/src/shared/open-url.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* Order, first answer wins:
|
|
7
7
|
* 1. manifest `services.<svc>.urls.<env>`
|
|
8
8
|
* 2. local only: the edge route for the service's compose container,
|
|
9
|
-
* `https://<route>.<stack>.<edge domain
|
|
9
|
+
* `https://<route>.<stack>.<edge domain>` (`http://` and the edge's http
|
|
10
|
+
* port for `tls: none`, 0.3.0-dev.40); the stack-wide service (the
|
|
10
11
|
* entrypoint, or one named after the stack) takes the `app` route, else
|
|
11
12
|
* the first route
|
|
12
13
|
* 3. the origin of the env's HTTP health URL
|
|
@@ -32,11 +33,22 @@ export function httpUrl(raw: unknown): string | null {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* How the edge serves its routes: https on 443 (every edge.tls strategy but
|
|
38
|
+
* `none`), or http on its http port for `tls: none`. A non-default port is
|
|
39
|
+
* part of the URL.
|
|
40
|
+
*/
|
|
41
|
+
export interface EdgeOrigin {
|
|
42
|
+
https: boolean;
|
|
43
|
+
port?: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
function edgeRouteUrl(
|
|
36
47
|
manifest: DenManifest,
|
|
37
48
|
subsystem: string,
|
|
38
49
|
repoName: string,
|
|
39
50
|
edgeDomain: string,
|
|
51
|
+
origin: EdgeOrigin,
|
|
40
52
|
): string | null {
|
|
41
53
|
const routes = manifest.compose?.routes;
|
|
42
54
|
if (!routes || !Object.keys(routes).length) return null;
|
|
@@ -51,7 +63,11 @@ function edgeRouteUrl(
|
|
|
51
63
|
});
|
|
52
64
|
if (!route && (manifest.entrypoint === subsystem || subsystem === stack))
|
|
53
65
|
route = routes.app ? "app" : names[0];
|
|
54
|
-
|
|
66
|
+
if (!route) return null;
|
|
67
|
+
const scheme = origin.https ? "https" : "http";
|
|
68
|
+
const dflt = origin.https ? 443 : 80;
|
|
69
|
+
const port = origin.port && origin.port !== dflt ? `:${origin.port}` : "";
|
|
70
|
+
return `${scheme}://${route}.${stack}.${edgeDomain}${port}/`;
|
|
55
71
|
}
|
|
56
72
|
|
|
57
73
|
export function openUrlFor(opts: {
|
|
@@ -63,6 +79,8 @@ export function openUrlFor(opts: {
|
|
|
63
79
|
healthUrl?: string | null;
|
|
64
80
|
/** Primary edge domain; local edge routes are skipped without it. */
|
|
65
81
|
edgeDomain?: string | null;
|
|
82
|
+
/** The edge's scheme and port (default: https on 443). */
|
|
83
|
+
edgeOrigin?: EdgeOrigin;
|
|
66
84
|
}): string | null {
|
|
67
85
|
const svc = opts.manifest.services?.[opts.subsystem];
|
|
68
86
|
const declared = httpUrl(
|
|
@@ -75,6 +93,7 @@ export function openUrlFor(opts: {
|
|
|
75
93
|
opts.subsystem,
|
|
76
94
|
opts.repoName,
|
|
77
95
|
opts.edgeDomain,
|
|
96
|
+
opts.edgeOrigin ?? { https: true },
|
|
78
97
|
);
|
|
79
98
|
if (routed) return routed;
|
|
80
99
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* GitHub sources clone through the user's gh, a repo with no source clones
|
|
18
18
|
* from its catalog `remote:` through plain git (SEC-0).
|
|
19
19
|
*/
|
|
20
|
-
import { ConfigError } from "./config-error.js";
|
|
20
|
+
import { ConfigError, type ConfigIssue } from "./config-error.js";
|
|
21
21
|
import type { ServiceCatalog, ServiceCatalogEntry } from "./types/index.js";
|
|
22
22
|
|
|
23
23
|
/** `orgs.<id>.sources[i]`: a git host and the owner (or group) it claims. */
|
|
@@ -65,13 +65,23 @@ const sourceOwner = (s: OrgSourceConfig): string =>
|
|
|
65
65
|
(s.owner ?? s.group ?? "").trim();
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* Check the ORG-1d keys of `orgs:` and throw
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* Check the ORG-1d keys of `orgs:` and throw a ConfigError listing every
|
|
69
|
+
* problem (0.3.0-dev.40; it threw on the first), each naming its key. A
|
|
70
|
+
* problem with a source skips the rest of that source; one with `sources`
|
|
71
|
+
* or `repos` as a whole skips that key. Entries without them are not
|
|
72
|
+
* looked at (C1). A repo listed by two orgs, or an owner claimed by two
|
|
73
|
+
* orgs' sources (or by a source and a legacy org of that name), names both.
|
|
72
74
|
*/
|
|
73
75
|
export function validateOrgSources(orgs: unknown, file: string): void {
|
|
74
76
|
if (!orgs || typeof orgs !== "object" || Array.isArray(orgs)) return;
|
|
77
|
+
const issues: ConfigIssue[] = [];
|
|
78
|
+
// `text` is the message as it reads after "<file>: ".
|
|
79
|
+
const bad = (key: string, text: string): void => {
|
|
80
|
+
const message = text.startsWith(key)
|
|
81
|
+
? text.slice(key.length).replace(/^:?\s*/, "")
|
|
82
|
+
: text;
|
|
83
|
+
issues.push({ file, key, message, text });
|
|
84
|
+
};
|
|
75
85
|
const entries = Object.entries(orgs as Record<string, unknown>);
|
|
76
86
|
const repoAt = new Map<string, string>();
|
|
77
87
|
const ownerAt = new Map<string, string>();
|
|
@@ -81,67 +91,74 @@ export function validateOrgSources(orgs: unknown, file: string): void {
|
|
|
81
91
|
const at = `orgs.${id}`;
|
|
82
92
|
if ("sources" in e) {
|
|
83
93
|
if (!Array.isArray(e.sources))
|
|
84
|
-
|
|
85
|
-
`${
|
|
94
|
+
bad(
|
|
95
|
+
`${at}.sources`,
|
|
96
|
+
`${at}.sources must be a list of { host, owner } or { host, group }`,
|
|
86
97
|
);
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const key of Object.keys(src))
|
|
95
|
-
if (!["host", "owner", "group"].includes(key))
|
|
96
|
-
throw new ConfigError(
|
|
97
|
-
`${file}: ${k}: unknown key "${key}" (known: host, owner, group)`,
|
|
98
|
+
else
|
|
99
|
+
e.sources.forEach((s: unknown, i: number) => {
|
|
100
|
+
const k = `${at}.sources[${i}]`;
|
|
101
|
+
if (!s || typeof s !== "object" || Array.isArray(s))
|
|
102
|
+
return bad(
|
|
103
|
+
k,
|
|
104
|
+
`${k} must be a map: { host: github, owner: <owner> }`,
|
|
98
105
|
);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
);
|
|
103
|
-
const hasOwner = src.owner !== undefined;
|
|
104
|
-
const hasGroup = src.group !== undefined;
|
|
105
|
-
if (hasOwner === hasGroup)
|
|
106
|
-
throw new ConfigError(
|
|
107
|
-
`${file}: ${k} needs exactly one of owner or group`,
|
|
106
|
+
const src = s as Record<string, unknown>;
|
|
107
|
+
const unknown = Object.keys(src).find(
|
|
108
|
+
(key) => !["host", "owner", "group"].includes(key),
|
|
108
109
|
);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
110
|
+
if (unknown !== undefined)
|
|
111
|
+
return bad(
|
|
112
|
+
k,
|
|
113
|
+
`${k}: unknown key "${unknown}" (known: host, owner, group)`,
|
|
114
|
+
);
|
|
115
|
+
if (typeof src.host !== "string" || !HOST_RE.test(src.host))
|
|
116
|
+
return bad(
|
|
117
|
+
`${k}.host`,
|
|
118
|
+
`${k}.host must be a git host id such as github or gitlab`,
|
|
119
|
+
);
|
|
120
|
+
const hasOwner = src.owner !== undefined;
|
|
121
|
+
const hasGroup = src.group !== undefined;
|
|
122
|
+
if (hasOwner === hasGroup)
|
|
123
|
+
return bad(k, `${k} needs exactly one of owner or group`);
|
|
124
|
+
const key = hasOwner ? "owner" : "group";
|
|
125
|
+
const v = src[key];
|
|
126
|
+
const ok =
|
|
127
|
+
typeof v === "string" &&
|
|
128
|
+
(src.host === "github" ? GH_OWNER_RE : GROUP_RE).test(v.trim());
|
|
129
|
+
if (!ok)
|
|
130
|
+
return bad(
|
|
131
|
+
`${k}.${key}`,
|
|
132
|
+
src.host === "github"
|
|
133
|
+
? `${k}.${key} must be a GitHub owner name (letters, digits and hyphens)`
|
|
134
|
+
: `${k}.${key} must be an owner or group path`,
|
|
135
|
+
);
|
|
136
|
+
const claim = `${src.host}:${lower((v as string).trim())}`;
|
|
137
|
+
const prior = ownerAt.get(claim);
|
|
138
|
+
if (prior && !prior.startsWith(`${at}.`))
|
|
139
|
+
return bad(
|
|
140
|
+
k,
|
|
141
|
+
`${src.host} owner ${(v as string).trim()} is claimed by both ${prior} and ${k}`,
|
|
142
|
+
);
|
|
143
|
+
if (!prior) ownerAt.set(claim, k);
|
|
144
|
+
});
|
|
128
145
|
}
|
|
129
146
|
if ("repos" in e) {
|
|
130
147
|
if (
|
|
131
148
|
!Array.isArray(e.repos) ||
|
|
132
149
|
e.repos.some((r) => typeof r !== "string" || !r.trim())
|
|
133
150
|
)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
151
|
+
bad(`${at}.repos`, `${at}.repos must be a list of repo names`);
|
|
152
|
+
else
|
|
153
|
+
for (const r of e.repos as string[]) {
|
|
154
|
+
const prior = repoAt.get(r.trim());
|
|
155
|
+
if (prior && prior !== `${at}.repos`)
|
|
156
|
+
bad(
|
|
157
|
+
`${at}.repos`,
|
|
158
|
+
`repo ${r.trim()} is claimed by both ${prior} and ${at}.repos`,
|
|
159
|
+
);
|
|
160
|
+
else repoAt.set(r.trim(), `${at}.repos`);
|
|
161
|
+
}
|
|
145
162
|
}
|
|
146
163
|
}
|
|
147
164
|
// A legacy org is the GitHub owner of its own name: a source elsewhere that
|
|
@@ -150,11 +167,13 @@ export function validateOrgSources(orgs: unknown, file: string): void {
|
|
|
150
167
|
if (isSourcedOrg(raw as WorkOrgConfig)) continue;
|
|
151
168
|
const prior = ownerAt.get(`github:${lower(id)}`);
|
|
152
169
|
if (prior)
|
|
153
|
-
|
|
154
|
-
|
|
170
|
+
bad(
|
|
171
|
+
`orgs.${id}`,
|
|
172
|
+
`github owner ${id} is claimed by both ${prior} and orgs.${id} ` +
|
|
155
173
|
`(an org with no sources: is the GitHub owner of its own name)`,
|
|
156
174
|
);
|
|
157
175
|
}
|
|
176
|
+
if (issues.length) throw new ConfigError(issues);
|
|
158
177
|
}
|
|
159
178
|
|
|
160
179
|
/** `gh: owner/repo`'s owner, as spelled, or "" (the legacy orgOf). */
|
package/src/shared/org.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* org:
|
|
6
6
|
* id: globex # defaults to edge.org
|
|
7
|
-
* title: Globex # defaults to the id
|
|
7
|
+
* title: Globex # defaults to the github_org's `orgs:` label, then the id
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
@@ -31,12 +31,60 @@ export function resolveOrg(
|
|
|
31
31
|
return { id, title: parsed?.org?.title?.trim() || id };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function readParsed(root: string): unknown {
|
|
35
|
+
return yaml.parse(
|
|
36
|
+
fs.readFileSync(path.join(root, product().configFile), "utf-8"),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
export function readOrg(root: string = getDistRoot()): Org | undefined {
|
|
35
41
|
try {
|
|
36
|
-
return resolveOrg(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
return resolveOrg(readParsed(root) as Parameters<typeof resolveOrg>[0]);
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Where the shell's org label came from (0.3.0-dev.18, additive). */
|
|
49
|
+
export type OrgTitleSource = "org.title" | "orgs" | "id";
|
|
50
|
+
|
|
51
|
+
/** The org as the shell labels it: `title` plus where it came from. */
|
|
52
|
+
export interface OrgView extends Org {
|
|
53
|
+
title_source: OrgTitleSource;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Pure: the org's display label for the top bar. `org.title` first, then
|
|
58
|
+
* the `orgs:` label of the GitHub org (`github_org`, the label the
|
|
59
|
+
* Environments page shows), then the id. A distribution with only
|
|
60
|
+
* `edge.org: <id>` no longer shows its bare id when it has a label.
|
|
61
|
+
*/
|
|
62
|
+
export function resolveOrgView(
|
|
63
|
+
parsed:
|
|
64
|
+
| {
|
|
65
|
+
org?: OrgConfig;
|
|
66
|
+
edge?: { org?: string };
|
|
67
|
+
github_org?: string;
|
|
68
|
+
orgs?: Record<string, { label?: string } | null | undefined>;
|
|
69
|
+
}
|
|
70
|
+
| null
|
|
71
|
+
| undefined,
|
|
72
|
+
): OrgView | undefined {
|
|
73
|
+
const org = resolveOrg(parsed);
|
|
74
|
+
if (!org) return undefined;
|
|
75
|
+
if (parsed?.org?.title?.trim()) return { ...org, title_source: "org.title" };
|
|
76
|
+
const gh =
|
|
77
|
+
typeof parsed?.github_org === "string" ? parsed.github_org.trim() : "";
|
|
78
|
+
const label = gh ? parsed?.orgs?.[gh]?.label : undefined;
|
|
79
|
+
if (typeof label === "string" && label.trim())
|
|
80
|
+
return { ...org, title: label.trim(), title_source: "orgs" };
|
|
81
|
+
return { ...org, title_source: "id" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readOrgView(root: string = getDistRoot()): OrgView | undefined {
|
|
85
|
+
try {
|
|
86
|
+
return resolveOrgView(
|
|
87
|
+
readParsed(root) as Parameters<typeof resolveOrgView>[0],
|
|
40
88
|
);
|
|
41
89
|
} catch {
|
|
42
90
|
return undefined;
|