@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +1547 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +507 -4
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +9 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/auth/headless-login.ts +1 -1
  22. package/src/shared/auth/sudo-vault.ts +93 -31
  23. package/src/shared/auth/token-providers/index.ts +7 -3
  24. package/src/shared/brand.ts +7 -0
  25. package/src/shared/config-error.ts +33 -3
  26. package/src/shared/constants.ts +3 -24
  27. package/src/shared/den-config.ts +141 -5
  28. package/src/shared/edge/config.ts +147 -21
  29. package/src/shared/edge/exec.ts +31 -0
  30. package/src/shared/edge/included.ts +169 -0
  31. package/src/shared/edge/render.ts +69 -14
  32. package/src/shared/edge/runtime.ts +128 -121
  33. package/src/shared/edge/status.ts +68 -8
  34. package/src/shared/edge/tls.ts +328 -0
  35. package/src/shared/edge/types.ts +25 -9
  36. package/src/shared/environment-manager/default-workspace.ts +16 -1
  37. package/src/shared/environment-manager/health.ts +73 -38
  38. package/src/shared/environment-manager/scope.ts +168 -0
  39. package/src/shared/environment-manager/service-runner.ts +789 -43
  40. package/src/shared/environment-manager/up-targets.ts +132 -0
  41. package/src/shared/health/rollup.ts +48 -0
  42. package/src/shared/open-url.ts +21 -2
  43. package/src/shared/org-sources.ts +78 -59
  44. package/src/shared/org.ts +53 -5
  45. package/src/shared/plugins/manifest.ts +406 -0
  46. package/src/shared/plugins/store.ts +358 -0
  47. package/src/shared/process/legacy-adopt.ts +285 -0
  48. package/src/shared/process/process-group.ts +202 -0
  49. package/src/shared/process/spawn-streaming.ts +7 -0
  50. package/src/shared/secrets/doppler.ts +1 -1
  51. package/src/shared/secrets/index.ts +279 -50
  52. package/src/shared/secrets/none.ts +56 -0
  53. package/src/shared/secrets/strategies.ts +202 -0
  54. package/src/shared/secrets/types.ts +1 -1
  55. package/src/shared/smoke/git-remote.ts +40 -0
  56. package/src/shared/stacks/manifest-commands.ts +1 -1
  57. package/src/shared/stacks/namespace.ts +178 -0
  58. package/src/shared/stacks/owners.ts +303 -0
  59. package/src/shared/stacks/render.ts +8 -3
  60. package/src/shared/stacks/runtime.ts +100 -16
  61. package/src/shared/stacks/status.ts +63 -12
  62. package/src/shared/strategies/registry.ts +89 -0
  63. package/src/shared/theme/contract.ts +538 -0
  64. package/src/shared/types/index.ts +12 -3
  65. package/src/ui/cli/create.ts +24 -7
  66. package/src/ui/cli/doctor-command.ts +131 -0
  67. package/src/ui/cli/edge-command.ts +56 -10
  68. package/src/ui/cli/flag-parser.ts +7 -2
  69. package/src/ui/cli/health-display.ts +25 -0
  70. package/src/ui/cli/index.ts +548 -60
  71. package/src/ui/cli/notice.ts +20 -0
  72. package/src/ui/cli/plugin-command.ts +849 -0
  73. package/src/ui/cli/stack-command.ts +63 -2
  74. package/src/ui/cli/test-runner.ts +11 -9
  75. package/src/ui/cli/theme-command.ts +136 -0
  76. package/src/ui/cli/web-command.ts +47 -23
  77. package/src/ui/mcp/api.ts +28 -2
  78. package/src/ui/mcp/scope.ts +22 -1
  79. package/src/ui/mcp/tools.ts +50 -7
  80. package/src/ui/server/appearance-prefs.ts +95 -0
  81. package/src/ui/server/daemon-reaper.ts +29 -1
  82. package/src/ui/server/engine-stamp.ts +83 -0
  83. package/src/ui/server/github-service.ts +23 -44
  84. package/src/ui/server/graph-page.ts +20 -0
  85. package/src/ui/server/graph-prefs.ts +44 -1
  86. package/src/ui/server/http-errors.ts +293 -0
  87. package/src/ui/server/http-helpers.ts +22 -3
  88. package/src/ui/server/index.ts +532 -51
  89. package/src/ui/server/public/appearance.js +531 -0
  90. package/src/ui/server/public/edge-page.js +248 -0
  91. package/src/ui/server/public/edge.html +251 -0
  92. package/src/ui/server/public/environments.html +65 -83
  93. package/src/ui/server/public/error-page.js +247 -0
  94. package/src/ui/server/public/error.html +260 -0
  95. package/src/ui/server/public/graph-model.js +332 -14
  96. package/src/ui/server/public/graph.css +141 -47
  97. package/src/ui/server/public/graph.js +223 -44
  98. package/src/ui/server/public/header.js +396 -600
  99. package/src/ui/server/public/health-fit.js +248 -0
  100. package/src/ui/server/public/health-rollup.js +128 -0
  101. package/src/ui/server/public/i18n/es.js +287 -0
  102. package/src/ui/server/public/i18n/ja.js +276 -0
  103. package/src/ui/server/public/index.html +139 -438
  104. package/src/ui/server/public/local-dev.html +3468 -4044
  105. package/src/ui/server/public/logs.html +71 -208
  106. package/src/ui/server/public/panel.js +14 -2
  107. package/src/ui/server/public/release.html +175 -389
  108. package/src/ui/server/public/services.html +596 -770
  109. package/src/ui/server/public/settings-page.js +759 -0
  110. package/src/ui/server/public/settings-registry.js +521 -0
  111. package/src/ui/server/public/settings.html +590 -0
  112. package/src/ui/server/public/shell.js +3869 -0
  113. package/src/ui/server/public/topbar.js +1413 -0
  114. package/src/ui/server/public/version-label.js +12 -0
  115. package/src/ui/server/public/workspace-picker.js +74 -34
  116. package/src/ui/server/readonly-routes.ts +12 -1
  117. package/src/ui/server/services-list.ts +94 -17
  118. package/src/ui/server/settings-distribution.ts +74 -0
  119. package/src/ui/server/shell-prefs.ts +192 -0
  120. package/src/ui/server/theme-routes.ts +292 -0
  121. package/src/ui/server/theme-runtime.ts +407 -0
  122. package/src/ui/server/ui-assets.ts +16 -1
  123. package/src/ui/server/ui-language.ts +50 -0
  124. package/src/ui/server/workspace-routes.ts +45 -3
  125. package/themes/den/den-theme.json +72 -0
  126. package/themes/den/favicon.ico +0 -0
  127. package/themes/den/mark.svg +6 -0
