@monster-orange/den 0.2.8 → 0.4.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 +1731 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +730 -6
- 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 +12 -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/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +173 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +192 -0
- package/src/shared/agents/runtimes.ts +47 -0
- 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 +257 -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/engine.ts +23 -0
- 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/environment-manager/workspaces.ts +19 -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/secret-env.ts +70 -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/sessions/client.ts +277 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +79 -0
- package/src/shared/sessions/host.ts +667 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- 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/agents-command.ts +812 -0
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +260 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +18 -5
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +580 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/sessions-command.ts +718 -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 +145 -20
- package/src/ui/mcp/server.ts +27 -2
- package/src/ui/mcp/tools.ts +94 -12
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +35 -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 +539 -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 +37 -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 +48 -18
- 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,
|
|
@@ -40,10 +41,12 @@ import {
|
|
|
40
41
|
syncJetBrainsWorkspaceSymlinks,
|
|
41
42
|
DEN_SERVICES_DIR,
|
|
42
43
|
DEN_WORKSPACES_DIR,
|
|
44
|
+
resolveWorkspaceDir,
|
|
43
45
|
} from "../../shared/environment-manager/workspaces.js";
|
|
44
46
|
import {
|
|
45
47
|
checkServiceHealth,
|
|
46
48
|
claimHealthyService,
|
|
49
|
+
settledDaemonRecord,
|
|
47
50
|
runServiceCommand,
|
|
48
51
|
prepareServiceSecretEnv,
|
|
49
52
|
resolveService,
|
|
@@ -515,21 +518,8 @@ export async function handleWorkspaceRoutes(
|
|
|
515
518
|
const wsFile = readWorkspacesFile();
|
|
516
519
|
const reposRoot = getReposRoot();
|
|
517
520
|
const resolvedDirs: Record<string, string> = {};
|
|
518
|
-
for (const [name, ws] of Object.entries(wsFile.workspaces))
|
|
519
|
-
|
|
520
|
-
resolvedDirs[name] = path.resolve(ROOT, ws.directory);
|
|
521
|
-
} else {
|
|
522
|
-
const gwDir = path.join(DEN_WORKSPACES_DIR, name);
|
|
523
|
-
const legacyDir = path.join(reposRoot, name);
|
|
524
|
-
if (fs.existsSync(gwDir)) {
|
|
525
|
-
resolvedDirs[name] = gwDir;
|
|
526
|
-
} else if (fs.existsSync(legacyDir)) {
|
|
527
|
-
resolvedDirs[name] = legacyDir;
|
|
528
|
-
} else {
|
|
529
|
-
resolvedDirs[name] = reposRoot;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
521
|
+
for (const [name, ws] of Object.entries(wsFile.workspaces))
|
|
522
|
+
resolvedDirs[name] = resolveWorkspaceDir(name, ws, reposRoot);
|
|
533
523
|
const editor = readUserConfig().editor ?? "";
|
|
534
524
|
const catalog = readCatalog();
|
|
535
525
|
const codeWorkspacePaths: Record<string, string> = {};
|
|
@@ -552,7 +542,21 @@ export async function handleWorkspaceRoutes(
|
|
|
552
542
|
syncJetBrainsWorkspaceSymlinks(wsFile);
|
|
553
543
|
}
|
|
554
544
|
|
|
555
|
-
|
|
545
|
+
// Each workspace's stack namespace, resolved, with where it came from
|
|
546
|
+
// (workspace, blueprint or default) and whether it differs from the
|
|
547
|
+
// default suffix: the page's "stacks: <ns>" chip.
|
|
548
|
+
const stackNamespaces = Object.fromEntries(
|
|
549
|
+
stackNamespaceRows(Object.keys(wsFile.workspaces)).map((r) => [
|
|
550
|
+
r.workspace,
|
|
551
|
+
r,
|
|
552
|
+
]),
|
|
553
|
+
);
|
|
554
|
+
return sendJson(res, {
|
|
555
|
+
...wsFile,
|
|
556
|
+
resolvedDirs,
|
|
557
|
+
codeWorkspacePaths,
|
|
558
|
+
stackNamespaces,
|
|
559
|
+
});
|
|
556
560
|
}
|
|
557
561
|
|
|
558
562
|
// ── API: rename workspace ─────────────────────────────────────────────────
|
|
@@ -1775,10 +1779,31 @@ export async function handleWorkspaceRoutes(
|
|
|
1775
1779
|
if (cmd === "up") {
|
|
1776
1780
|
const health = await checkServiceHealth(resolved, "local");
|
|
1777
1781
|
if (cancelled) return;
|
|
1778
|
-
|
|
1782
|
+
// `conflict`: a shared stack namespace runs another checkout's
|
|
1783
|
+
// project; the real up refuses it, naming both.
|
|
1784
|
+
const claim =
|
|
1785
|
+
health.health === "ok"
|
|
1786
|
+
? claimHealthyService(resolved, ws.name, (text) =>
|
|
1787
|
+
emit({ type: "line", text }),
|
|
1788
|
+
)
|
|
1789
|
+
: "untracked";
|
|
1790
|
+
// Another process answers on its port (said above): nothing is
|
|
1791
|
+
// claimed and nothing started on top of it (0.3.0-dev.39).
|
|
1792
|
+
if (health.health === "ok" && claim === "busy") {
|
|
1793
|
+
overallExit = 1;
|
|
1794
|
+
emit({
|
|
1795
|
+
type: "svc-done",
|
|
1796
|
+
address,
|
|
1797
|
+
exitCode: 1,
|
|
1798
|
+
repoName: svcRepoName,
|
|
1799
|
+
subsystem: svcSubsystem,
|
|
1800
|
+
});
|
|
1801
|
+
safeRunNext(i + 1);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (health.health === "ok" && claim !== "conflict") {
|
|
1779
1805
|
// Skipping must not lose the refcount — this workspace is an
|
|
1780
1806
|
// owner of what's running, whether den tracked it or not.
|
|
1781
|
-
const claim = claimHealthyService(resolved, ws.name);
|
|
1782
1807
|
emit({
|
|
1783
1808
|
type: "line",
|
|
1784
1809
|
text:
|
|
@@ -1801,6 +1826,11 @@ export async function handleWorkspaceRoutes(
|
|
|
1801
1826
|
return;
|
|
1802
1827
|
}
|
|
1803
1828
|
} else {
|
|
1829
|
+
// A pre-dev.37 record is settled before the skip decides
|
|
1830
|
+
// (0.3.0-dev.39): adopted, it is running; stale, it is dropped.
|
|
1831
|
+
settledDaemonRecord(resolved, ws.name, (text) =>
|
|
1832
|
+
emit({ type: "line", text }),
|
|
1833
|
+
);
|
|
1804
1834
|
let running = hasRunningOpctlContainers(resolved);
|
|
1805
1835
|
if (!running) {
|
|
1806
1836
|
// Not opctl-visible but still responding (host-run process)?
|
|
@@ -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
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="Den">
|
|
2
|
+
<rect width="32" height="32" rx="7" fill="#3a2e22"/>
|
|
3
|
+
<path d="M5 26.5h22" stroke="#9bbb63" stroke-width="2" stroke-linecap="round"/>
|
|
4
|
+
<path d="M8 26V15a8 8 0 0 1 16 0v11h-4.5V15.5a3.5 3.5 0 0 0-7 0V26z" fill="#f2eee8"/>
|
|
5
|
+
<path d="M13 26v-9.5a3 3 0 0 1 6 0V26z" fill="#c8a46a" opacity="0.9"/>
|
|
6
|
+
</svg>
|