@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,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme runtime: which theme the web UI draws with, and that theme as CSS.
|
|
3
|
+
*
|
|
4
|
+
* Themes come from three places (see src/ui/cli/theme-command.ts):
|
|
5
|
+
* built-in <engineRoot>/themes/den/ Den's own, always present
|
|
6
|
+
* locked <distRoot>/plugins/<id>/ + lock vendored by `theme add`
|
|
7
|
+
* local <distRoot>/plugins/<id>/, no lock the distribution's own
|
|
8
|
+
*
|
|
9
|
+
* The active theme is the user's choice (user.yml `appearance.theme`), else
|
|
10
|
+
* the distribution's (org config `brand.theme`), else den. A choice that is not
|
|
11
|
+
* installed or fails validation falls through to the next, ending at den,
|
|
12
|
+
* with one warning in the server log per bad choice. Only a validated
|
|
13
|
+
* manifest is ever rendered: its values passed the schema's CSS-value rule
|
|
14
|
+
* (no ; { } < > \ url() @import), so writing them into custom properties
|
|
15
|
+
* cannot break out of the declaration.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "fs";
|
|
18
|
+
import * as path from "path";
|
|
19
|
+
import {
|
|
20
|
+
installedThemeIds,
|
|
21
|
+
readLock,
|
|
22
|
+
themeDirFor,
|
|
23
|
+
validationSummary,
|
|
24
|
+
} from "../cli/theme-command.js";
|
|
25
|
+
import {
|
|
26
|
+
themeDirIn,
|
|
27
|
+
validatePluginDir,
|
|
28
|
+
} from "../../shared/plugins/manifest.js";
|
|
29
|
+
import {
|
|
30
|
+
markImage,
|
|
31
|
+
themeFiles,
|
|
32
|
+
validateThemeDir,
|
|
33
|
+
type ColorToken,
|
|
34
|
+
type ThemeManifest,
|
|
35
|
+
type ThemeMode,
|
|
36
|
+
} from "../../shared/theme/contract.js";
|
|
37
|
+
|
|
38
|
+
export const BUILTIN_THEME = "den";
|
|
39
|
+
|
|
40
|
+
export type ThemeOrigin = "built-in" | "locked" | "local";
|
|
41
|
+
|
|
42
|
+
export interface ThemeRoots {
|
|
43
|
+
distRoot: string;
|
|
44
|
+
engineRoot: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InstalledTheme {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
source: ThemeOrigin;
|
|
52
|
+
/** Validates under this Den: only then is it rendered or served. */
|
|
53
|
+
ok: boolean;
|
|
54
|
+
/** Why it is not ok. */
|
|
55
|
+
problem?: string;
|
|
56
|
+
dir: string;
|
|
57
|
+
theme?: ThemeManifest;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const COLOR_TOKENS: readonly ColorToken[] = [
|
|
61
|
+
"bg",
|
|
62
|
+
"surface",
|
|
63
|
+
"raised",
|
|
64
|
+
"sunken",
|
|
65
|
+
"text",
|
|
66
|
+
"muted",
|
|
67
|
+
"faint",
|
|
68
|
+
"border",
|
|
69
|
+
"border-strong",
|
|
70
|
+
"accent",
|
|
71
|
+
"accent-text",
|
|
72
|
+
"accent-fill",
|
|
73
|
+
"on-accent",
|
|
74
|
+
"ok",
|
|
75
|
+
"warn",
|
|
76
|
+
"bad",
|
|
77
|
+
"remote",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
export function builtinThemeDir(engineRoot: string): string {
|
|
81
|
+
return path.join(engineRoot, "themes", BUILTIN_THEME);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inspect(
|
|
85
|
+
id: string,
|
|
86
|
+
dir: string,
|
|
87
|
+
source: ThemeOrigin,
|
|
88
|
+
expectId?: string,
|
|
89
|
+
pluginDir?: string,
|
|
90
|
+
): InstalledTheme {
|
|
91
|
+
// The theme itself, to render; the verdict is `validate`'s for the whole
|
|
92
|
+
// plugin (author/maintainer, `den` range, README, capabilities), so what
|
|
93
|
+
// fails validate never loads and is never offered (PL-SPEC d4).
|
|
94
|
+
const v = validateThemeDir(dir, expectId ? { expectId } : {});
|
|
95
|
+
const verdict = validatePluginDir(
|
|
96
|
+
pluginDir ?? dir,
|
|
97
|
+
expectId ? { expectId } : {},
|
|
98
|
+
);
|
|
99
|
+
const ok = v.ok && verdict.ok;
|
|
100
|
+
return {
|
|
101
|
+
id,
|
|
102
|
+
name: v.theme?.name ?? id,
|
|
103
|
+
version: v.theme?.version ?? "?",
|
|
104
|
+
source,
|
|
105
|
+
ok,
|
|
106
|
+
...(ok ? {} : { problem: validationSummary(verdict.ok ? v : verdict) }),
|
|
107
|
+
dir,
|
|
108
|
+
...(v.theme ? { theme: v.theme } : {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Where plugin `id`'s theme is: plugins/<id>/ for a den-theme.json plugin,
|
|
114
|
+
* else the directory its den-plugin.json theme entry names.
|
|
115
|
+
*/
|
|
116
|
+
function pluginThemeDir(distRoot: string, id: string): string {
|
|
117
|
+
const dir = themeDirFor(distRoot, id);
|
|
118
|
+
return themeDirIn(dir) ?? dir;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Every theme this distribution can use: den first, then plugins/<id>/ sorted. */
|
|
122
|
+
export function listThemes(roots: ThemeRoots): InstalledTheme[] {
|
|
123
|
+
let lock: ReturnType<typeof readLock> = {};
|
|
124
|
+
try {
|
|
125
|
+
lock = readLock(roots.distRoot);
|
|
126
|
+
} catch {
|
|
127
|
+
/* an unreadable lock: every theme is then local; `theme list` reports it */
|
|
128
|
+
}
|
|
129
|
+
const out = [
|
|
130
|
+
inspect(BUILTIN_THEME, builtinThemeDir(roots.engineRoot), "built-in"),
|
|
131
|
+
];
|
|
132
|
+
for (const id of installedThemeIds(roots.distRoot)) {
|
|
133
|
+
if (id === BUILTIN_THEME) continue; // the built-in one is never shadowed
|
|
134
|
+
out.push(
|
|
135
|
+
inspect(
|
|
136
|
+
id,
|
|
137
|
+
pluginThemeDir(roots.distRoot, id),
|
|
138
|
+
lock[id]?.kinds?.includes("theme") ? "locked" : "local",
|
|
139
|
+
id,
|
|
140
|
+
themeDirFor(roots.distRoot, id),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** One theme by id, or null when no such theme is installed. */
|
|
148
|
+
export function findTheme(
|
|
149
|
+
roots: ThemeRoots,
|
|
150
|
+
id: string,
|
|
151
|
+
): InstalledTheme | null {
|
|
152
|
+
if (id === BUILTIN_THEME)
|
|
153
|
+
return inspect(id, builtinThemeDir(roots.engineRoot), "built-in");
|
|
154
|
+
if (!/^[a-z][a-z0-9-]{1,39}$/.test(id)) return null;
|
|
155
|
+
if (!installedThemeIds(roots.distRoot).includes(id)) return null;
|
|
156
|
+
let locked = false;
|
|
157
|
+
try {
|
|
158
|
+
locked = !!readLock(roots.distRoot)[id]?.kinds?.includes("theme");
|
|
159
|
+
} catch {
|
|
160
|
+
/* see listThemes */
|
|
161
|
+
}
|
|
162
|
+
return inspect(
|
|
163
|
+
id,
|
|
164
|
+
pluginThemeDir(roots.distRoot, id),
|
|
165
|
+
locked ? "locked" : "local",
|
|
166
|
+
id,
|
|
167
|
+
themeDirFor(roots.distRoot, id),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface ThemeChoice {
|
|
172
|
+
/** user.yml `appearance.theme`. */
|
|
173
|
+
user?: string | null;
|
|
174
|
+
/** The org config's `brand.theme`. */
|
|
175
|
+
distribution?: string | null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ActiveTheme {
|
|
179
|
+
id: string;
|
|
180
|
+
theme: ThemeManifest;
|
|
181
|
+
dir: string;
|
|
182
|
+
source: ThemeOrigin;
|
|
183
|
+
/** Which choice won. */
|
|
184
|
+
chosenBy: "user" | "distribution" | "default";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const warned = new Set<string>();
|
|
188
|
+
|
|
189
|
+
/** Forget which warnings were logged (tests). */
|
|
190
|
+
export function resetThemeWarnings(): void {
|
|
191
|
+
warned.clear();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The theme to draw with. Never throws: a bad choice is warned about once
|
|
196
|
+
* and skipped. Null only if Den's own theme is unusable (a broken install).
|
|
197
|
+
*/
|
|
198
|
+
export function resolveActiveTheme(
|
|
199
|
+
roots: ThemeRoots,
|
|
200
|
+
choice: ThemeChoice,
|
|
201
|
+
warn: (line: string) => void = (l) => console.warn(l),
|
|
202
|
+
): ActiveTheme | null {
|
|
203
|
+
const candidates: [string, ActiveTheme["chosenBy"], string][] = [];
|
|
204
|
+
if (choice.user) candidates.push([choice.user, "user", "your appearance"]);
|
|
205
|
+
if (choice.distribution)
|
|
206
|
+
candidates.push([
|
|
207
|
+
choice.distribution,
|
|
208
|
+
"distribution",
|
|
209
|
+
"the distribution's `brand.theme`",
|
|
210
|
+
]);
|
|
211
|
+
candidates.push([BUILTIN_THEME, "default", "the default"]);
|
|
212
|
+
for (const [id, chosenBy, label] of candidates) {
|
|
213
|
+
let t: InstalledTheme | null = null;
|
|
214
|
+
try {
|
|
215
|
+
t = findTheme(roots, String(id));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
t = null;
|
|
218
|
+
warnOnce(
|
|
219
|
+
warn,
|
|
220
|
+
`${id}:error`,
|
|
221
|
+
() =>
|
|
222
|
+
`theme "${id}" (${label}) could not be read: ${(e as Error).message}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (t?.ok && t.theme)
|
|
226
|
+
return {
|
|
227
|
+
id: t.id,
|
|
228
|
+
theme: t.theme,
|
|
229
|
+
dir: t.dir,
|
|
230
|
+
source: t.source,
|
|
231
|
+
chosenBy,
|
|
232
|
+
};
|
|
233
|
+
if (id === BUILTIN_THEME && chosenBy === "default") {
|
|
234
|
+
warnOnce(
|
|
235
|
+
warn,
|
|
236
|
+
"den:broken",
|
|
237
|
+
() =>
|
|
238
|
+
`the built-in theme is not valid (${t?.problem ?? "missing"}); the UI has no theme`,
|
|
239
|
+
);
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
warnOnce(warn, `${id}:${t ? "invalid" : "missing"}`, () =>
|
|
243
|
+
t
|
|
244
|
+
? `theme "${id}" (${label}) is not valid under this Den: ${t.problem}; using the next choice`
|
|
245
|
+
: `theme "${id}" (${label}) is not installed; using the next choice`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function warnOnce(
|
|
252
|
+
warn: (line: string) => void,
|
|
253
|
+
key: string,
|
|
254
|
+
line: () => string,
|
|
255
|
+
): void {
|
|
256
|
+
if (warned.has(key)) return;
|
|
257
|
+
warned.add(key);
|
|
258
|
+
warn(`[theme] ${line()}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** URL of a file in a theme, as /theme/<id>/<file> serves it. */
|
|
262
|
+
export function themeAssetUrl(id: string, rel: string): string {
|
|
263
|
+
return `/theme/${id}/${rel.split("/").map(encodeURIComponent).join("/")}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const DEFAULT_TYPE = {
|
|
267
|
+
sans: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
|
|
268
|
+
mono: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
269
|
+
ja: '"Hiragino Sans", "Noto Sans JP", "Yu Gothic", Meiryo, sans-serif',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/** A generic family ends a stack that can always resolve. */
|
|
273
|
+
const GENERIC_FAMILY =
|
|
274
|
+
/(?:^|,)\s*(?:sans-serif|serif|monospace|cursive|fantasy|system-ui|ui-sans-serif|ui-serif|ui-monospace|ui-rounded|math|emoji)\s*$/i;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* A theme's font stack, made safe to use alone: one that does not end in a
|
|
278
|
+
* generic family gets Den's stack after it, so a missing font never drops
|
|
279
|
+
* the UI to the browser's default serif. The pages use only var(--sans),
|
|
280
|
+
* var(--mono), var(--display); the fallbacks live here, nowhere else.
|
|
281
|
+
*/
|
|
282
|
+
export function fontStack(stack: string | undefined, fallback: string): string {
|
|
283
|
+
if (!stack?.trim()) return fallback;
|
|
284
|
+
return GENERIC_FAMILY.test(stack) ? stack : `${stack}, ${fallback}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Den's base rules, the same under every theme. A link in body content is
|
|
289
|
+
* underlined (subtly, in a tint of its own color), so it never reads by
|
|
290
|
+
* color alone: a theme whose accent text is red would otherwise make links
|
|
291
|
+
* look like errors next to --bad. Nav links, buttons and pills are not
|
|
292
|
+
* body content. :where() keeps the rule at zero specificity, so any page
|
|
293
|
+
* style that draws a link differently wins.
|
|
294
|
+
*/
|
|
295
|
+
const BASE_CSS = [
|
|
296
|
+
"/* Den base (every theme) */",
|
|
297
|
+
':where(a[href]:not(nav a, header a, .topbar a, [role="button"], [class*="btn"], [class*="pill"], [class*="tab"])) {',
|
|
298
|
+
" text-decoration-line: underline;",
|
|
299
|
+
" text-decoration-thickness: 1px;",
|
|
300
|
+
" text-underline-offset: 0.2em;",
|
|
301
|
+
" text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);",
|
|
302
|
+
"}",
|
|
303
|
+
':where(a[href]:not(nav a, header a, .topbar a, [role="button"], [class*="btn"], [class*="pill"], [class*="tab"])):hover {',
|
|
304
|
+
" text-decoration-color: currentColor;",
|
|
305
|
+
"}",
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The theme as CSS: @font-face for its fonts, type and shape on :root, and
|
|
310
|
+
* one block of color tokens per mode, selected by <html data-mode>. The
|
|
311
|
+
* theme's default mode also applies when data-mode is unset. Then Den's
|
|
312
|
+
* base rules (BASE_CSS).
|
|
313
|
+
*/
|
|
314
|
+
export function renderThemeCss(id: string, theme: ThemeManifest): string {
|
|
315
|
+
const lines: string[] = [
|
|
316
|
+
`/* ${theme.name} ${theme.version} (${id}, den.theme/v1). Generated by Den from den-theme.json. */`,
|
|
317
|
+
];
|
|
318
|
+
for (const f of theme.fonts ?? [])
|
|
319
|
+
for (const file of f.files) {
|
|
320
|
+
const [lo, hi] = (file.weight ?? "400").split(" ");
|
|
321
|
+
lines.push(
|
|
322
|
+
`@font-face { font-family: "${f.family}"; src: url("${themeAssetUrl(id, file.path)}") format("woff2"); font-weight: ${hi ? `${lo} ${hi}` : lo}; font-style: ${file.style ?? "normal"}; font-display: swap; }`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const type = theme.type ?? {};
|
|
326
|
+
const shape = theme.shape ?? {};
|
|
327
|
+
const sans = fontStack(type.sans, DEFAULT_TYPE.sans);
|
|
328
|
+
lines.push(
|
|
329
|
+
":root {",
|
|
330
|
+
` --sans: ${sans};`,
|
|
331
|
+
` --mono: ${fontStack(type.mono, DEFAULT_TYPE.mono)};`,
|
|
332
|
+
` --display: ${fontStack(type.display, sans)};`,
|
|
333
|
+
` --ja: ${fontStack(type.ja, DEFAULT_TYPE.ja)};`,
|
|
334
|
+
` --radius: ${shape.radius ?? "8px"};`,
|
|
335
|
+
` --radius-button: ${shape["radius-button"] ?? "6px"};`,
|
|
336
|
+
` --density: ${shape.density === "compact" ? "0.85" : "1"};`,
|
|
337
|
+
"}",
|
|
338
|
+
);
|
|
339
|
+
const def: ThemeMode = theme.default_mode ?? "dark";
|
|
340
|
+
for (const mode of [def, def === "dark" ? "light" : "dark"] as ThemeMode[]) {
|
|
341
|
+
const c = theme.modes[mode];
|
|
342
|
+
const sel =
|
|
343
|
+
mode === def
|
|
344
|
+
? `:root,\n:root[data-mode="${mode}"]`
|
|
345
|
+
: `:root[data-mode="${mode}"]`;
|
|
346
|
+
lines.push(`${sel} {`, ` color-scheme: ${mode};`);
|
|
347
|
+
for (const t of COLOR_TOKENS) lines.push(` --${t}: ${c[t]};`);
|
|
348
|
+
lines.push(
|
|
349
|
+
` --shadow: ${c.shadow ?? (mode === "dark" ? "0 12px 30px rgb(0 0 0 / 0.5)" : "0 8px 24px rgb(0 0 0 / 0.14)")};`,
|
|
350
|
+
` --bar: ${c.bar ?? "none"};`,
|
|
351
|
+
"}",
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
lines.push(...BASE_CSS);
|
|
355
|
+
return `${lines.join("\n")}\n`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const ASSET_TYPES: Record<string, string> = {
|
|
359
|
+
".svg": "image/svg+xml",
|
|
360
|
+
".png": "image/png",
|
|
361
|
+
".webp": "image/webp",
|
|
362
|
+
".ico": "image/x-icon",
|
|
363
|
+
".woff2": "font/woff2",
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The theme's browser-tab icon in a mode, relative to its directory: its
|
|
368
|
+
* `favicon` (both modes), else its mark's image (the dark variant in dark
|
|
369
|
+
* mode, if it has one), else null (a text mark: Den's own icon).
|
|
370
|
+
*/
|
|
371
|
+
export function faviconFile(
|
|
372
|
+
theme: ThemeManifest,
|
|
373
|
+
mode: ThemeMode,
|
|
374
|
+
): string | null {
|
|
375
|
+
return theme.favicon ?? markImage(theme.mark, mode)?.file ?? null;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* The file /theme/<id>/<rel> may serve, or null: only a file the validated
|
|
380
|
+
* manifest names (its mark, its favicon, its fonts), only .svg, .png,
|
|
381
|
+
* .webp, .ico or .woff2, only inside the theme's directory. `rel` is the
|
|
382
|
+
* decoded remainder of the URL path.
|
|
383
|
+
*/
|
|
384
|
+
export function themeAsset(
|
|
385
|
+
roots: ThemeRoots,
|
|
386
|
+
id: string,
|
|
387
|
+
rel: string,
|
|
388
|
+
): { file: string; type: string } | null {
|
|
389
|
+
const type = ASSET_TYPES[path.extname(rel).toLowerCase()];
|
|
390
|
+
if (!type) return null;
|
|
391
|
+
const t = findTheme(roots, id);
|
|
392
|
+
if (!t?.ok || !t.theme) return null;
|
|
393
|
+
if (!themeFiles(t.theme).includes(rel)) return null;
|
|
394
|
+
const root = path.resolve(t.dir);
|
|
395
|
+
const file = path.resolve(root, rel);
|
|
396
|
+
if (!file.startsWith(root + path.sep)) return null;
|
|
397
|
+
try {
|
|
398
|
+
// Resolve links too: the real file must still be inside the theme.
|
|
399
|
+
const real = fs.realpathSync(file);
|
|
400
|
+
const realRoot = fs.realpathSync(root);
|
|
401
|
+
if (!real.startsWith(realRoot + path.sep) || !fs.statSync(real).isFile())
|
|
402
|
+
return null;
|
|
403
|
+
return { file: real, type };
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Static assets the graph needs beyond the pages themselves:
|
|
3
|
-
* - the
|
|
3
|
+
* - the UI's own modules in public/ (UI_MODULES: the graph, the appearance
|
|
4
|
+
* controls, the version label, the top bar's chips and overlays),
|
|
5
|
+
* served like header.js;
|
|
4
6
|
* - browser libraries the UI loads, served by Den's own server from
|
|
5
7
|
* node_modules (never a CDN), so the UI works offline. Each is a pinned
|
|
6
8
|
* dependency in package.json. npm hoists Den's dependencies into a
|
|
@@ -19,7 +21,20 @@ export const UI_MODULES: readonly string[] = [
|
|
|
19
21
|
"/graph-model.js",
|
|
20
22
|
"/panel.js",
|
|
21
23
|
"/graph.css",
|
|
24
|
+
"/appearance.js",
|
|
25
|
+
"/version-label.js",
|
|
22
26
|
"/changelog-view.js",
|
|
27
|
+
"/health-fit.js",
|
|
28
|
+
"/health-rollup.js",
|
|
29
|
+
"/shell.js",
|
|
30
|
+
"/settings-registry.js",
|
|
31
|
+
"/settings-page.js",
|
|
32
|
+
"/topbar.js",
|
|
33
|
+
"/edge-page.js",
|
|
34
|
+
"/error-page.js",
|
|
35
|
+
// The UI's catalogs other than English (R19; en is in shell.js).
|
|
36
|
+
"/i18n/ja.js",
|
|
37
|
+
"/i18n/es.js",
|
|
23
38
|
];
|
|
24
39
|
|
|
25
40
|
/** URL path → file inside node_modules. */
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web UI's language (R19, 0.3.0-dev.39): a user pref, stored as
|
|
3
|
+
* `language:` in config/local/user.yml and served by /api/settings, like
|
|
4
|
+
* `editor`. Absent means English.
|
|
5
|
+
*
|
|
6
|
+
* The catalogs are public/shell.js's MESSAGES (en, the fallback) and one
|
|
7
|
+
* classic script per other language, public/i18n/<lang>.js, which adds its
|
|
8
|
+
* table to window.denMessages before shell.js runs. A page served in a
|
|
9
|
+
* language other than English gets that script and `<html lang>` from
|
|
10
|
+
* localizePage, so the first paint is already in that language. An English
|
|
11
|
+
* page is served exactly as it is on disk.
|
|
12
|
+
*
|
|
13
|
+
* Only Den's own words are translated: names (services, workspaces, repos,
|
|
14
|
+
* themes, languages), logs and command output never are.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Every language the UI ships a catalog for; the first is the fallback. */
|
|
18
|
+
export const UI_LANGUAGES = ["en", "ja", "es"] as const;
|
|
19
|
+
export type UiLanguage = (typeof UI_LANGUAGES)[number];
|
|
20
|
+
export const DEFAULT_UI_LANGUAGE: UiLanguage = "en";
|
|
21
|
+
|
|
22
|
+
export function isUiLanguage(v: unknown): v is UiLanguage {
|
|
23
|
+
return (
|
|
24
|
+
typeof v === "string" && (UI_LANGUAGES as readonly string[]).includes(v)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The stored value, normalised: an unknown or missing one is English. */
|
|
29
|
+
export function uiLanguage(stored: unknown): UiLanguage {
|
|
30
|
+
return isUiLanguage(stored) ? stored : DEFAULT_UI_LANGUAGE;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The catalog script a language's pages load before shell.js (none for en). */
|
|
34
|
+
export function catalogPath(lang: UiLanguage): string | null {
|
|
35
|
+
return lang === DEFAULT_UI_LANGUAGE ? null : `/i18n/${lang}.js`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A page in `lang`: `<html lang>` set, and the language's catalog loaded
|
|
40
|
+
* just before shell.js, so the shell draws in it from the first paint.
|
|
41
|
+
* English, or a page without the shell, comes back unchanged.
|
|
42
|
+
*/
|
|
43
|
+
export function localizePage(html: string, lang: UiLanguage): string {
|
|
44
|
+
const src = catalogPath(lang);
|
|
45
|
+
const shell = '<script src="/shell.js"></script>';
|
|
46
|
+
if (!src || !html.includes(shell)) return html;
|
|
47
|
+
return html
|
|
48
|
+
.replace(/<html lang="[^"]*"/, () => `<html lang="${lang}"`)
|
|
49
|
+
.replace(shell, () => `<script src="${src}"></script>${shell}`);
|
|
50
|
+
}
|
|
@@ -19,6 +19,7 @@ import type { ResolvedService } from "../../shared/types/index.js";
|
|
|
19
19
|
import type { EditorManager } from "./editor-manager.js";
|
|
20
20
|
import type { GitHubService } from "./github-service.js";
|
|
21
21
|
import { JETBRAINS_EDITORS } from "./editor-manager.js";
|
|
22
|
+
import { stackNamespaceRows } from "../../shared/stacks/namespace.js";
|
|
22
23
|
import {
|
|
23
24
|
readWorkspacesFile,
|
|
24
25
|
writeWorkspacesFile,
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
import {
|
|
45
46
|
checkServiceHealth,
|
|
46
47
|
claimHealthyService,
|
|
48
|
+
settledDaemonRecord,
|
|
47
49
|
runServiceCommand,
|
|
48
50
|
prepareServiceSecretEnv,
|
|
49
51
|
resolveService,
|
|
@@ -86,10 +88,12 @@ import { depService } from "../../shared/manifest/deps.js";
|
|
|
86
88
|
import { waitForReady } from "../../shared/environment-manager/readiness.js";
|
|
87
89
|
import {
|
|
88
90
|
readCatalog,
|
|
89
|
-
|
|
91
|
+
needsGhRepair,
|
|
92
|
+
resolveRepoCloneSource,
|
|
90
93
|
upsertCatalogEntry,
|
|
91
94
|
} from "../../shared/environment-manager/catalog.js";
|
|
92
95
|
import { getReposRoot, readUserConfig } from "../../shared/den-config.js";
|
|
96
|
+
import { redactRemote } from "../../shared/environment-manager/git-remote.js";
|
|
93
97
|
import { getDistRoot } from "../../shared/paths.js";
|
|
94
98
|
import { recordRemovedFromWorkspace } from "../../shared/environment-manager/default-workspace.js";
|
|
95
99
|
import { getSudoEnv, prewarmSudo } from "./sudo-store.js";
|
|
@@ -320,7 +324,7 @@ async function buildAvailableRepos(wsOverride: string | null) {
|
|
|
320
324
|
hasManifest: false,
|
|
321
325
|
alreadyAdded,
|
|
322
326
|
source: "catalog",
|
|
323
|
-
gh: entry.gh,
|
|
327
|
+
gh: entry.gh ?? null,
|
|
324
328
|
});
|
|
325
329
|
}
|
|
326
330
|
|
|
@@ -550,7 +554,21 @@ export async function handleWorkspaceRoutes(
|
|
|
550
554
|
syncJetBrainsWorkspaceSymlinks(wsFile);
|
|
551
555
|
}
|
|
552
556
|
|
|
553
|
-
|
|
557
|
+
// Each workspace's stack namespace, resolved, with where it came from
|
|
558
|
+
// (workspace, blueprint or default) and whether it differs from the
|
|
559
|
+
// default suffix: the page's "stacks: <ns>" chip.
|
|
560
|
+
const stackNamespaces = Object.fromEntries(
|
|
561
|
+
stackNamespaceRows(Object.keys(wsFile.workspaces)).map((r) => [
|
|
562
|
+
r.workspace,
|
|
563
|
+
r,
|
|
564
|
+
]),
|
|
565
|
+
);
|
|
566
|
+
return sendJson(res, {
|
|
567
|
+
...wsFile,
|
|
568
|
+
resolvedDirs,
|
|
569
|
+
codeWorkspacePaths,
|
|
570
|
+
stackNamespaces,
|
|
571
|
+
});
|
|
554
572
|
}
|
|
555
573
|
|
|
556
574
|
// ── API: rename workspace ─────────────────────────────────────────────────
|
|
@@ -1773,10 +1791,31 @@ export async function handleWorkspaceRoutes(
|
|
|
1773
1791
|
if (cmd === "up") {
|
|
1774
1792
|
const health = await checkServiceHealth(resolved, "local");
|
|
1775
1793
|
if (cancelled) return;
|
|
1776
|
-
|
|
1794
|
+
// `conflict`: a shared stack namespace runs another checkout's
|
|
1795
|
+
// project; the real up refuses it, naming both.
|
|
1796
|
+
const claim =
|
|
1797
|
+
health.health === "ok"
|
|
1798
|
+
? claimHealthyService(resolved, ws.name, (text) =>
|
|
1799
|
+
emit({ type: "line", text }),
|
|
1800
|
+
)
|
|
1801
|
+
: "untracked";
|
|
1802
|
+
// Another process answers on its port (said above): nothing is
|
|
1803
|
+
// claimed and nothing started on top of it (0.3.0-dev.39).
|
|
1804
|
+
if (health.health === "ok" && claim === "busy") {
|
|
1805
|
+
overallExit = 1;
|
|
1806
|
+
emit({
|
|
1807
|
+
type: "svc-done",
|
|
1808
|
+
address,
|
|
1809
|
+
exitCode: 1,
|
|
1810
|
+
repoName: svcRepoName,
|
|
1811
|
+
subsystem: svcSubsystem,
|
|
1812
|
+
});
|
|
1813
|
+
safeRunNext(i + 1);
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (health.health === "ok" && claim !== "conflict") {
|
|
1777
1817
|
// Skipping must not lose the refcount — this workspace is an
|
|
1778
1818
|
// owner of what's running, whether den tracked it or not.
|
|
1779
|
-
const claim = claimHealthyService(resolved, ws.name);
|
|
1780
1819
|
emit({
|
|
1781
1820
|
type: "line",
|
|
1782
1821
|
text:
|
|
@@ -1799,6 +1838,11 @@ export async function handleWorkspaceRoutes(
|
|
|
1799
1838
|
return;
|
|
1800
1839
|
}
|
|
1801
1840
|
} else {
|
|
1841
|
+
// A pre-dev.37 record is settled before the skip decides
|
|
1842
|
+
// (0.3.0-dev.39): adopted, it is running; stale, it is dropped.
|
|
1843
|
+
settledDaemonRecord(resolved, ws.name, (text) =>
|
|
1844
|
+
emit({ type: "line", text }),
|
|
1845
|
+
);
|
|
1802
1846
|
let running = hasRunningOpctlContainers(resolved);
|
|
1803
1847
|
if (!running) {
|
|
1804
1848
|
// Not opctl-visible but still responding (host-run process)?
|
|
@@ -2159,16 +2203,18 @@ export async function handleWorkspaceRoutes(
|
|
|
2159
2203
|
// flow writes one), which used to read as "not in the catalog" and
|
|
2160
2204
|
// skip the clone entirely. Derive the conventional slug instead, and
|
|
2161
2205
|
// persist it so every other flow (CLI up/setup) stops hitting this.
|
|
2162
|
-
|
|
2206
|
+
// ORG-1d: a repo with no source and a `remote:` clones from it.
|
|
2207
|
+
const ghSlug = resolveRepoCloneSource(svc.repoName, readCatalog(), () =>
|
|
2163
2208
|
githubOrgFor(ws.name),
|
|
2164
2209
|
);
|
|
2165
|
-
|
|
2166
|
-
|
|
2210
|
+
const entry = readCatalog()[svc.repoName];
|
|
2211
|
+
if (needsGhRepair(entry, ghSlug)) {
|
|
2212
|
+
upsertCatalogEntry(svc.repoName, { ...entry, gh: ghSlug });
|
|
2167
2213
|
}
|
|
2168
2214
|
const branch = (svc as { branch?: string | null }).branch ?? "main";
|
|
2169
2215
|
emit({
|
|
2170
2216
|
type: "line",
|
|
2171
|
-
text: ` ${svc.repoName}: cloning ${ghSlug} (${branch})…`,
|
|
2217
|
+
text: ` ${svc.repoName}: cloning ${redactRemote(ghSlug)} (${branch})…`,
|
|
2172
2218
|
});
|
|
2173
2219
|
try {
|
|
2174
2220
|
// Synchronous (same helper the workspace-add flow uses) — the server
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/Monster-Orange/den/main/schemas/den.theme.v1.json",
|
|
3
|
+
"schema": "den.theme/v1",
|
|
4
|
+
"id": "den",
|
|
5
|
+
"name": "Den",
|
|
6
|
+
"version": "2.0.0",
|
|
7
|
+
"description": "Den's default: a sheltered workspace in walnut and umber neutrals with a moss-green accent. System fonts.",
|
|
8
|
+
"author": "Monster Orange",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"homepage": "https://github.com/Monster-Orange/den-themes/tree/main/themes/den",
|
|
11
|
+
"default_mode": "dark",
|
|
12
|
+
"type": {
|
|
13
|
+
"sans": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica, Arial, sans-serif",
|
|
14
|
+
"mono": "ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace",
|
|
15
|
+
"display": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica, Arial, sans-serif",
|
|
16
|
+
"ja": "\"Hiragino Sans\", \"Hiragino Kaku Gothic ProN\", \"Noto Sans JP\", \"Yu Gothic\", Meiryo, sans-serif"
|
|
17
|
+
},
|
|
18
|
+
"shape": {
|
|
19
|
+
"radius": "8px",
|
|
20
|
+
"radius-button": "6px",
|
|
21
|
+
"density": "comfortable"
|
|
22
|
+
},
|
|
23
|
+
"mark": {
|
|
24
|
+
"svg": "mark.svg",
|
|
25
|
+
"shape": "rounded"
|
|
26
|
+
},
|
|
27
|
+
"favicon": "favicon.ico",
|
|
28
|
+
"modes": {
|
|
29
|
+
"light": {
|
|
30
|
+
"bg": "#f2eee8",
|
|
31
|
+
"surface": "#fbf9f6",
|
|
32
|
+
"raised": "#ffffff",
|
|
33
|
+
"sunken": "#e8e2d9",
|
|
34
|
+
"text": "#241e18",
|
|
35
|
+
"muted": "#5c5247",
|
|
36
|
+
"faint": "#6d6358",
|
|
37
|
+
"border": "#dcd3c7",
|
|
38
|
+
"border-strong": "#8d7f6d",
|
|
39
|
+
"accent": "#557a2a",
|
|
40
|
+
"accent-text": "#4a6b23",
|
|
41
|
+
"accent-fill": "#4a6b23",
|
|
42
|
+
"on-accent": "#ffffff",
|
|
43
|
+
"ok": "#2e7246",
|
|
44
|
+
"warn": "#8c5a10",
|
|
45
|
+
"bad": "#b0392a",
|
|
46
|
+
"remote": "#23677a",
|
|
47
|
+
"shadow": "0 8px 24px rgb(48 36 24 / 0.14)",
|
|
48
|
+
"bar": "linear-gradient(90deg, #557a2a 0%, #8a6a48 100%)"
|
|
49
|
+
},
|
|
50
|
+
"dark": {
|
|
51
|
+
"bg": "#1a1612",
|
|
52
|
+
"surface": "#221d18",
|
|
53
|
+
"raised": "#2b251f",
|
|
54
|
+
"sunken": "#14110e",
|
|
55
|
+
"text": "#ede6dc",
|
|
56
|
+
"muted": "#b5aa9b",
|
|
57
|
+
"faint": "#978b7e",
|
|
58
|
+
"border": "#3a322a",
|
|
59
|
+
"border-strong": "#7a6d5d",
|
|
60
|
+
"accent": "#9bbb63",
|
|
61
|
+
"accent-text": "#a8c672",
|
|
62
|
+
"accent-fill": "#9bbb63",
|
|
63
|
+
"on-accent": "#1a1612",
|
|
64
|
+
"ok": "#6fc28f",
|
|
65
|
+
"warn": "#e0a84e",
|
|
66
|
+
"bad": "#ef7f6a",
|
|
67
|
+
"remote": "#74b8c8",
|
|
68
|
+
"shadow": "0 12px 30px rgb(0 0 0 / 0.5)",
|
|
69
|
+
"bar": "linear-gradient(90deg, #9bbb63 0%, #a07c55 100%)"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
Binary file
|