@@ -0,0 +1,406 @@
1
+ /**
2
+ * The plugin contract, den.plugin/v1 (PL-1a): a `den-plugin.json` naming what
3
+ * a plugin provides (one entry per extension point) and the capabilities it
4
+ * needs. See schemas/den.plugin.v1.json.
5
+ *
6
+ * A directory that holds only a `den-theme.json` is an implicit theme plugin,
7
+ * `{provides: [{kind: "theme", manifest: "den-theme.json"}], capabilities: []}`,
8
+ * so every published and local theme keeps working unchanged.
9
+ *
10
+ * validatePluginDir checks the schema, each provided kind with its own
11
+ * validator (a theme with the den.theme/v1 validator, a page's entry module
12
+ * on disk) and admission rules 1-3:
13
+ * 1. it names a maintainer (the schema requires one);
14
+ * 2. a README says its tier, what it may touch and what it must never do
15
+ * (files-tier plugins, themes among them, are exempt);
16
+ * 3. an SPDX license (the rule den.theme/v1 has), and every bundled font
17
+ * licensed (the theme validator's rule).
18
+ */
19
+ import * as fs from "fs";
20
+ import * as path from "path";
21
+ import Ajv2020 from "ajv/dist/2020.js";
22
+ import addFormats from "ajv-formats";
23
+ import { getEngineRoot } from "../paths.js";
24
+ import {
25
+ denRangeIssue,
26
+ THEME_FILE,
27
+ schemaIssues,
28
+ validateThemeDir,
29
+ type ContrastFailure,
30
+ type ThemeManifest,
31
+ } from "../theme/contract.js";
32
+
33
+ export const PLUGIN_SCHEMA_ID = "den.plugin/v1";
34
+ export const PLUGIN_FILE = "den-plugin.json";
35
+ /** A plugin id, and so its directory's name. */
36
+ export const PLUGIN_ID = /^[a-z][a-z0-9-]{1,39}$/;
37
+ /** What list and validate say about a plugins/<name>/ with no manifest. */
38
+ export const PLUGIN_NOT_A_PLUGIN = `no ${PLUGIN_FILE} or ${THEME_FILE}, so it isn't a plugin`;
39
+ /** PLUGIN_ID in words, for messages that refuse a name. */
40
+ export const PLUGIN_ID_RULE =
41
+ "2–40 characters: lowercase letters, digits or dashes, starting with a letter";
42
+ /** Den's own id: its built-in theme. A distribution plugin may not use it. */
43
+ export const BUILTIN_ID = "den";
44
+
45
+ export type PluginTier = "files" | "browser" | "server";
46
+
47
+ /** Every kind, by tier (PL-SPEC §4). */
48
+ export const KIND_TIERS: Readonly<Record<string, PluginTier>> = {
49
+ theme: "files",
50
+ catalog: "files",
51
+ "persona-pack": "files",
52
+ page: "browser",
53
+ "mcp-tools": "server",
54
+ "work-items": "server",
55
+ runtime: "server",
56
+ "session-backend": "server",
57
+ notifier: "server",
58
+ secrets: "server",
59
+ identity: "server",
60
+ "health-source": "server",
61
+ };
62
+
63
+ export interface PluginProvide {
64
+ kind: string;
65
+ /** kind theme: the den-theme.json, relative to the plugin. */
66
+ manifest?: string;
67
+ /** kind page: the ES module, relative to the plugin. */
68
+ entry?: string;
69
+ nav?: { group: string; label: string; icon?: string };
70
+ [field: string]: unknown;
71
+ }
72
+
73
+ export interface PluginManifest {
74
+ schema: typeof PLUGIN_SCHEMA_ID;
75
+ id: string;
76
+ name: string;
77
+ version: string;
78
+ license: string;
79
+ den: string;
80
+ maintainer: string;
81
+ description?: string;
82
+ homepage?: string;
83
+ readme?: string;
84
+ provides: PluginProvide[];
85
+ capabilities?: string[];
86
+ }
87
+
88
+ /** A plugin as Den sees it, whichever manifest it came from. */
89
+ export interface PluginInfo {
90
+ id: string;
91
+ name: string;
92
+ version: string;
93
+ /** Derived from provides, in order, once each (the lock's `kinds`). */
94
+ kinds: string[];
95
+ /** Declared capabilities; for a local plugin, its approved set. */
96
+ capabilities: string[];
97
+ tier: PluginTier;
98
+ /** The file it was read from: den-plugin.json or den-theme.json. */
99
+ manifestFile: string;
100
+ /** True for a directory holding only a den-theme.json. */
101
+ implicit: boolean;
102
+ /** The parsed den-plugin.json (absent for an implicit theme plugin). */
103
+ manifest?: PluginManifest;
104
+ /** The theme it provides, when it validated. */
105
+ theme?: ThemeManifest;
106
+ }
107
+
108
+ export interface PluginIssue {
109
+ /** Where: a file in the plugin, with a JSON pointer ("den-plugin.json#/id"). */
110
+ at: string;
111
+ message: string;
112
+ }
113
+
114
+ export interface PluginValidation {
115
+ ok: boolean;
116
+ /** The manifest it was read from, or null when the dir has neither. */
117
+ manifestFile: string | null;
118
+ plugin?: PluginInfo;
119
+ issues: PluginIssue[];
120
+ contrast: ContrastFailure[];
121
+ }
122
+
123
+ /**
124
+ * Capabilities that act, rather than only read or add a nav entry. A local
125
+ * plugin declaring one gets the git warning (see plugin validate).
126
+ */
127
+ export function actingCapabilities(capabilities: string[]): string[] {
128
+ return capabilities.filter((c) =>
129
+ /^(api\.actions|secrets|exec|net|send):/.test(c),
130
+ );
131
+ }
132
+
133
+ export function tierOf(kinds: string[]): PluginTier {
134
+ const tiers = kinds.map((k) => KIND_TIERS[k] ?? "server");
135
+ return tiers.includes("server")
136
+ ? "server"
137
+ : tiers.includes("browser")
138
+ ? "browser"
139
+ : "files";
140
+ }
141
+
142
+ let validator: ReturnType<InstanceType<typeof Ajv2020>["compile"]> | null =
143
+ null;
144
+
145
+ function schemaValidator() {
146
+ if (validator) return validator;
147
+ const read = (f: string) =>
148
+ JSON.parse(
149
+ fs.readFileSync(path.join(getEngineRoot(), "schemas", f), "utf-8"),
150
+ ) as object;
151
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
152
+ addFormats(ajv);
153
+ // den.plugin/v1 takes its id, version and SPDX license rules from
154
+ // den.theme/v1 by $ref, so the two never diverge.
155
+ ajv.addSchema(read("den.theme.v1.json"));
156
+ validator = ajv.compile(read("den.plugin.v1.json"));
157
+ return validator;
158
+ }
159
+
160
+ const CAPABILITY_MESSAGE =
161
+ "unknown capability: the vocabulary is ui.nav, api.read, api.actions:<id,...>, secrets:<ref,...>, net:<host,...>, exec:<binary,...>, send:<channel>";
162
+
163
+ function pluginMessage(e: {
164
+ instancePath: string;
165
+ keyword: string;
166
+ }): string | null {
167
+ if (/^\/capabilities\/\d+$/.test(e.instancePath) && e.keyword === "pattern")
168
+ return CAPABILITY_MESSAGE;
169
+ return null;
170
+ }
171
+
172
+ /**
173
+ * Files in `dir` that are symlinks, relative. A plugin is vendored and
174
+ * served by path, so a link could reach outside it.
175
+ */
176
+ function symlinksIn(dir: string): string[] {
177
+ const out: string[] = [];
178
+ const walk = (rel: string): void => {
179
+ for (const e of fs.readdirSync(path.join(dir, rel), {
180
+ withFileTypes: true,
181
+ })) {
182
+ if (e.name.startsWith(".")) continue;
183
+ const r = rel ? `${rel}/${e.name}` : e.name;
184
+ if (e.isSymbolicLink()) out.push(r);
185
+ else if (e.isDirectory()) walk(r);
186
+ }
187
+ };
188
+ walk("");
189
+ return out;
190
+ }
191
+
192
+ /** `rel` inside `root` as an absolute path, or null when it escapes. */
193
+ function inside(root: string, rel: string): string | null {
194
+ const abs = path.resolve(root, rel);
195
+ return abs.startsWith(path.resolve(root) + path.sep) ? abs : null;
196
+ }
197
+
198
+ /**
199
+ * Admission rule 2: a browser- or server-tier plugin has a README that says
200
+ * its tier, what it may touch and what it must never do. Den can check the
201
+ * file is there, names the tier and says "never"; the words are for review.
202
+ */
203
+ function readmeIssues(
204
+ dir: string,
205
+ m: PluginManifest,
206
+ tier: PluginTier,
207
+ ): PluginIssue[] {
208
+ if (tier === "files") return [];
209
+ const rel = m.readme ?? "README.md";
210
+ const abs = inside(dir, rel);
211
+ if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isFile())
212
+ return [
213
+ {
214
+ at: rel,
215
+ message: `missing: a ${tier}-tier plugin needs a README that says its tier, what it may touch (its capabilities, in words) and what it must never do`,
216
+ },
217
+ ];
218
+ const text = fs.readFileSync(abs, "utf-8");
219
+ const missing = [
220
+ new RegExp(`\\b${tier}\\b`, "i").test(text) ? null : `its tier (${tier})`,
221
+ /\bnever\b/i.test(text) ? null : "what it must never do",
222
+ ].filter((x): x is string => !!x);
223
+ return missing.length
224
+ ? [{ at: rel, message: `does not say ${missing.join(" or ")}` }]
225
+ : [];
226
+ }
227
+
228
+ /**
229
+ * Validate the plugin in `dir`: den-plugin.json, or an implicit theme plugin
230
+ * (den-theme.json only). With `expectId`, the manifest's id must match its
231
+ * directory. A directory with neither manifest is an error, never skipped
232
+ * (PL-1a): an id can't collide unseen.
233
+ */
234
+ export function validatePluginDir(
235
+ dir: string,
236
+ opts: { expectId?: string } = {},
237
+ ): PluginValidation {
238
+ const pluginFile = path.join(dir, PLUGIN_FILE);
239
+ if (!fs.existsSync(pluginFile)) {
240
+ if (!fs.existsSync(path.join(dir, THEME_FILE)))
241
+ return {
242
+ ok: false,
243
+ manifestFile: null,
244
+ issues: [
245
+ {
246
+ at: "",
247
+ message: PLUGIN_NOT_A_PLUGIN,
248
+ },
249
+ ],
250
+ contrast: [],
251
+ };
252
+ // An implicit theme plugin: the theme validator is the whole check, plus
253
+ // rule 1: a den-theme.json's `author` is its maintainer (PL-SPEC d4).
254
+ const v = validateThemeDir(dir, opts);
255
+ const issues = [...v.issues];
256
+ if (v.theme && !v.theme.author?.trim())
257
+ issues.push({
258
+ at: "/author",
259
+ message:
260
+ "no author: a theme's author is its maintainer, and a plugin needs one",
261
+ });
262
+ return {
263
+ ok: v.ok && issues.length === 0,
264
+ manifestFile: THEME_FILE,
265
+ issues,
266
+ contrast: v.contrast,
267
+ ...(v.theme
268
+ ? {
269
+ plugin: {
270
+ id: v.theme.id,
271
+ name: v.theme.name,
272
+ version: v.theme.version,
273
+ kinds: ["theme"],
274
+ capabilities: [],
275
+ tier: "files",
276
+ manifestFile: THEME_FILE,
277
+ implicit: true,
278
+ theme: v.theme,
279
+ },
280
+ }
281
+ : {}),
282
+ };
283
+ }
284
+
285
+ let raw: unknown;
286
+ try {
287
+ raw = JSON.parse(fs.readFileSync(pluginFile, "utf-8"));
288
+ } catch (e) {
289
+ return {
290
+ ok: false,
291
+ manifestFile: PLUGIN_FILE,
292
+ issues: [
293
+ {
294
+ at: PLUGIN_FILE,
295
+ message: `not valid JSON: ${(e as Error).message}`,
296
+ },
297
+ ],
298
+ contrast: [],
299
+ };
300
+ }
301
+ const validate = schemaValidator();
302
+ if (!validate(raw))
303
+ return {
304
+ ok: false,
305
+ manifestFile: PLUGIN_FILE,
306
+ issues: schemaIssues(validate.errors, pluginMessage).map((i) => ({
307
+ at: `${PLUGIN_FILE}${i.at ? `#${i.at}` : ""}`,
308
+ message: i.message,
309
+ })),
310
+ contrast: [],
311
+ };
312
+
313
+ const m = raw as PluginManifest;
314
+ const kinds = m.provides
315
+ .map((p) => p.kind)
316
+ .filter((k, i, a) => a.indexOf(k) === i);
317
+ const tier = tierOf(kinds);
318
+ const issues: PluginIssue[] = [];
319
+ const contrast: ContrastFailure[] = [];
320
+ let theme: ThemeManifest | undefined;
321
+ if (opts.expectId && m.id !== opts.expectId)
322
+ issues.push({
323
+ at: `${PLUGIN_FILE}#/id`,
324
+ message: `id "${m.id}" does not match its directory "${opts.expectId}"`,
325
+ });
326
+
327
+ // Each kind's own validator. Kinds without one yet (catalog, persona-pack,
328
+ // the server tier) are checked by the schema only, until their slice.
329
+ m.provides.forEach((p, i) => {
330
+ const at = `${PLUGIN_FILE}#/provides/${i}`;
331
+ if (p.kind === "theme" && p.manifest) {
332
+ const abs = inside(dir, p.manifest);
333
+ if (!abs || !fs.existsSync(abs)) {
334
+ issues.push({ at, message: `${p.manifest} is missing` });
335
+ return;
336
+ }
337
+ const v = validateThemeDir(path.dirname(abs), { expectId: m.id });
338
+ for (const t of v.issues)
339
+ issues.push({
340
+ at: `${p.manifest}${t.at ? `#${t.at}` : ""}`,
341
+ message: t.message,
342
+ });
343
+ contrast.push(...v.contrast);
344
+ theme = v.theme;
345
+ } else if (p.kind === "page" && p.entry) {
346
+ const abs = inside(dir, p.entry);
347
+ if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isFile())
348
+ issues.push({ at, message: `${p.entry} is missing` });
349
+ }
350
+ });
351
+ issues.push(...readmeIssues(dir, m, tier));
352
+ const range = denRangeIssue((m as { den?: string }).den);
353
+ if (range) issues.push({ at: `${PLUGIN_FILE}#/den`, message: range });
354
+ for (const link of symlinksIn(dir))
355
+ issues.push({
356
+ at: link,
357
+ message: "is a symlink: a plugin's files must be its own",
358
+ });
359
+
360
+ return {
361
+ ok: issues.length === 0 && contrast.length === 0,
362
+ manifestFile: PLUGIN_FILE,
363
+ plugin: {
364
+ id: m.id,
365
+ name: m.name,
366
+ version: m.version,
367
+ kinds,
368
+ capabilities: m.capabilities ?? [],
369
+ tier,
370
+ manifestFile: PLUGIN_FILE,
371
+ implicit: false,
372
+ manifest: m,
373
+ ...(theme ? { theme } : {}),
374
+ },
375
+ issues,
376
+ contrast,
377
+ };
378
+ }
379
+
380
+ /**
381
+ * The directory holding the theme a plugin provides: the plugin itself for
382
+ * an implicit theme plugin, else the directory of its theme provide's
383
+ * manifest. Null when it provides no theme. Reads den-plugin.json without
384
+ * the schema (the runtime validates the theme itself), so it only trusts a
385
+ * manifest path that stays inside the plugin.
386
+ */
387
+ export function themeDirIn(pluginDir: string): string | null {
388
+ const file = path.join(pluginDir, PLUGIN_FILE);
389
+ if (!fs.existsSync(file))
390
+ return fs.existsSync(path.join(pluginDir, THEME_FILE)) ? pluginDir : null;
391
+ try {
392
+ const m = JSON.parse(fs.readFileSync(file, "utf-8")) as {
393
+ provides?: { kind?: unknown; manifest?: unknown }[];
394
+ };
395
+ const p = (Array.isArray(m.provides) ? m.provides : []).find(
396
+ (x) => x?.kind === "theme" && typeof x.manifest === "string",
397
+ );
398
+ if (!p) return null;
399
+ const rel = p.manifest as string;
400
+ if (path.basename(rel) !== THEME_FILE) return null;
401
+ const abs = inside(pluginDir, rel);
402
+ return abs && fs.existsSync(abs) ? path.dirname(abs) : null;
403
+ } catch {
404
+ return null;
405
+ }
406
+ }