@monster-orange/den 0.4.11 → 0.4.13
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 +169 -0
- package/README.md +7 -7
- package/docs/distributions.md +212 -12
- package/package.json +2 -1
- package/src/shared/agents/catalog.ts +77 -0
- package/src/shared/agents/default-pack/be-dev.md +1 -0
- package/src/shared/agents/default-pack/devops.md +1 -0
- package/src/shared/agents/default-pack/fe-dev.md +1 -0
- package/src/shared/agents/default-pack/orchestrator.md +3 -1
- package/src/shared/agents/default-pack/planner.md +1 -0
- package/src/shared/agents/default-pack/reviewer.md +1 -0
- package/src/shared/agents/default-pack/tester.md +16 -0
- package/src/shared/agents/default-pack/ux-designer.md +1 -0
- package/src/shared/agents/den-claude.ts +151 -17
- package/src/shared/agents/permissions.ts +108 -0
- package/src/shared/agents/persona-store.ts +286 -0
- package/src/shared/agents/personas.ts +116 -12
- package/src/shared/agents/records.ts +57 -0
- package/src/shared/agents/roles.ts +288 -0
- package/src/shared/agents/runtime.ts +49 -0
- package/src/shared/agents/session-name.ts +110 -0
- package/src/shared/agents/transcript-follow.ts +180 -0
- package/src/shared/den-config.ts +27 -1
- package/src/ui/cli/agent-ops.ts +578 -11
- package/src/ui/cli/agents-command.ts +12 -2
- package/src/ui/cli/env-probe.ts +4 -1
- package/src/ui/mcp/tools.ts +1 -1
- package/src/ui/server/agent-paths.ts +52 -0
- package/src/ui/server/agent-routes.ts +329 -19
- package/src/ui/server/agents-layout-pref.ts +43 -0
- package/src/ui/server/editor-open-pref.ts +41 -0
- package/src/ui/server/index.ts +22 -0
- package/src/ui/server/public/agent-start.js +3 -2
- package/src/ui/server/public/agents-page.js +1037 -67
- package/src/ui/server/public/agents-section.js +1 -1
- package/src/ui/server/public/agents.html +236 -20
- package/src/ui/server/public/header.js +2 -0
- package/src/ui/server/public/i18n/es.js +139 -0
- package/src/ui/server/public/i18n/ja.js +139 -0
- package/src/ui/server/public/persona-ui.css +539 -0
- package/src/ui/server/public/persona-ui.js +796 -0
- package/src/ui/server/public/settings-page.js +4 -9
- package/src/ui/server/public/settings-registry.js +66 -0
- package/src/ui/server/public/settings.html +1 -0
- package/src/ui/server/public/shell.js +137 -0
- package/src/ui/server/public/term-input.js +129 -0
- package/src/ui/server/public/topbar.js +22 -4
- package/src/ui/server/route-classes.ts +29 -0
- package/src/ui/server/session-routes.ts +34 -1
- package/src/ui/server/ui-assets.ts +8 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orchestrator
|
|
3
|
+
role: orchestrator
|
|
3
4
|
description: Takes the next ready story, picks the persona for it and starts that agent.
|
|
4
5
|
standing: true
|
|
5
6
|
context: [ready work items, running agents in the workspace]
|
|
@@ -9,7 +10,8 @@ You are the orchestrator. Keep work flowing without overloading the machine
|
|
|
9
10
|
or the people reviewing it.
|
|
10
11
|
|
|
11
12
|
- Take the next ready story, in priority order. Pick the persona that fits it:
|
|
12
|
-
fe-dev, be-dev or devops for implementation, ux-designer for design work
|
|
13
|
+
fe-dev, be-dev or devops for implementation, ux-designer for design work,
|
|
14
|
+
tester to run the suites.
|
|
13
15
|
- Start one agent per story, in its own worktree, and link it to the story.
|
|
14
16
|
- Respect the workspace's concurrency cap; wait rather than queueing more.
|
|
15
17
|
- When a pull request is opened, ask the reviewer for a review. Never approve
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tester
|
|
3
|
+
role: tester
|
|
4
|
+
description: Runs the test suites and reports failures with repro steps.
|
|
5
|
+
context: [the story and its acceptance criteria, the repo's test commands]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the tester. Find out what is broken before anyone ships it.
|
|
9
|
+
|
|
10
|
+
- Run the workspace's unit, integration and end-to-end suites, as the repo
|
|
11
|
+
documents them.
|
|
12
|
+
- For each failure, write the exact repro: the smallest failing command, what
|
|
13
|
+
it printed, and what you expected.
|
|
14
|
+
- Separate real failures from flaky ones; run a suspected flake again before
|
|
15
|
+
reporting it.
|
|
16
|
+
- Don't fix code. Report what you found to the person or agent who asked.
|
|
@@ -61,11 +61,26 @@ import {
|
|
|
61
61
|
type LaunchOutput,
|
|
62
62
|
type Runtime,
|
|
63
63
|
type RuntimeDetect,
|
|
64
|
+
type RuntimeModel,
|
|
65
|
+
type RuntimePermissions,
|
|
64
66
|
} from "./runtime.js";
|
|
67
|
+
import { PERMISSION_INTENTS, type PermissionIntent } from "./permissions.js";
|
|
65
68
|
import { builtinEntry } from "./catalog.js";
|
|
66
69
|
import { catalogFind } from "./catalog-runtime.js";
|
|
67
70
|
import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
|
|
68
71
|
import { claudeProjectsDir } from "../usage/paths.js";
|
|
72
|
+
import { cleanName, transcriptTitle } from "./transcript-follow.js";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `/rename "x"` keeps the quotes in customTitle: one matching pair around
|
|
76
|
+
* the whole name goes, for display only (0.4.13). Nothing inside: null.
|
|
77
|
+
*/
|
|
78
|
+
export function unquoteTitle(name: string | null): string | null {
|
|
79
|
+
if (!name || name.length < 2) return name;
|
|
80
|
+
const q = name[0];
|
|
81
|
+
if ((q !== '"' && q !== "'") || name[name.length - 1] !== q) return name;
|
|
82
|
+
return cleanName(name.slice(1, -1));
|
|
83
|
+
}
|
|
69
84
|
|
|
70
85
|
export const DEN_CLAUDE_ID = "den-claude";
|
|
71
86
|
const BIN = "claude";
|
|
@@ -89,12 +104,91 @@ export const PERMISSION_MODES = [
|
|
|
89
104
|
] as const;
|
|
90
105
|
export const DEFAULT_PERMISSION_MODE = "default";
|
|
91
106
|
|
|
92
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Den's permission intents (AG-13) as Claude Code's modes:
|
|
109
|
+
* https://code.claude.com/docs/en/iam (permission modes): `default` asks
|
|
110
|
+
* on first use of each tool, `acceptEdits` accepts file edits for the
|
|
111
|
+
* session, `bypassPermissions` skips every prompt.
|
|
112
|
+
*/
|
|
113
|
+
export const INTENT_MODE: Record<PermissionIntent, string> = {
|
|
114
|
+
ask: "default",
|
|
115
|
+
edits: "acceptEdits",
|
|
116
|
+
all: "bypassPermissions",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One plain command prefix as Claude Code allow rules (the same docs,
|
|
121
|
+
* Bash rules): the command alone, and the command with arguments (`*`
|
|
122
|
+
* matches any run of characters). Deny rules (Den's state dir) still win:
|
|
123
|
+
* Claude Code checks deny before allow.
|
|
124
|
+
*/
|
|
125
|
+
export const COMMAND_RULES = ["Bash({c})", "Bash({c} *)"];
|
|
126
|
+
|
|
127
|
+
/** How den-claude expresses permission intents (AG-13.2). */
|
|
128
|
+
export const DEN_CLAUDE_PERMISSIONS: RuntimePermissions = {
|
|
129
|
+
modes: {
|
|
130
|
+
ask: "--permission-mode default: asks before edits and commands",
|
|
131
|
+
edits:
|
|
132
|
+
"--permission-mode acceptEdits: file edits run without asking; commands ask",
|
|
133
|
+
all: "--permission-mode bypassPermissions: nothing asks",
|
|
134
|
+
},
|
|
135
|
+
commandRules: COMMAND_RULES,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** A command prefix's allow rules (permissions.ts checked it). */
|
|
139
|
+
export function allowRules(commands: readonly string[]): string[] {
|
|
140
|
+
return commands.flatMap((c) =>
|
|
141
|
+
COMMAND_RULES.map((r) => r.split("{c}").join(c)),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Claude Code's models (AG-12.3): the aliases `claude --model` takes,
|
|
147
|
+
* each "latest" (the alias tracks its newest model) or pinned to one full
|
|
148
|
+
* model id. Claude Code has no command that lists models, so the list
|
|
149
|
+
* ships with each Den release (https://code.claude.com/docs/en/model-config:
|
|
150
|
+
* aliases opus, sonnet, haiku, or a full model name). Newest first.
|
|
151
|
+
*/
|
|
152
|
+
export const DEN_CLAUDE_MODELS: readonly RuntimeModel[] = [
|
|
153
|
+
{
|
|
154
|
+
id: "opus",
|
|
155
|
+
label: "Opus",
|
|
156
|
+
versions: [
|
|
157
|
+
"claude-opus-5-5",
|
|
158
|
+
"claude-opus-5",
|
|
159
|
+
"claude-opus-4-8",
|
|
160
|
+
"claude-opus-4-7",
|
|
161
|
+
"claude-opus-4-6",
|
|
162
|
+
],
|
|
163
|
+
latest: "claude-opus-5-5",
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: "sonnet",
|
|
167
|
+
label: "Sonnet",
|
|
168
|
+
versions: ["claude-sonnet-5-5", "claude-sonnet-5", "claude-sonnet-4-6"],
|
|
169
|
+
latest: "claude-sonnet-5-5",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "haiku",
|
|
173
|
+
label: "Haiku",
|
|
174
|
+
versions: ["claude-haiku-5-5", "claude-haiku-4-5"],
|
|
175
|
+
latest: "claude-haiku-5-5",
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The mode to launch with: the agent's intent (Start › Advanced), else the
|
|
181
|
+
* persona's `permission_mode`: one of Den's intents, or one of Claude
|
|
182
|
+
* Code's own modes (as before AG-13). A bad one names the key.
|
|
183
|
+
*/
|
|
93
184
|
function permissionMode(input: LaunchInput): string {
|
|
185
|
+
if (input.permission) return INTENT_MODE[input.permission];
|
|
94
186
|
const m = input.persona.permissionMode ?? DEFAULT_PERMISSION_MODE;
|
|
187
|
+
if ((PERMISSION_INTENTS as readonly string[]).includes(m))
|
|
188
|
+
return INTENT_MODE[m as PermissionIntent];
|
|
95
189
|
if (!(PERMISSION_MODES as readonly string[]).includes(m))
|
|
96
190
|
throw new LaunchError(
|
|
97
|
-
`${input.persona.source}: permission_mode "${m.replace(/[^\x20-\x7e]/g, "?").slice(0, 40)}" must be one of ${PERMISSION_MODES.join(", ")}`,
|
|
191
|
+
`${input.persona.source}: permission_mode "${m.replace(/[^\x20-\x7e]/g, "?").slice(0, 40)}" must be one of ${[...PERMISSION_INTENTS, ...PERMISSION_MODES].join(", ")}`,
|
|
98
192
|
);
|
|
99
193
|
return m;
|
|
100
194
|
}
|
|
@@ -243,7 +337,9 @@ function common(input: LaunchInput): {
|
|
|
243
337
|
}),
|
|
244
338
|
)
|
|
245
339
|
: null;
|
|
246
|
-
const
|
|
340
|
+
const allow = allowRules(input.allow ?? []);
|
|
341
|
+
const settings =
|
|
342
|
+
input.stateDir || hooks || allow.length ? settingsPath(input) : null;
|
|
247
343
|
return {
|
|
248
344
|
argv: [
|
|
249
345
|
"--mcp-config",
|
|
@@ -267,13 +363,18 @@ function common(input: LaunchInput): {
|
|
|
267
363
|
? {
|
|
268
364
|
[settings]: `${JSON.stringify(
|
|
269
365
|
{
|
|
270
|
-
...(input.stateDir
|
|
366
|
+
...(input.stateDir || allow.length
|
|
271
367
|
? {
|
|
272
368
|
permissions: {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
369
|
+
...(allow.length ? { allow } : {}),
|
|
370
|
+
...(input.stateDir
|
|
371
|
+
? {
|
|
372
|
+
deny: stateDirDenyRules(
|
|
373
|
+
input.stateDir,
|
|
374
|
+
input.env?.HOME,
|
|
375
|
+
),
|
|
376
|
+
}
|
|
377
|
+
: {}),
|
|
277
378
|
},
|
|
278
379
|
}
|
|
279
380
|
: {}),
|
|
@@ -288,7 +389,9 @@ function common(input: LaunchInput): {
|
|
|
288
389
|
warnings:
|
|
289
390
|
mode === "bypassPermissions"
|
|
290
391
|
? [
|
|
291
|
-
|
|
392
|
+
input.permission === "all"
|
|
393
|
+
? `⚠ agent ${input.agentId} starts with auto-approve all (bypassPermissions): Claude Code will not ask before running any tool`
|
|
394
|
+
: `⚠ persona ${input.persona.id} sets permission_mode: bypassPermissions: Claude Code will not ask before running any tool`,
|
|
292
395
|
]
|
|
293
396
|
: [],
|
|
294
397
|
};
|
|
@@ -313,6 +416,31 @@ export function firstPromptArg(text: string | undefined): string[] {
|
|
|
313
416
|
return [t.startsWith("-") ? `Prompt: ${t}` : t];
|
|
314
417
|
}
|
|
315
418
|
|
|
419
|
+
const TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Claude Code's transcript for conversation `token`: <token>.jsonl in any
|
|
423
|
+
* project under its projects dir, or null (a bad token, none written yet).
|
|
424
|
+
*/
|
|
425
|
+
export function transcriptFile(
|
|
426
|
+
token: string | undefined,
|
|
427
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
428
|
+
): string | null {
|
|
429
|
+
if (!token || !TOKEN_RE.test(token)) return null;
|
|
430
|
+
const root = claudeProjectsDir(env);
|
|
431
|
+
let projects: string[] = [];
|
|
432
|
+
try {
|
|
433
|
+
projects = fs.readdirSync(root);
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
for (const p of projects) {
|
|
438
|
+
const file = path.join(root, p, `${token}.jsonl`);
|
|
439
|
+
if (fs.existsSync(file)) return file;
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
316
444
|
export const denClaude: Runtime = {
|
|
317
445
|
id: DEN_CLAUDE_ID,
|
|
318
446
|
label: DEN_CLAUDE_CATALOG.name,
|
|
@@ -321,6 +449,9 @@ export const denClaude: Runtime = {
|
|
|
321
449
|
denTools: true,
|
|
322
450
|
firstPrompt: true,
|
|
323
451
|
find: DEN_CLAUDE_FIND,
|
|
452
|
+
models: () =>
|
|
453
|
+
DEN_CLAUDE_MODELS.map((m) => ({ ...m, versions: [...m.versions] })),
|
|
454
|
+
permissions: DEN_CLAUDE_PERMISSIONS,
|
|
324
455
|
detect,
|
|
325
456
|
launch(input): LaunchOutput {
|
|
326
457
|
const bin = binFor(input);
|
|
@@ -355,17 +486,20 @@ export const denClaude: Runtime = {
|
|
|
355
486
|
},
|
|
356
487
|
// Its transcript, <token>.jsonl in any project under its projects dir.
|
|
357
488
|
conversationExists(token, env = process.env) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
489
|
+
return transcriptFile(token, env) !== null;
|
|
490
|
+
},
|
|
491
|
+
// AG-4b, unofficial (catalog sessionName): `/rename` appends a
|
|
492
|
+
// {"type":"custom-title","customTitle":…} record to the transcript; the
|
|
493
|
+
// latest one, read on from the last ask (never the whole file), less one
|
|
494
|
+
// pair of surrounding quotes. Null on any failure: it must never break a
|
|
495
|
+
// pane.
|
|
496
|
+
sessionName(rec, env = process.env) {
|
|
361
497
|
try {
|
|
362
|
-
|
|
498
|
+
const file = transcriptFile(rec.resumeToken, env);
|
|
499
|
+
return file ? unquoteTitle(transcriptTitle(file)) : null;
|
|
363
500
|
} catch {
|
|
364
|
-
return
|
|
501
|
+
return null;
|
|
365
502
|
}
|
|
366
|
-
return projects.some((p) =>
|
|
367
|
-
fs.existsSync(path.join(root, p, `${token}.jsonl`)),
|
|
368
|
-
);
|
|
369
503
|
},
|
|
370
504
|
launchFailures: {
|
|
371
505
|
noConversation: /no conversation found/i,
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Permissions as intent (AG-13): what a person chooses, before any
|
|
3
|
+
* runtime's syntax.
|
|
4
|
+
*
|
|
5
|
+
* ask Ask for everything (the default)
|
|
6
|
+
* edits Auto-approve edits in the worktree
|
|
7
|
+
* all Auto-approve all (a session started so is always tagged)
|
|
8
|
+
*
|
|
9
|
+
* And extra allowed commands: plain command prefixes ("npm test"), one per
|
|
10
|
+
* entry, no patterns. Each runtime translates both into its own launch
|
|
11
|
+
* config (den-claude.ts); a runtime that can't express one says so, and it
|
|
12
|
+
* is never silently dropped. What a persona pack shares is this intent
|
|
13
|
+
* only, never vendor syntax.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const PERMISSION_INTENTS = ["ask", "edits", "all"] as const;
|
|
17
|
+
export type PermissionIntent = (typeof PERMISSION_INTENTS)[number];
|
|
18
|
+
export const DEFAULT_INTENT: PermissionIntent = "ask";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A persona's `permission_mode` as an intent. Den's three intents are
|
|
22
|
+
* themselves; Claude Code's own modes from before AG-13 read as the intent
|
|
23
|
+
* they meant (default, acceptEdits, bypassPermissions). Anything else
|
|
24
|
+
* (Claude Code's plan, a typo) is not an intent: undefined, and the
|
|
25
|
+
* runtime decides (den-claude refuses a mode it doesn't know).
|
|
26
|
+
*/
|
|
27
|
+
export function intentOf(
|
|
28
|
+
mode: string | undefined,
|
|
29
|
+
): PermissionIntent | undefined {
|
|
30
|
+
if (mode === undefined) return DEFAULT_INTENT;
|
|
31
|
+
if ((PERMISSION_INTENTS as readonly string[]).includes(mode))
|
|
32
|
+
return mode as PermissionIntent;
|
|
33
|
+
if (mode === "default") return "ask";
|
|
34
|
+
if (mode === "acceptEdits") return "edits";
|
|
35
|
+
if (mode === "bypassPermissions") return "all";
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The most extra commands one persona or launch takes. */
|
|
40
|
+
export const MAX_COMMANDS = 32;
|
|
41
|
+
/** The longest command prefix. */
|
|
42
|
+
export const MAX_COMMAND_LEN = 120;
|
|
43
|
+
/**
|
|
44
|
+
* What a plain prefix is made of: letters, digits, spaces and . _ / : = @
|
|
45
|
+
* % + , -. No shell syntax (quotes, ;, &, |, $, backticks, redirects) and
|
|
46
|
+
* no pattern characters (* ? [ ] ^ ( ) { }): Den translates a prefix, it
|
|
47
|
+
* never passes one through a shell.
|
|
48
|
+
*/
|
|
49
|
+
const PREFIX_RE = /^[A-Za-z0-9][A-Za-z0-9 ._/:=@%+,-]*$/;
|
|
50
|
+
const PATTERN_RE = /[*?[\]^$(){}|]/;
|
|
51
|
+
|
|
52
|
+
export type CommandProblem = "pattern" | "invalid" | "long";
|
|
53
|
+
|
|
54
|
+
/** Why `c` isn't a plain prefix, or null when it is. */
|
|
55
|
+
export function commandProblem(c: string): CommandProblem | null {
|
|
56
|
+
if ([...c].length > MAX_COMMAND_LEN) return "long";
|
|
57
|
+
if (PATTERN_RE.test(c)) return "pattern";
|
|
58
|
+
if (!PREFIX_RE.test(c) || /\s{2,}/.test(c) || c !== c.trim())
|
|
59
|
+
return "invalid";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const shown = (v: unknown) =>
|
|
64
|
+
(JSON.stringify(v) ?? "undefined").replace(/[^\x20-\x7e]/g, "?").slice(0, 60);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A list of extra allowed commands, checked: an array of plain prefixes
|
|
68
|
+
* (each trimmed, duplicates dropped), at most MAX_COMMANDS. undefined or
|
|
69
|
+
* null is none. Anything else throws, naming the entry and why.
|
|
70
|
+
*/
|
|
71
|
+
export function checkCommands(v: unknown, where: string): string[] {
|
|
72
|
+
if (v === undefined || v === null) return [];
|
|
73
|
+
if (!Array.isArray(v))
|
|
74
|
+
throw new Error(`${where}: must be a list of command prefixes`);
|
|
75
|
+
if (v.length > MAX_COMMANDS)
|
|
76
|
+
throw new Error(`${where}: at most ${MAX_COMMANDS} commands`);
|
|
77
|
+
const out: string[] = [];
|
|
78
|
+
v.forEach((x, i) => {
|
|
79
|
+
if (typeof x !== "string")
|
|
80
|
+
throw new Error(`${where}[${i}]: must be a string`);
|
|
81
|
+
const c = x.trim();
|
|
82
|
+
const why = c ? commandProblem(c) : "invalid";
|
|
83
|
+
if (why === "pattern")
|
|
84
|
+
throw new Error(
|
|
85
|
+
`${where}[${i}]: ${shown(c)} is a pattern: use a plain prefix ("npm test")`,
|
|
86
|
+
);
|
|
87
|
+
if (why === "long")
|
|
88
|
+
throw new Error(`${where}[${i}]: at most ${MAX_COMMAND_LEN} characters`);
|
|
89
|
+
if (why)
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${where}[${i}]: ${shown(c)} must be a plain prefix: letters, digits, single spaces and . _ / : = @ % + , -`,
|
|
92
|
+
);
|
|
93
|
+
if (!out.includes(c)) out.push(c);
|
|
94
|
+
});
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** An intent, checked; a usage message naming `where` otherwise. */
|
|
99
|
+
export function checkIntent(v: unknown, where: string): PermissionIntent {
|
|
100
|
+
if (
|
|
101
|
+
typeof v === "string" &&
|
|
102
|
+
(PERMISSION_INTENTS as readonly string[]).includes(v)
|
|
103
|
+
)
|
|
104
|
+
return v as PermissionIntent;
|
|
105
|
+
throw new Error(
|
|
106
|
+
`${where}: ${shown(v)} must be one of ${PERMISSION_INTENTS.join(", ")}`,
|
|
107
|
+
);
|
|
108
|
+
}
|