@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `stack ls | up <name…> [--build] | down <name…> | restart <name…> | health <name…> | ps <name> | logs <name> [args]`
|
|
2
|
+
* `stack ls | up <name…> [--build] | down <name…> [--force] | restart <name…> | health <name…> | ps <name> | logs <name> [args]`
|
|
3
3
|
*
|
|
4
4
|
* Runs repos' Docker Compose projects with no host ports, each as its own
|
|
5
5
|
* project (`<stack>-<workspace>`), served by the edge at
|
|
@@ -20,7 +20,19 @@ import {
|
|
|
20
20
|
stackRestartService,
|
|
21
21
|
stackUp,
|
|
22
22
|
} from "../../shared/stacks/runtime.js";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
loadStackContext,
|
|
25
|
+
rejoinRunningStacks,
|
|
26
|
+
stackStatuses,
|
|
27
|
+
} from "../../shared/stacks/status.js";
|
|
28
|
+
import {
|
|
29
|
+
assertStackClaim,
|
|
30
|
+
forgetStackOwners,
|
|
31
|
+
recordStackOwner,
|
|
32
|
+
releaseStackOwner,
|
|
33
|
+
stackRefcounted,
|
|
34
|
+
} from "../../shared/stacks/owners.js";
|
|
35
|
+
import { resolveStackNamespace } from "../../shared/stacks/namespace.js";
|
|
24
36
|
import type { StackConfig } from "../../shared/stacks/types.js";
|
|
25
37
|
import { STACK_HEALTH_UNKNOWN_EXIT } from "../../shared/constants.js";
|
|
26
38
|
|
|
@@ -104,6 +116,19 @@ export async function cmdStack(argv: string[]): Promise<void> {
|
|
|
104
116
|
const ctx = loadStackContext();
|
|
105
117
|
const { edge, stacks } = ctx;
|
|
106
118
|
const names = args.filter((a) => !a.startsWith("-"));
|
|
119
|
+
// workspaces.yml overriding a different blueprint stack_namespace.
|
|
120
|
+
if (ctx.denWorkspace && ctx.namespaceSource === "workspace")
|
|
121
|
+
for (const w of resolveStackNamespace(ctx.denWorkspace).warnings)
|
|
122
|
+
console.error(chalk.yellow(`⚠ ${w}`));
|
|
123
|
+
/** Each stack's other live owners; throws on a checkout mismatch. */
|
|
124
|
+
const claims = (targets: string[]): Map<string, string[]> => {
|
|
125
|
+
const out = new Map<string, string[]>();
|
|
126
|
+
if (!stackRefcounted(ctx)) return out;
|
|
127
|
+
for (const n of targets)
|
|
128
|
+
if (stacks[n])
|
|
129
|
+
out.set(n, assertStackClaim(ctx, stacks[n], ctx.denWorkspace));
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
107
132
|
const pick = () => {
|
|
108
133
|
if (!names.length)
|
|
109
134
|
throw new Error(
|
|
@@ -131,13 +156,25 @@ export async function cmdStack(argv: string[]): Promise<void> {
|
|
|
131
156
|
// Re-render the edge so its cert covers every stack, then start the stacks.
|
|
132
157
|
const order = startOrder(pick(), stacks);
|
|
133
158
|
const secrets = await resolveStacksSecretEnv(order.map((n) => stacks[n]));
|
|
159
|
+
// A shared namespace: refused before anything starts when another
|
|
160
|
+
// workspace runs one of these projects from a different checkout.
|
|
161
|
+
const shared = claims(order);
|
|
134
162
|
await edgeUp(edge);
|
|
163
|
+
rejoinRunningStacks();
|
|
135
164
|
for (const n of order) {
|
|
136
165
|
console.log(chalk.cyan(`\n▶ ${n} (${projectName(n, ctx.workspace)})`));
|
|
166
|
+
if (shared.get(n)?.length)
|
|
167
|
+
console.log(
|
|
168
|
+
chalk.gray(
|
|
169
|
+
` shared with [${shared.get(n)!.join(", ")}] (stack namespace ${ctx.workspace})`,
|
|
170
|
+
),
|
|
171
|
+
);
|
|
137
172
|
stackUp(ctx, stacks[n], {
|
|
138
173
|
build: args.includes("--build"),
|
|
139
174
|
secretEnv: secrets.get(n),
|
|
140
175
|
});
|
|
176
|
+
if (stackRefcounted(ctx))
|
|
177
|
+
recordStackOwner(ctx, stacks[n], ctx.denWorkspace);
|
|
141
178
|
for (const route of Object.keys(stacks[n].routes))
|
|
142
179
|
console.log(chalk.green(` ✓ ${routeUrl(`${route}.${n}`, edge)}`));
|
|
143
180
|
}
|
|
@@ -158,14 +195,24 @@ export async function cmdStack(argv: string[]): Promise<void> {
|
|
|
158
195
|
}
|
|
159
196
|
return;
|
|
160
197
|
}
|
|
198
|
+
const shared = claims(pick());
|
|
161
199
|
await edgeUp(edge);
|
|
200
|
+
rejoinRunningStacks();
|
|
162
201
|
for (const n of pick()) {
|
|
163
202
|
console.log(chalk.cyan(`↻ ${n}`));
|
|
203
|
+
if (shared.get(n)?.length)
|
|
204
|
+
console.log(
|
|
205
|
+
chalk.yellow(
|
|
206
|
+
` Warning: restarting a shared stack — this interrupts [${shared.get(n)!.join(", ")}]`,
|
|
207
|
+
),
|
|
208
|
+
);
|
|
164
209
|
stackDown(ctx, stacks[n]);
|
|
165
210
|
stackUp(ctx, stacks[n], {
|
|
166
211
|
build: args.includes("--build"),
|
|
167
212
|
secretEnv: secrets.get(n),
|
|
168
213
|
});
|
|
214
|
+
if (stackRefcounted(ctx))
|
|
215
|
+
recordStackOwner(ctx, stacks[n], ctx.denWorkspace);
|
|
169
216
|
}
|
|
170
217
|
return;
|
|
171
218
|
}
|
|
@@ -173,8 +220,22 @@ export async function cmdStack(argv: string[]): Promise<void> {
|
|
|
173
220
|
for (const n of startOrder(pick(), stacks)
|
|
174
221
|
.reverse()
|
|
175
222
|
.filter((n) => names.includes(n))) {
|
|
223
|
+
// A shared namespace: the last live owner stops it; any other
|
|
224
|
+
// detaches. `--force` stops it whoever else holds it.
|
|
225
|
+
if (stackRefcounted(ctx) && !args.includes("--force")) {
|
|
226
|
+
const r = releaseStackOwner(ctx, stacks[n], ctx.denWorkspace);
|
|
227
|
+
if (!r.stop) {
|
|
228
|
+
console.log(
|
|
229
|
+
chalk.cyan(
|
|
230
|
+
`▼ ${n}: detached workspace "${ctx.denWorkspace}" — ${projectName(n, ctx.workspace)} still running for [${r.remaining.join(", ")}]`,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
176
236
|
console.log(chalk.cyan(`▼ ${n}`));
|
|
177
237
|
stackDown(ctx, stacks[n]);
|
|
238
|
+
forgetStackOwners(ctx, stacks[n]);
|
|
178
239
|
}
|
|
179
240
|
return;
|
|
180
241
|
case "ps":
|
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
tsxCommand,
|
|
25
25
|
type EngineCommand,
|
|
26
26
|
} from "../../shared/engine.js";
|
|
27
|
+
import { product } from "../../shared/product.js";
|
|
28
|
+
|
|
29
|
+
/** Remediation when test sessions cannot be created (no vendor assumed). */
|
|
30
|
+
const SETUP_SECRETS_HINT = ` make ${product().cli}-setup-secrets - Set up the selected secrets.provider`;
|
|
31
|
+
const CI_CREDENTIALS_HINT =
|
|
32
|
+
" CI: E2E_<ROLE>_EMAIL / E2E_<ROLE>_PASSWORD env vars";
|
|
27
33
|
|
|
28
34
|
/** After Playwright smoke runs: fail if storage fixture handoff was left behind. */
|
|
29
35
|
function smokePostRunTeardownVerifyStep(env: NodeJS.ProcessEnv): CommandStep {
|
|
@@ -913,13 +919,11 @@ export class TestRunner {
|
|
|
913
919
|
if (shouldSetup) {
|
|
914
920
|
console.log(
|
|
915
921
|
chalk.cyan(
|
|
916
|
-
"\n📝 Sessions are created automatically — ensure
|
|
922
|
+
"\n📝 Sessions are created automatically — ensure the secrets backend is ready:\n",
|
|
917
923
|
),
|
|
918
924
|
);
|
|
919
|
-
console.log(chalk.cyan(
|
|
920
|
-
console.log(
|
|
921
|
-
chalk.cyan(" make auth-configure - One-time admin setup\n"),
|
|
922
|
-
);
|
|
925
|
+
console.log(chalk.cyan(SETUP_SECRETS_HINT));
|
|
926
|
+
console.log(chalk.cyan(`${CI_CREDENTIALS_HINT}\n`));
|
|
923
927
|
return false;
|
|
924
928
|
} else {
|
|
925
929
|
console.log(chalk.red("Cannot run tests without authentication\n"));
|
|
@@ -934,10 +938,8 @@ export class TestRunner {
|
|
|
934
938
|
"\n⚠️ Authentication failed — sessions are created fresh each run\n",
|
|
935
939
|
),
|
|
936
940
|
);
|
|
937
|
-
console.log(chalk.cyan(
|
|
938
|
-
console.log(
|
|
939
|
-
chalk.cyan(" make auth-configure - One-time admin setup\n"),
|
|
940
|
-
);
|
|
941
|
+
console.log(chalk.cyan(SETUP_SECRETS_HINT));
|
|
942
|
+
console.log(chalk.cyan(`${CI_CREDENTIALS_HINT}\n`));
|
|
941
943
|
return false;
|
|
942
944
|
}
|
|
943
945
|
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `theme validate | add | list | remove | new`
|
|
3
|
+
*
|
|
4
|
+
* Sugar over `plugin *` (PL-2a): the same commands, filtered to kind theme,
|
|
5
|
+
* with the same output and exit codes. Only `new` is theme-specific.
|
|
6
|
+
*
|
|
7
|
+
* A theme is a den-theme.json (den.theme/v1) with its files, in any git repo.
|
|
8
|
+
* `add` fetches one at an exact ref, validates it, and vendors it into the
|
|
9
|
+
* distribution (`plugins/<id>/`) with a lock entry (`plugins.lock.json`: kinds,
|
|
10
|
+
* source, ref, commit, content hash), so it is pinned the way Den is, works offline,
|
|
11
|
+
* and the whole team gets the same files from the distribution's own repo.
|
|
12
|
+
* Den's default theme ships inside Den (themes/den in the package).
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import { getDistRoot, getEngineRoot } from "../../shared/paths.js";
|
|
18
|
+
import { product } from "../../shared/product.js";
|
|
19
|
+
import { readBrand } from "../../shared/brand.js";
|
|
20
|
+
import { THEME_FILE } from "../../shared/theme/contract.js";
|
|
21
|
+
import { PLUGIN_ID, PLUGIN_ID_RULE } from "../../shared/plugins/manifest.js";
|
|
22
|
+
import {
|
|
23
|
+
PLUGINS_DIR,
|
|
24
|
+
hashFiles,
|
|
25
|
+
parsePluginSource,
|
|
26
|
+
pluginDirFor,
|
|
27
|
+
type LockEntry,
|
|
28
|
+
type PluginSource,
|
|
29
|
+
} from "../../shared/plugins/store.js";
|
|
30
|
+
import { runPluginCommand, type PluginDeps } from "./plugin-command.js";
|
|
31
|
+
|
|
32
|
+
// The plugin store is where themes live (PL-2a); these names stay for
|
|
33
|
+
// theme-runtime and older callers.
|
|
34
|
+
export {
|
|
35
|
+
LOCK_FILE,
|
|
36
|
+
PLUGINS_DIR,
|
|
37
|
+
installedThemeIds,
|
|
38
|
+
readLock,
|
|
39
|
+
refKind,
|
|
40
|
+
type GitRunner,
|
|
41
|
+
type LockEntry,
|
|
42
|
+
} from "../../shared/plugins/store.js";
|
|
43
|
+
export { validationSummary } from "./plugin-command.js";
|
|
44
|
+
|
|
45
|
+
export type ThemeSource = PluginSource;
|
|
46
|
+
export const parseThemeSource = parsePluginSource;
|
|
47
|
+
export const hashTheme = hashFiles;
|
|
48
|
+
export const themeDirFor = pluginDirFor;
|
|
49
|
+
|
|
50
|
+
/** The lock's theme entries (the lock is shared with other plugin kinds). */
|
|
51
|
+
export function themesIn(lock: Record<string, LockEntry>): LockEntry[] {
|
|
52
|
+
return Object.values(lock).filter((e) => e.kinds?.includes("theme"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ThemeDeps = PluginDeps;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Run `theme <sub>`; returns the exit code. add, list, validate and remove
|
|
59
|
+
* are `plugin <sub>` filtered to kind theme; new is the theme scaffold.
|
|
60
|
+
*/
|
|
61
|
+
export async function cmdTheme(
|
|
62
|
+
args: string[],
|
|
63
|
+
deps: ThemeDeps = {},
|
|
64
|
+
): Promise<number> {
|
|
65
|
+
const log = deps.log ?? ((l: string) => console.log(l));
|
|
66
|
+
const distRoot = deps.distRoot ?? getDistRoot();
|
|
67
|
+
const [sub, ...rest] = args;
|
|
68
|
+
const cli = product().cli;
|
|
69
|
+
|
|
70
|
+
switch (sub) {
|
|
71
|
+
case "validate":
|
|
72
|
+
case "add":
|
|
73
|
+
case "list":
|
|
74
|
+
case "remove":
|
|
75
|
+
case undefined:
|
|
76
|
+
return runPluginCommand(args, "theme", deps);
|
|
77
|
+
|
|
78
|
+
case "new": {
|
|
79
|
+
// A local theme to edit: a copy of the built-in one, in plugins/<id>/,
|
|
80
|
+
// committed with the distribution so the whole team gets it.
|
|
81
|
+
const id = rest[0];
|
|
82
|
+
if (!id || !PLUGIN_ID.test(id) || id === "den") {
|
|
83
|
+
log(
|
|
84
|
+
chalk.red(
|
|
85
|
+
`Usage: ${cli} theme new <id> (${PLUGIN_ID_RULE}; not "den")`,
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
const dest = themeDirFor(distRoot, id);
|
|
91
|
+
if (fs.existsSync(dest)) {
|
|
92
|
+
log(
|
|
93
|
+
chalk.red(`✗ ${PLUGINS_DIR}/${id}/ already exists; nothing changed`),
|
|
94
|
+
);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
const from = path.join(
|
|
98
|
+
deps.engineRoot ?? getEngineRoot(),
|
|
99
|
+
"themes",
|
|
100
|
+
"den",
|
|
101
|
+
);
|
|
102
|
+
// Not Den's tab icon: a new theme's tab shows its own mark.
|
|
103
|
+
const denIcon = path.join(from, "favicon.ico");
|
|
104
|
+
fs.cpSync(from, dest, {
|
|
105
|
+
recursive: true,
|
|
106
|
+
filter: (src) => src !== denIcon,
|
|
107
|
+
});
|
|
108
|
+
const file = path.join(dest, THEME_FILE);
|
|
109
|
+
const m = JSON.parse(fs.readFileSync(file, "utf-8")) as Record<
|
|
110
|
+
string,
|
|
111
|
+
unknown
|
|
112
|
+
>;
|
|
113
|
+
delete m.favicon;
|
|
114
|
+
const name = id
|
|
115
|
+
.split("-")
|
|
116
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
117
|
+
.join(" ");
|
|
118
|
+
Object.assign(m, { id, name, version: "0.1.0" });
|
|
119
|
+
delete m.homepage;
|
|
120
|
+
// The author is the theme's maintainer (plugin rule 1): the team.
|
|
121
|
+
m.author = readBrand().name;
|
|
122
|
+
fs.writeFileSync(file, `${JSON.stringify(m, null, 2)}\n`);
|
|
123
|
+
log(
|
|
124
|
+
` ${id} → ${PLUGINS_DIR}/${id}/ (a copy of den: edit ${THEME_FILE} and mark.svg)`,
|
|
125
|
+
);
|
|
126
|
+
log(` ${cli} theme validate checks it; commit it and your team has it.`);
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
default:
|
|
131
|
+
log(
|
|
132
|
+
`Usage: ${cli} theme [list] | validate [dir] | new <id> | add <repo>[/<subpath>]@<ref> | remove <id>`,
|
|
133
|
+
);
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -4,11 +4,12 @@ import * as fs from "fs";
|
|
|
4
4
|
import * as http from "http";
|
|
5
5
|
import * as path from "path";
|
|
6
6
|
import { pathToFileURL } from "url";
|
|
7
|
+
import { CLI_SPAWN_TIMEOUT_MS, DEFAULT_PORT } from "../../shared/constants.js";
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../../shared/
|
|
9
|
+
readSecretsConfig,
|
|
10
|
+
secretsProcessEnv,
|
|
11
|
+
type SecretsConfig,
|
|
12
|
+
} from "../../shared/secrets/index.js";
|
|
12
13
|
import { getDistRoot, getEngineRoot, getStateDir } from "../../shared/paths.js";
|
|
13
14
|
import { hasDenUiMarker } from "../../shared/ui-marker.js";
|
|
14
15
|
import { engineBin, engineModuleFile } from "../../shared/engine.js";
|
|
@@ -269,26 +270,43 @@ async function getHttpText(
|
|
|
269
270
|
});
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
/**
|
|
274
|
+
* The API is up: /api/server-info answers whatever the config holds
|
|
275
|
+
* (0.3.0-dev.42: /api/health answers 422 under an invalid config, so
|
|
276
|
+
* `web up` reported a running server as not ready). A server too old for
|
|
277
|
+
* server-info is asked /api/health, as before.
|
|
278
|
+
*/
|
|
272
279
|
async function isDenWebApiRunning(port: number): Promise<boolean> {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
const json = async (p: string): Promise<unknown> => {
|
|
281
|
+
const response = await getHttpText(port, p);
|
|
282
|
+
if (!response || response.statusCode !== 200) return undefined;
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(response.body) as unknown;
|
|
285
|
+
} catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const info = await json("/api/server-info");
|
|
290
|
+
if (
|
|
291
|
+
info &&
|
|
292
|
+
typeof info === "object" &&
|
|
293
|
+
typeof (info as { pid?: unknown }).pid === "number"
|
|
294
|
+
)
|
|
295
|
+
return true;
|
|
296
|
+
return Array.isArray(await json("/api/health"));
|
|
280
297
|
}
|
|
281
298
|
|
|
282
299
|
async function isDenWebUiReady(port: number): Promise<boolean> {
|
|
283
300
|
const response = await getHttpText(port, "/index.html");
|
|
284
301
|
return (
|
|
285
|
-
|
|
302
|
+
// 422: the "Config is invalid" page, the shell serving as it should.
|
|
303
|
+
(response?.statusCode === 200 || response?.statusCode === 422) &&
|
|
286
304
|
/<html[\s>]/i.test(response.body) &&
|
|
287
305
|
hasDenUiMarker(response.body)
|
|
288
306
|
);
|
|
289
307
|
}
|
|
290
308
|
|
|
291
|
-
async function isDenWebRunning(port: number): Promise<boolean> {
|
|
309
|
+
export async function isDenWebRunning(port: number): Promise<boolean> {
|
|
292
310
|
return (await isDenWebApiRunning(port)) && (await isDenWebUiReady(port));
|
|
293
311
|
}
|
|
294
312
|
|
|
@@ -350,6 +368,20 @@ export function resolveServerEntrypoint(engine: string = getEngineRoot()): {
|
|
|
350
368
|
};
|
|
351
369
|
}
|
|
352
370
|
|
|
371
|
+
/**
|
|
372
|
+
* The env the web server daemon starts with: the caller's, its port, and
|
|
373
|
+
* whatever the selected secrets strategy gives (only 1password gives one,
|
|
374
|
+
* OP_ACCOUNT, from the distribution's `secrets.onepassword.account` or the
|
|
375
|
+
* caller's own OP_ACCOUNT). Den adds no account of its own.
|
|
376
|
+
*/
|
|
377
|
+
export function webServerEnv(
|
|
378
|
+
port: number,
|
|
379
|
+
base: NodeJS.ProcessEnv = process.env,
|
|
380
|
+
cfg: SecretsConfig = readSecretsConfig(),
|
|
381
|
+
): NodeJS.ProcessEnv {
|
|
382
|
+
return { ...base, PORT: String(port), ...secretsProcessEnv(cfg, base) };
|
|
383
|
+
}
|
|
384
|
+
|
|
353
385
|
function startDenWebServer(options: WebCommandOptions): StartedServer {
|
|
354
386
|
const root = getDistRoot();
|
|
355
387
|
const entrypoint = resolveServerEntrypoint();
|
|
@@ -369,11 +401,7 @@ function startDenWebServer(options: WebCommandOptions): StartedServer {
|
|
|
369
401
|
{
|
|
370
402
|
cwd: root,
|
|
371
403
|
detached: true,
|
|
372
|
-
env:
|
|
373
|
-
...process.env,
|
|
374
|
-
PORT: String(options.port),
|
|
375
|
-
OP_ACCOUNT: process.env.OP_ACCOUNT ?? OP_ACCOUNT_DEFAULT,
|
|
376
|
-
},
|
|
404
|
+
env: webServerEnv(options.port),
|
|
377
405
|
stdio: ["ignore", logFd, logFd],
|
|
378
406
|
},
|
|
379
407
|
);
|
|
@@ -435,11 +463,7 @@ export function spawnDetachedWebRestart(port: number): number | undefined {
|
|
|
435
463
|
{
|
|
436
464
|
cwd: root,
|
|
437
465
|
detached: true,
|
|
438
|
-
env:
|
|
439
|
-
...process.env,
|
|
440
|
-
PORT: String(port),
|
|
441
|
-
OP_ACCOUNT: process.env.OP_ACCOUNT ?? OP_ACCOUNT_DEFAULT,
|
|
442
|
-
},
|
|
466
|
+
env: webServerEnv(port),
|
|
443
467
|
stdio: ["ignore", logFd, logFd],
|
|
444
468
|
},
|
|
445
469
|
);
|
package/src/ui/mcp/api.ts
CHANGED
|
@@ -85,6 +85,28 @@ export class DenHttpError extends Error {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* An error body as a line: the server's R26b JSON ({ ok: false, error,
|
|
90
|
+
* code, requestId }) as its message and request id, anything else as is.
|
|
91
|
+
*/
|
|
92
|
+
export function errorText(body: string): string {
|
|
93
|
+
try {
|
|
94
|
+
const j = JSON.parse(body) as {
|
|
95
|
+
error?: unknown;
|
|
96
|
+
code?: unknown;
|
|
97
|
+
requestId?: unknown;
|
|
98
|
+
};
|
|
99
|
+
if (j && typeof j.error === "string")
|
|
100
|
+
return (
|
|
101
|
+
j.error +
|
|
102
|
+
(typeof j.requestId === "string" ? ` (request ${j.requestId})` : "")
|
|
103
|
+
);
|
|
104
|
+
} catch {
|
|
105
|
+
/* not JSON */
|
|
106
|
+
}
|
|
107
|
+
return body;
|
|
108
|
+
}
|
|
109
|
+
|
|
88
110
|
/** Read routes: GET only. */
|
|
89
111
|
const READ_ROUTES = new Set([
|
|
90
112
|
"/api/health",
|
|
@@ -268,8 +290,12 @@ export function createDenApi(opts: {
|
|
|
268
290
|
signal,
|
|
269
291
|
});
|
|
270
292
|
if (!res.ok) {
|
|
271
|
-
|
|
272
|
-
|
|
293
|
+
throw new DenHttpError(
|
|
294
|
+
method,
|
|
295
|
+
path,
|
|
296
|
+
res.status,
|
|
297
|
+
errorText((await res.text()).slice(0, 2000)),
|
|
298
|
+
);
|
|
273
299
|
}
|
|
274
300
|
return res;
|
|
275
301
|
} catch (e) {
|