@crouter/api 0.3.387 → 0.3.389
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/dist/api/__tests__/integration/client.test.js +97 -0
- package/dist/api/client.d.ts +7 -0
- package/dist/api/client.js +40 -21
- package/dist/api/dto/config.d.ts +11 -1
- package/dist/core/asset-root.d.ts +7 -0
- package/dist/core/asset-root.js +18 -0
- package/dist/core/canvas/boot-id.d.ts +6 -0
- package/dist/core/canvas/boot-id.js +26 -0
- package/dist/core/canvas/paths.d.ts +72 -0
- package/dist/core/canvas/paths.js +163 -0
- package/dist/core/canvas/pid.d.ts +391 -0
- package/dist/core/canvas/pid.js +948 -0
- package/dist/core/command-plugins/bundle.d.ts +149 -0
- package/dist/core/command-plugins/bundle.js +588 -0
- package/dist/core/command-plugins/endpoint.d.ts +24 -0
- package/dist/core/command-plugins/endpoint.js +51 -0
- package/dist/core/config.d.ts +233 -0
- package/dist/core/config.js +1120 -0
- package/dist/core/env-name.d.ts +6 -0
- package/dist/core/env-name.js +9 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.js +90 -0
- package/dist/core/events/emit.d.ts +6 -0
- package/dist/core/events/emit.js +42 -0
- package/dist/core/events/envelope.d.ts +2 -0
- package/dist/core/events/envelope.js +84 -0
- package/dist/core/events/errors.d.ts +4 -0
- package/dist/core/events/errors.js +69 -0
- package/dist/core/events/operation-id.d.ts +4 -0
- package/dist/core/events/operation-id.js +24 -0
- package/dist/core/events/serialize.d.ts +4 -0
- package/dist/core/events/serialize.js +199 -0
- package/dist/core/events/source.d.ts +16 -0
- package/dist/core/events/source.js +31 -0
- package/dist/core/events/types.d.ts +68 -0
- package/dist/core/events/types.js +11 -0
- package/dist/core/exclusive-lock.d.ts +34 -0
- package/dist/core/exclusive-lock.js +197 -0
- package/dist/core/fs-utils.d.ts +44 -0
- package/dist/core/fs-utils.js +208 -0
- package/dist/core/help.d.ts +309 -0
- package/dist/core/help.js +406 -0
- package/dist/core/human/page-catalog.d.ts +57 -0
- package/dist/core/human/page-catalog.js +172 -0
- package/dist/core/installed-plugins.d.ts +2 -0
- package/dist/core/installed-plugins.js +79 -0
- package/dist/core/io.d.ts +122 -0
- package/dist/core/io.js +373 -0
- package/dist/core/keybindings/attach-control.d.ts +49 -0
- package/dist/core/keybindings/attach-control.js +42 -0
- package/dist/core/keybindings/catalog.d.ts +18 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/types.d.ts +42 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/layout.d.ts +26 -0
- package/dist/core/layout.js +94 -0
- package/dist/core/locked-file.d.ts +27 -0
- package/dist/core/locked-file.js +118 -0
- package/dist/core/log.d.ts +9 -0
- package/dist/core/log.js +89 -0
- package/dist/core/manifest.d.ts +5 -0
- package/dist/core/manifest.js +15 -0
- package/dist/core/plugin-env.d.ts +8 -0
- package/dist/core/plugin-env.js +31 -0
- package/dist/core/plugin-extensions.d.ts +29 -0
- package/dist/core/plugin-extensions.js +191 -0
- package/dist/core/plugin-swap-lock.d.ts +9 -0
- package/dist/core/plugin-swap-lock.js +31 -0
- package/dist/core/preview-result-path.d.ts +4 -0
- package/dist/core/preview-result-path.js +26 -0
- package/dist/core/profiles/env-store.d.ts +22 -0
- package/dist/core/profiles/env-store.js +163 -0
- package/dist/core/profiles/fuzzy-match.d.ts +19 -0
- package/dist/core/profiles/fuzzy-match.js +92 -0
- package/dist/core/profiles/manifest.d.ts +120 -0
- package/dist/core/profiles/manifest.js +529 -0
- package/dist/core/rate-limit-scope.d.ts +25 -0
- package/dist/core/rate-limit-scope.js +64 -0
- package/dist/core/render.d.ts +12 -0
- package/dist/core/render.js +138 -0
- package/dist/core/resolver.d.ts +14 -0
- package/dist/core/resolver.js +111 -0
- package/dist/core/runtime/branded-host.d.ts +25 -0
- package/dist/core/runtime/branded-host.js +264 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
- package/dist/core/runtime/broker/daemon-ops.js +177 -0
- package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
- package/dist/core/runtime/broker/signal-stream.js +149 -0
- package/dist/core/scope.d.ts +32 -0
- package/dist/core/scope.js +184 -0
- package/dist/core/scoped-state/db.d.ts +17 -0
- package/dist/core/scoped-state/db.js +247 -0
- package/dist/core/scoped-state/migrate.d.ts +8 -0
- package/dist/core/scoped-state/migrate.js +187 -0
- package/dist/core/scoped-state/paths.d.ts +9 -0
- package/dist/core/scoped-state/paths.js +27 -0
- package/dist/core/scoped-state/profiles.d.ts +27 -0
- package/dist/core/scoped-state/profiles.js +93 -0
- package/dist/core/scoped-state/providers.d.ts +24 -0
- package/dist/core/scoped-state/providers.js +19 -0
- package/dist/core/scoped-state/schema.d.ts +6 -0
- package/dist/core/scoped-state/schema.js +43 -0
- package/dist/core/scoped-state/settings.d.ts +28 -0
- package/dist/core/scoped-state/settings.js +83 -0
- package/dist/core/spaces/open-beneath.d.ts +71 -0
- package/dist/core/spaces/open-beneath.js +581 -0
- package/dist/core/sqlite-statements.d.ts +4 -0
- package/dist/core/sqlite-statements.js +17 -0
- package/dist/core/subscription-state.d.ts +121 -0
- package/dist/core/subscription-state.js +287 -0
- package/dist/core/user-settings.d.ts +377 -0
- package/dist/core/user-settings.js +458 -0
- package/dist/daemon/broker-signals/bus.d.ts +30 -0
- package/dist/daemon/broker-signals/bus.js +87 -0
- package/dist/daemon/manage.d.ts +176 -0
- package/dist/daemon/manage.js +664 -0
- package/dist/daemon/pidfile.d.ts +8 -0
- package/dist/daemon/pidfile.js +37 -0
- package/dist/daemon/startup-policy.d.ts +1 -0
- package/dist/daemon/startup-policy.js +1 -0
- package/dist/native/linux.d.ts +29 -0
- package/dist/native/linux.js +20 -0
- package/dist/shared/env.d.ts +116 -0
- package/dist/shared/env.js +271 -0
- package/dist/shared/inbox-entry-body.d.ts +22 -0
- package/dist/shared/inbox-entry-body.js +116 -0
- package/dist/shared/working-activity.d.ts +9 -0
- package/dist/shared/working-activity.js +27 -0
- package/dist/types.d.ts +562 -0
- package/dist/types.js +186 -0
- package/package.json +1 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Descriptor types and renderers for the -h layer of the crtr CLI.
|
|
2
|
+
// Pure functions — no side effects, no commander, no process.exit.
|
|
3
|
+
// Rendering matches reference.md shapes exactly:
|
|
4
|
+
// root L11-32, branch L43-58, leaf L65-189.
|
|
5
|
+
/** `help` with the async state the render shows read once: the leaf's for
|
|
6
|
+
* ordinary help, only `focusedFlag`'s for focused help. A read that throws
|
|
7
|
+
* renders no element, the same soft failure `evalDynamic` gives a sync one. */
|
|
8
|
+
export async function withAsyncHelpState(help, state, focusedFlag) {
|
|
9
|
+
const read = async (load) => {
|
|
10
|
+
try {
|
|
11
|
+
const s = await load();
|
|
12
|
+
return () => s;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return () => null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const flagLoad = focusedFlag === undefined ? undefined : state.flags?.[focusedFlag];
|
|
19
|
+
const flagState = new Map(flagLoad === undefined ? [] : [[focusedFlag, await read(flagLoad)]]);
|
|
20
|
+
return {
|
|
21
|
+
...help,
|
|
22
|
+
...(focusedFlag === undefined && state.leaf !== undefined ? { dynamicState: await read(state.leaf) } : {}),
|
|
23
|
+
params: help.params?.map((p) => {
|
|
24
|
+
const dynamicState = p.kind === 'flag' ? flagState.get(p.name) : undefined;
|
|
25
|
+
return p.kind === 'flag' && p.focusedHelp !== undefined && dynamicState !== undefined
|
|
26
|
+
? { ...p, focusedHelp: { ...p.focusedHelp, dynamicState } }
|
|
27
|
+
: p;
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
// Internal helpers
|
|
32
|
+
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
33
|
+
* subtree that owns the state authors it through this, so the tag name and any
|
|
34
|
+
* scalar metadata (e.g. a count) travel with the data and render identically
|
|
35
|
+
* at every level the block appears. The tag name carries the label, so the
|
|
36
|
+
* body never repeats it. Attribute values are controlled (counts, short
|
|
37
|
+
* tokens) and not escaped. */
|
|
38
|
+
export function stateBlock(tag, attrs, body) {
|
|
39
|
+
const a = Object.entries(attrs)
|
|
40
|
+
.map(([k, v]) => ` ${k}="${v}"`)
|
|
41
|
+
.join('');
|
|
42
|
+
return `<${tag}${a}>\n${body}\n</${tag}>`;
|
|
43
|
+
}
|
|
44
|
+
/** Evaluate a dynamicState hook, soft-failing to null on throw or empty. */
|
|
45
|
+
function evalDynamic(fn) {
|
|
46
|
+
if (fn === undefined)
|
|
47
|
+
return null;
|
|
48
|
+
try {
|
|
49
|
+
const s = fn();
|
|
50
|
+
return s !== null && s !== '' ? s : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Return the longest string length in an array of names. */
|
|
57
|
+
function maxLen(names) {
|
|
58
|
+
let max = 0;
|
|
59
|
+
for (const n of names) {
|
|
60
|
+
if (n.length > max)
|
|
61
|
+
max = n.length;
|
|
62
|
+
}
|
|
63
|
+
return max;
|
|
64
|
+
}
|
|
65
|
+
/** Pad a string to the given width with trailing spaces. */
|
|
66
|
+
function pad(s, width) {
|
|
67
|
+
return s + ' '.repeat(Math.max(0, width - s.length));
|
|
68
|
+
}
|
|
69
|
+
// renderRoot
|
|
70
|
+
const IO_CONTRACT = 'I/O contract: flags and positional args on input; stdout is agent-ready markdown/XML you\n' +
|
|
71
|
+
'act on directly — read it as a continuation of your prompt, don\'t parse it as data.\n' +
|
|
72
|
+
'Exit 0 on success, non-zero on failure. Schemas appear at leaf -h; a leaf may route an uncommon flag to focused --flag -h guidance.';
|
|
73
|
+
// Behavioral instruction (not a schema) — engrained in the appended system
|
|
74
|
+
// prompt so the model treats unfamiliar capabilities as a cue to discover the
|
|
75
|
+
// contract, never to guess, AND reads a command's contract before invoking it.
|
|
76
|
+
// Lives in the root guide, outside any leaf -h. Scoped to state-changing
|
|
77
|
+
// invocations: cheap read-only inspection doesn't need a forced preflight.
|
|
78
|
+
const CAPABILITY_DISCOVERY = 'Before running a crtr command that changes state — creates, mutates, or ' +
|
|
79
|
+
"deletes anything — whose exact contract (args, flags, effects) you haven't " +
|
|
80
|
+
'verified this session, run `-h` on it and read the schema first — a reliable ' +
|
|
81
|
+
'read beats a guess that wastes a turn or triggers an unintended effect. ' +
|
|
82
|
+
'When a leaf routes a flag to focused help, read that focused help before using the flag; the compact row is discovery, not its invocation contract. ' +
|
|
83
|
+
"Same when the user names a capability you don't fully recognize: " +
|
|
84
|
+
'`-h` it before acting.';
|
|
85
|
+
/** Lines for a command's subcommand affordance at root: any promoted
|
|
86
|
+
* (common/important) subcommands, then a remainder line naming how many other
|
|
87
|
+
* subcommands exist behind `crtr <name> -h`. Returns [] when the command has
|
|
88
|
+
* no listable subcommands at all. */
|
|
89
|
+
function rootSubcommandLines(c) {
|
|
90
|
+
const promoted = c.subcommands ?? [];
|
|
91
|
+
const other = c.otherSubcommandCount ?? 0;
|
|
92
|
+
if (promoted.length === 0 && other === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const out = [];
|
|
95
|
+
if (promoted.length > 0) {
|
|
96
|
+
const labelW = maxLen(promoted.map((s) => s.path));
|
|
97
|
+
for (const s of promoted) {
|
|
98
|
+
// important → padded name + shortform desc; common → bare name.
|
|
99
|
+
out.push(s.desc !== undefined && s.desc !== ''
|
|
100
|
+
? ` ${pad(s.path, labelW)} ${s.desc}`
|
|
101
|
+
: ` ${s.path}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (other > 0) {
|
|
105
|
+
const word = promoted.length > 0 ? 'other subcommand' : 'subcommand';
|
|
106
|
+
out.push(` [+${other} ${word}${other === 1 ? '' : 's'} — \`crtr ${c.name} -h\`]`);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
export function renderRoot(h) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
lines.push(`${h.tagline}`);
|
|
113
|
+
lines.push('');
|
|
114
|
+
// Each subtree is one <command name="…"> block. The uniform wrapper states
|
|
115
|
+
// "this is a command you invoke as `crtr <name>`" — so the model reads them
|
|
116
|
+
// by one rule, and a nested state element (which is never a <command>) can't
|
|
117
|
+
// be mistaken for a sibling command. Inside: the concept (what it is), the
|
|
118
|
+
// selection rubric (when to pick it), then any self-named state element
|
|
119
|
+
// grouped with the command it belongs to. Once injected into a system prompt,
|
|
120
|
+
// each block reads as one self-contained concern domain. Header (tagline) and
|
|
121
|
+
// footer (Globals + I/O contract + capability-discovery rule) are the only
|
|
122
|
+
// non-command areas. Two levels of nesting: <command> → <state>.
|
|
123
|
+
for (const c of h.commands) {
|
|
124
|
+
lines.push(`<command name="${c.name}">`);
|
|
125
|
+
lines.push(c.concept);
|
|
126
|
+
lines.push(`use when ${c.useWhen}`);
|
|
127
|
+
// The command's subcommand surface: promoted (common/important) children
|
|
128
|
+
// inline, plus a "[+N other subcommands]" pointer to its own -h. Sits
|
|
129
|
+
// between the selection rubric and any live state block.
|
|
130
|
+
for (const l of rootSubcommandLines(c))
|
|
131
|
+
lines.push(l);
|
|
132
|
+
// dynamicState returns a complete self-named element (e.g.
|
|
133
|
+
// <kinds count="7">…</kinds>) — emit it as-is, nested in the command.
|
|
134
|
+
const state = evalDynamic(c.dynamicState);
|
|
135
|
+
if (state !== null)
|
|
136
|
+
lines.push(state);
|
|
137
|
+
lines.push('</command>');
|
|
138
|
+
lines.push('');
|
|
139
|
+
}
|
|
140
|
+
// Globals block (footer) — rendered only when globals exist, so an empty
|
|
141
|
+
// list never leaves a bare "Globals" header. -h itself is not a global: the
|
|
142
|
+
// capability-discovery rule below teaches -h usage with its reasoning, so no
|
|
143
|
+
// per-command CTA or standalone "-h: print help" stub is needed.
|
|
144
|
+
if (h.globals.length > 0) {
|
|
145
|
+
lines.push('Globals');
|
|
146
|
+
const gNameW = maxLen(h.globals.map((g) => g.name));
|
|
147
|
+
for (const g of h.globals) {
|
|
148
|
+
lines.push(` ${pad(g.name, gNameW)} ${g.desc}`);
|
|
149
|
+
}
|
|
150
|
+
lines.push('');
|
|
151
|
+
}
|
|
152
|
+
lines.push(IO_CONTRACT);
|
|
153
|
+
lines.push('');
|
|
154
|
+
lines.push(CAPABILITY_DISCOVERY);
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
// renderBranch
|
|
158
|
+
/** Escape a value for a rendered XML attribute. Output is light XML around
|
|
159
|
+
* markdown read as prose by a model, not parsed — so we only guard the
|
|
160
|
+
* double-quote that would visually break the attribute, swapping it for a
|
|
161
|
+
* single quote rather than emitting noisy entities. */
|
|
162
|
+
function attr(s) {
|
|
163
|
+
return s.replace(/"/g, "'");
|
|
164
|
+
}
|
|
165
|
+
/** A branch with more visible children than this lists only its promoted
|
|
166
|
+
* (`common` / `important`) children in its own -h and counts the rest. A
|
|
167
|
+
* plugin that mounts one command per catalog entry (a provider with >1,000
|
|
168
|
+
* services) would otherwise print every one on each `-h`. Unlisted children
|
|
169
|
+
* still route, and their own `-h` still renders. */
|
|
170
|
+
export const WIDE_BRANCH_LISTING_LIMIT = 40;
|
|
171
|
+
/** Split a branch's non-hidden children into the ones its -h lists and the count
|
|
172
|
+
* it leaves out. A branch at or under the limit, or one with no promoted child
|
|
173
|
+
* to fall back on, lists everything. */
|
|
174
|
+
export function collapseWideChildren(visible) {
|
|
175
|
+
if (visible.length <= WIDE_BRANCH_LISTING_LIMIT)
|
|
176
|
+
return { listed: [...visible], unlisted: 0 };
|
|
177
|
+
const promoted = visible.filter((c) => c.tier === 'common' || c.tier === 'important');
|
|
178
|
+
if (promoted.length === 0)
|
|
179
|
+
return { listed: [...visible], unlisted: 0 };
|
|
180
|
+
return { listed: promoted, unlisted: visible.length - promoted.length };
|
|
181
|
+
}
|
|
182
|
+
/** The children of a branch that its parent's (root) -h promotes inline: its
|
|
183
|
+
* `common` / `important` ones. On a wide branch only leaves are promoted, so a
|
|
184
|
+
* promoted service branch shows in the branch's own -h but does not widen the
|
|
185
|
+
* root listing every agent reads. */
|
|
186
|
+
export function rootPromotedChildren(visible) {
|
|
187
|
+
const wide = visible.length > WIDE_BRANCH_LISTING_LIMIT;
|
|
188
|
+
return visible.filter((c) => (c.tier === 'common' || c.tier === 'important') && (!wide || c.kind === 'leaf'));
|
|
189
|
+
}
|
|
190
|
+
export function renderBranch(h) {
|
|
191
|
+
const lines = [];
|
|
192
|
+
// The branch renders as one <command> card: its own description in the
|
|
193
|
+
// opening attribute, then orientation prose / live state, then one
|
|
194
|
+
// self-closing <subcommand> per child. Each child's description + whenToUse
|
|
195
|
+
// are assembled by defineBranch from the child's own self-description, so the
|
|
196
|
+
// parent never restates what a child is — the child owns its representation.
|
|
197
|
+
lines.push(`<command name="${h.name}" description="${attr(h.summary)}">`);
|
|
198
|
+
const branchState = evalDynamic(h.dynamicState);
|
|
199
|
+
if (branchState !== null)
|
|
200
|
+
lines.push(branchState);
|
|
201
|
+
if (h.model !== undefined)
|
|
202
|
+
lines.push(h.model);
|
|
203
|
+
const { listed, unlisted } = collapseWideChildren((h.listing ?? []).filter((c) => c.tier !== 'hidden'));
|
|
204
|
+
for (const c of listed) {
|
|
205
|
+
const plugin = c.plugin === undefined ? '' : ` plugin="${attr(c.plugin)}"`;
|
|
206
|
+
const subs = c.subCount !== undefined && c.subCount > 0 ? ` subcommands="${c.subCount}"` : '';
|
|
207
|
+
// whenToUse plainly states when to reach for this child, rendered verbatim —
|
|
208
|
+
// expansive with examples for judgment-heavy commands, concise for
|
|
209
|
+
// single-purpose ones. It does not restate "read my -h"; the
|
|
210
|
+
// capability-discovery rule in the root footer already teaches that.
|
|
211
|
+
if (lines.length > 1)
|
|
212
|
+
lines.push('');
|
|
213
|
+
lines.push(`<subcommand name="${c.name}" description="${attr(c.description)}" whenToUse="${attr(c.whenToUse)}"${plugin}${subs}/>`);
|
|
214
|
+
}
|
|
215
|
+
if (unlisted > 0) {
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push(`<unlisted-subcommands count="${unlisted}">${unlisted} more subcommands are not listed here. Each one runs by name, and \`crtr ${h.name} <name> -h\` prints its help.</unlisted-subcommands>`);
|
|
218
|
+
}
|
|
219
|
+
if (h.extensionIssue !== undefined) {
|
|
220
|
+
lines.push(`<extension-issue fragment="${attr(h.extensionIssue.fragment)}">This repository's contributed commands were rejected: ${h.extensionIssue.message}. Run \`crtr sys doctor\` for the full report.</extension-issue>`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('</command>');
|
|
223
|
+
return lines.join('\n');
|
|
224
|
+
}
|
|
225
|
+
// renderLeafArgv
|
|
226
|
+
/** Build the display label for a param entry (left column). */
|
|
227
|
+
function paramLabel(p) {
|
|
228
|
+
if (p.kind === 'positional')
|
|
229
|
+
return `${p.name.toUpperCase()}${p.repeatable === true ? '...' : ''}`;
|
|
230
|
+
if (p.kind === 'stdin')
|
|
231
|
+
return 'stdin';
|
|
232
|
+
if (p.kind === 'context-file')
|
|
233
|
+
return '--context-file PATH';
|
|
234
|
+
// flag
|
|
235
|
+
const f = p;
|
|
236
|
+
if (f.type === 'bool')
|
|
237
|
+
return `--${f.name}`;
|
|
238
|
+
return `--${f.name} ${f.name.toUpperCase().replace(/-/g, '_')}`;
|
|
239
|
+
}
|
|
240
|
+
/** Build the description line for a param entry (right column). */
|
|
241
|
+
/** The sentence a `path` param with a declared `encoding` contributes: it is the
|
|
242
|
+
* whole reason the agent can pass a local path instead of pre-encoding bytes,
|
|
243
|
+
* so the leaf's own `-h` must state it. */
|
|
244
|
+
function fileEncodingNote(encoding) {
|
|
245
|
+
if (encoding === undefined)
|
|
246
|
+
return '';
|
|
247
|
+
return encoding === 'base64'
|
|
248
|
+
? ' Local file path: the file is read and its raw bytes are base64-encoded into the request; the path itself is never sent.'
|
|
249
|
+
: ' Local file path: the file is read as UTF-8 text and its content is sent as the value; the path itself is never sent.';
|
|
250
|
+
}
|
|
251
|
+
/** The sentence a param with an ambient env default contributes — the agent must
|
|
252
|
+
* know the value can arrive without being typed, and from where. */
|
|
253
|
+
function envDefaultNote(envVar) {
|
|
254
|
+
return envVar === undefined
|
|
255
|
+
? ''
|
|
256
|
+
: ` Defaults to $${envVar} when that is set and non-empty in the environment, and counts as supplied.`;
|
|
257
|
+
}
|
|
258
|
+
/** The sentence a `file` param contributes: the value names a file, not text. */
|
|
259
|
+
function fileParamNote(type) {
|
|
260
|
+
return type === 'file' ? ' A file: pass its path.' : '';
|
|
261
|
+
}
|
|
262
|
+
function focusedFlagHelpNote(flag, leafName) {
|
|
263
|
+
if (flag.focusedHelp === undefined)
|
|
264
|
+
return '';
|
|
265
|
+
return ` Before using this flag, run \`crtr ${leafName} --${flag.name} -h\` and read its focused guidance.`;
|
|
266
|
+
}
|
|
267
|
+
function paramDesc(p, leafName) {
|
|
268
|
+
const req = p.required ? 'required' : 'optional';
|
|
269
|
+
if (p.kind === 'positional') {
|
|
270
|
+
const repeatable = p.repeatable === true
|
|
271
|
+
? ' Repeatable — pass multiple values to accumulate in argv order.'
|
|
272
|
+
: '';
|
|
273
|
+
return `positional, ${req}.${fileParamNote(p.type)}${fileEncodingNote(p.encoding)}${envDefaultNote(p.defaultFromEnv)} ${p.constraint}${repeatable}`;
|
|
274
|
+
}
|
|
275
|
+
if (p.kind === 'stdin')
|
|
276
|
+
return `${req}. ${p.constraint}`;
|
|
277
|
+
if (p.kind === 'context-file') {
|
|
278
|
+
const shape = p.shape !== undefined
|
|
279
|
+
? ` Shape: ${p.shape}`
|
|
280
|
+
: '';
|
|
281
|
+
return `${req}. Path to a JSON file.${shape} ${p.constraint}`.trim();
|
|
282
|
+
}
|
|
283
|
+
// flag
|
|
284
|
+
const f = p;
|
|
285
|
+
const focusedNote = focusedFlagHelpNote(f, leafName);
|
|
286
|
+
if (f.type === 'bool')
|
|
287
|
+
return `${req} boolean. Presence means true. ${f.constraint}${focusedNote}`.trim();
|
|
288
|
+
const dflt = f.default !== undefined ? ` Default: ${String(f.default)}.` : '';
|
|
289
|
+
const choices = f.type === 'enum' && f.choices !== undefined
|
|
290
|
+
? ` One of: ${f.choices.join(', ')}.`
|
|
291
|
+
: '';
|
|
292
|
+
const repeatable = f.repeatable === true
|
|
293
|
+
? ' Repeatable — pass multiple times to accumulate.'
|
|
294
|
+
: '';
|
|
295
|
+
return `${f.type}, ${req}.${choices}${dflt}${fileParamNote(f.type)}${fileEncodingNote(f.encoding)}${envDefaultNote(f.defaultFromEnv)} ${f.constraint}${repeatable}${focusedNote}`.trim();
|
|
296
|
+
}
|
|
297
|
+
/** One output shape's type phrase: `array of <element>` for an array with a
|
|
298
|
+
* declared element shape, the declared type otherwise. */
|
|
299
|
+
function outputTypePhrase(shape) {
|
|
300
|
+
if (shape.items === undefined)
|
|
301
|
+
return shape.type;
|
|
302
|
+
const nullable = /\|\s*null\s*$/i.test(shape.type) ? ', or null' : '';
|
|
303
|
+
return `array of ${outputTypePhrase(shape.items)}${nullable}`;
|
|
304
|
+
}
|
|
305
|
+
/** The values sentence of an enum shape, or nothing. */
|
|
306
|
+
function outputValuesNote(shape) {
|
|
307
|
+
return shape.values !== undefined ? ` One of: ${shape.values.join(', ')}.` : '';
|
|
308
|
+
}
|
|
309
|
+
/** Render output fields as rows, then each structural field's shape beneath
|
|
310
|
+
* it, one indent deeper: an object's children, an array's element children
|
|
311
|
+
* and enum values, so the result's shape reads straight from `-h`. */
|
|
312
|
+
function renderOutputFields(fields, indent, lines) {
|
|
313
|
+
const nameW = maxLen(fields.map((f) => f.name));
|
|
314
|
+
for (const f of fields) {
|
|
315
|
+
const constraint = f.constraint.length > 0 ? ` ${f.constraint}` : '';
|
|
316
|
+
lines.push(`${indent}${pad(f.name, nameW)} ${outputTypePhrase(f)}.${outputValuesNote(f)}${constraint}`);
|
|
317
|
+
renderOutputShapeBody(f, `${indent} `, lines);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** The nested part of a shape: object children, or an array element's shape. */
|
|
321
|
+
function renderOutputShapeBody(shape, indent, lines) {
|
|
322
|
+
if (shape.children !== undefined && shape.children.length > 0) {
|
|
323
|
+
renderOutputFields(shape.children, indent, lines);
|
|
324
|
+
}
|
|
325
|
+
let item = shape.items;
|
|
326
|
+
while (item !== undefined) {
|
|
327
|
+
const note = outputValuesNote(item);
|
|
328
|
+
if (item.constraint.length > 0 || note.length > 0) {
|
|
329
|
+
lines.push(`${indent}each element: ${outputTypePhrase(item)}.${note}${item.constraint.length > 0 ? ` ${item.constraint}` : ''}`);
|
|
330
|
+
}
|
|
331
|
+
if (item.children !== undefined && item.children.length > 0) {
|
|
332
|
+
renderOutputFields(item.children, indent, lines);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
item = item.items;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** Render one leaf's ordinary contract, or the focused continuation for one
|
|
339
|
+
* uncommon flag selected through `--flag -h`. The caller reaches focused help
|
|
340
|
+
* from the ordinary row, so that view contains only leaf identity and the new
|
|
341
|
+
* parameter detail instead of repeating information already in context. */
|
|
342
|
+
export function renderLeafArgv(h, focusedFlag) {
|
|
343
|
+
const lines = [`${h.name}: ${h.summary}.`];
|
|
344
|
+
// Hidden flags are parsed but never advertised in -h (see FlagParam.hidden).
|
|
345
|
+
const params = (h.params ?? []).filter((p) => !(p.kind === 'flag' && p.hidden === true));
|
|
346
|
+
const focused = focusedFlag === undefined
|
|
347
|
+
? undefined
|
|
348
|
+
: params.find((p) => p.kind === 'flag' && p.name === focusedFlag && p.focusedHelp !== undefined);
|
|
349
|
+
if (focused?.focusedHelp !== undefined) {
|
|
350
|
+
const detail = focused.focusedHelp;
|
|
351
|
+
lines.push('');
|
|
352
|
+
lines.push(`<parameter name="${focused.name}">`);
|
|
353
|
+
lines.push('When to use');
|
|
354
|
+
lines.push(` ${detail.whenToUse}`);
|
|
355
|
+
lines.push('');
|
|
356
|
+
lines.push('Value');
|
|
357
|
+
lines.push(` ${detail.value}`);
|
|
358
|
+
if ((detail.effects ?? []).length > 0) {
|
|
359
|
+
lines.push('');
|
|
360
|
+
lines.push('Effects');
|
|
361
|
+
for (const effect of detail.effects ?? [])
|
|
362
|
+
lines.push(` ${effect}`);
|
|
363
|
+
}
|
|
364
|
+
const focusedState = evalDynamic(detail.dynamicState);
|
|
365
|
+
if (focusedState !== null) {
|
|
366
|
+
lines.push('');
|
|
367
|
+
lines.push(focusedState);
|
|
368
|
+
}
|
|
369
|
+
lines.push('</parameter>');
|
|
370
|
+
return lines.join('\n');
|
|
371
|
+
}
|
|
372
|
+
if (h.guide !== undefined) {
|
|
373
|
+
lines.push('');
|
|
374
|
+
lines.push(h.guide);
|
|
375
|
+
}
|
|
376
|
+
lines.push('');
|
|
377
|
+
if (params.length > 0) {
|
|
378
|
+
lines.push('Input');
|
|
379
|
+
const labels = params.map(paramLabel);
|
|
380
|
+
const colW = maxLen(labels);
|
|
381
|
+
for (let i = 0; i < params.length; i++) {
|
|
382
|
+
lines.push(` ${pad(labels[i], colW)} ${paramDesc(params[i], h.name)}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
lines.push(h.inputNote !== undefined ? h.inputNote : 'No input parameters.');
|
|
387
|
+
}
|
|
388
|
+
lines.push('');
|
|
389
|
+
// The result is rendered as instruction-shaped XML+markdown; these fields are
|
|
390
|
+
// the information it carries, in order, not a literal JSON shape.
|
|
391
|
+
lines.push('Output (fields carried in the rendered result)');
|
|
392
|
+
renderOutputFields(h.output, ' ', lines);
|
|
393
|
+
lines.push('');
|
|
394
|
+
lines.push('Effects');
|
|
395
|
+
for (const e of h.effects) {
|
|
396
|
+
lines.push(` ${e}`);
|
|
397
|
+
}
|
|
398
|
+
// Optional bounded runtime-state block (e.g. the live <kinds> list), appended
|
|
399
|
+
// after the schema. Soft-fails to omission on null/throw, mirroring renderBranch.
|
|
400
|
+
const state = evalDynamic(h.dynamicState);
|
|
401
|
+
if (state !== null) {
|
|
402
|
+
lines.push('');
|
|
403
|
+
lines.push(state);
|
|
404
|
+
}
|
|
405
|
+
return lines.join('\n');
|
|
406
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PageComponentRegistration, ProductPageComponents } from '../../types.js';
|
|
2
|
+
/** The page component kinds crtr validates and renders itself. */
|
|
3
|
+
export declare const BUILTIN_PAGE_KINDS: readonly ["options", "text", "table", "cards", "chart"];
|
|
4
|
+
export declare const STRUCTURAL_PAGE_KINDS: readonly ["page", "step"];
|
|
5
|
+
export interface PageComponentCatalogEntry {
|
|
6
|
+
kind: string;
|
|
7
|
+
tag: string;
|
|
8
|
+
description: string;
|
|
9
|
+
useWhen: string;
|
|
10
|
+
doc?: string;
|
|
11
|
+
builtin: boolean;
|
|
12
|
+
structural: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** The JSX component name a registered kind is authored as. Registry names reach page
|
|
15
|
+
* code as JS identifiers, so a hyphenated kind is authored in its PascalCase form
|
|
16
|
+
* (`worker-card` → `<WorkerCard>`); the registered kind string is what travels on
|
|
17
|
+
* the wire. */
|
|
18
|
+
export declare function pageComponentTag(kind: string): string;
|
|
19
|
+
/** The built-in component kind each authored JSX component name maps to. */
|
|
20
|
+
export declare const BUILTIN_KIND_BY_TAG: Readonly<Record<string, string>>;
|
|
21
|
+
/** Every JSX identifier reserved by the built-in page registry, including structure. */
|
|
22
|
+
export declare const BUILTIN_PAGE_COMPONENT_TAGS: ReadonlySet<string>;
|
|
23
|
+
/** The shadcn display set a page may compose with. These carry no response and never
|
|
24
|
+
* appear in the page manifest; they are in scope so an authored page can lay its
|
|
25
|
+
* content out. */
|
|
26
|
+
export declare const DISPLAY_PAGE_COMPONENT_TAGS: ReadonlySet<string>;
|
|
27
|
+
/** Normalize legacy kind strings and documented product component registrations.
|
|
28
|
+
* `label` names the source in every error message — the default is a scope
|
|
29
|
+
* `config.json` block; a plugin contribution passes its own plugin-named
|
|
30
|
+
* label so a bad registration points at the package that shipped it. */
|
|
31
|
+
export declare function normalizePageComponents(raw: unknown, label?: string): PageComponentRegistration[];
|
|
32
|
+
/** One contributing source of product page components: a scope's own
|
|
33
|
+
* `config.json` block, or one installed plugin's manifest contributions. */
|
|
34
|
+
export interface PageComponentLayer {
|
|
35
|
+
/** How this source is named in a collision error, e.g. `plugin "acme"`. */
|
|
36
|
+
origin: string;
|
|
37
|
+
components: readonly PageComponentRegistration[];
|
|
38
|
+
}
|
|
39
|
+
/** Concatenate every contributing layer into one active catalog, rejecting a
|
|
40
|
+
* kind — or a derived JSX tag — that two sources both register. There is no
|
|
41
|
+
* precedence here on purpose: two sources claiming one component kind is an
|
|
42
|
+
* authoring defect, and silently letting either win would ship a page component
|
|
43
|
+
* bound to a renderer nobody chose. Each layer's own entries are already
|
|
44
|
+
* validated by `normalizePageComponents`; this is the cross-source gate. */
|
|
45
|
+
export declare function mergePageComponentLayers(layers: readonly PageComponentLayer[]): PageComponentRegistration[];
|
|
46
|
+
/** STRICT install-time validation of a plugin-declared `page_components` block
|
|
47
|
+
* — the loud counterpart to a read path that must never ship a half-valid
|
|
48
|
+
* catalog. Returns one human-readable reason per defect; empty = valid. Used
|
|
49
|
+
* by the archive-bundle validator and by source plugin installs. */
|
|
50
|
+
export declare function invalidPageComponentsReasons(raw: unknown): string[];
|
|
51
|
+
/** Names-only projection for page validation and rendering consumers. */
|
|
52
|
+
export declare function pageComponentKinds(components: readonly PageComponentRegistration[]): string[];
|
|
53
|
+
/** Full built-in + product catalog for CLI discovery. */
|
|
54
|
+
export declare function pageComponentCatalog(components: readonly PageComponentRegistration[]): PageComponentCatalogEntry[];
|
|
55
|
+
/** Whether crtr or the product catalog registers a page component kind. */
|
|
56
|
+
export declare function productPageComponent(kind: string, components: ProductPageComponents): PageComponentRegistration | undefined;
|
|
57
|
+
export declare function isRegisteredPageKind(kind: string, productComponents: ProductPageComponents): boolean;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/** The page component kinds crtr validates and renders itself. */
|
|
2
|
+
export const BUILTIN_PAGE_KINDS = ['options', 'text', 'table', 'cards', 'chart'];
|
|
3
|
+
export const STRUCTURAL_PAGE_KINDS = ['page', 'step'];
|
|
4
|
+
const PAGE_KIND_PATTERN = /^[a-z][a-z0-9-]{0,63}$/;
|
|
5
|
+
const BUILTIN_PAGE_KIND_SET = new Set(BUILTIN_PAGE_KINDS);
|
|
6
|
+
const BUILTIN_COMPONENTS = [
|
|
7
|
+
{ kind: 'page', tag: 'Page', description: 'the page itself: title, subtitle, and every step it contains', useWhen: 'use as the single root element every page returns', builtin: true, structural: true },
|
|
8
|
+
{ kind: 'step', tag: 'Step', description: 'one step inside a Page', useWhen: 'use for each step the user sees and completes in sequence', builtin: true, structural: true },
|
|
9
|
+
{ kind: 'options', tag: 'UserQuestion', description: 'known alternatives with single or multiple selection', useWhen: 'use when the user picks among known alternatives, optionally with freetext', builtin: true, structural: false },
|
|
10
|
+
{ kind: 'text', tag: 'UserText', description: 'writing surface whose answer is the text the user hands back', useWhen: 'use when you need prose from the user — a value they write, or a draft they revise; prose they only read is a plain element', builtin: true, structural: false },
|
|
11
|
+
{ kind: 'table', tag: 'UserTable', description: 'rows and columns with optional selection', useWhen: 'use when structured records are easiest to compare in rows and columns', builtin: true, structural: false },
|
|
12
|
+
{ kind: 'cards', tag: 'UserCards', description: 'visual records with optional selection', useWhen: 'use when the user compares or picks richer items than a compact option list can carry', builtin: true, structural: false },
|
|
13
|
+
{ kind: 'chart', tag: 'Chart', description: 'line, bar, or area data visualization', useWhen: 'use when shape, trend, or magnitude matters more than exact tabular values', builtin: true, structural: false },
|
|
14
|
+
];
|
|
15
|
+
/** The JSX component name a registered kind is authored as. Registry names reach page
|
|
16
|
+
* code as JS identifiers, so a hyphenated kind is authored in its PascalCase form
|
|
17
|
+
* (`worker-card` → `<WorkerCard>`); the registered kind string is what travels on
|
|
18
|
+
* the wire. */
|
|
19
|
+
export function pageComponentTag(kind) {
|
|
20
|
+
return kind.split('-').map((part) => (part === '' ? '' : `${part[0].toUpperCase()}${part.slice(1)}`)).join('');
|
|
21
|
+
}
|
|
22
|
+
/** The built-in component kind each authored JSX component name maps to. */
|
|
23
|
+
export const BUILTIN_KIND_BY_TAG = Object.fromEntries(BUILTIN_COMPONENTS.filter((entry) => !entry.structural).map((entry) => [entry.tag, entry.kind]));
|
|
24
|
+
/** Every JSX identifier reserved by the built-in page registry, including structure. */
|
|
25
|
+
export const BUILTIN_PAGE_COMPONENT_TAGS = new Set(BUILTIN_COMPONENTS.map((entry) => entry.tag));
|
|
26
|
+
/** The shadcn display set a page may compose with. These carry no response and never
|
|
27
|
+
* appear in the page manifest; they are in scope so an authored page can lay its
|
|
28
|
+
* content out. */
|
|
29
|
+
export const DISPLAY_PAGE_COMPONENT_TAGS = new Set([
|
|
30
|
+
'Card', 'CardHeader', 'CardTitle', 'CardDescription', 'CardContent', 'CardFooter', 'CardAction',
|
|
31
|
+
'Badge', 'Button', 'Separator', 'Progress', 'Alert', 'AlertTitle', 'AlertDescription',
|
|
32
|
+
'Table', 'TableHeader', 'TableBody', 'TableFooter', 'TableRow', 'TableHead', 'TableCell', 'TableCaption',
|
|
33
|
+
'Tabs', 'TabsList', 'TabsTrigger', 'TabsContent',
|
|
34
|
+
'Accordion', 'AccordionItem', 'AccordionTrigger', 'AccordionContent',
|
|
35
|
+
]);
|
|
36
|
+
function describeEntry(entry) {
|
|
37
|
+
try {
|
|
38
|
+
const json = JSON.stringify(entry);
|
|
39
|
+
return json === undefined ? String(entry) : json;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return String(entry);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function optionalText(value, field, label, index) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
49
|
+
throw new Error(`${label}[${index}].${field} must be a non-empty string; received ${describeEntry(value)}`);
|
|
50
|
+
}
|
|
51
|
+
return value.trim();
|
|
52
|
+
}
|
|
53
|
+
/** Normalize legacy kind strings and documented product component registrations.
|
|
54
|
+
* `label` names the source in every error message — the default is a scope
|
|
55
|
+
* `config.json` block; a plugin contribution passes its own plugin-named
|
|
56
|
+
* label so a bad registration points at the package that shipped it. */
|
|
57
|
+
export function normalizePageComponents(raw, label = 'page_components') {
|
|
58
|
+
if (raw === undefined)
|
|
59
|
+
return [];
|
|
60
|
+
if (!Array.isArray(raw))
|
|
61
|
+
throw new Error(`${label} must be an array; received ${describeEntry(raw)}`);
|
|
62
|
+
const seenKinds = new Set();
|
|
63
|
+
const seenTags = new Set();
|
|
64
|
+
return raw.map((entry, index) => {
|
|
65
|
+
let registration;
|
|
66
|
+
if (typeof entry === 'string') {
|
|
67
|
+
registration = { kind: entry.trim() };
|
|
68
|
+
}
|
|
69
|
+
else if (entry !== null && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
70
|
+
const object = entry;
|
|
71
|
+
const extra = Object.keys(object).filter((key) => !['kind', 'description', 'useWhen', 'doc', 'display'].includes(key));
|
|
72
|
+
if (extra.length > 0)
|
|
73
|
+
throw new Error(`${label}[${index}] has unknown field(s): ${extra.join(', ')}`);
|
|
74
|
+
if (typeof object['kind'] !== 'string')
|
|
75
|
+
throw new Error(`${label}[${index}].kind must be a string; offending entry: ${describeEntry(entry)}`);
|
|
76
|
+
if (object['display'] !== undefined && typeof object['display'] !== 'boolean')
|
|
77
|
+
throw new Error(`${label}[${index}].display must be a boolean; offending entry: ${describeEntry(entry)}`);
|
|
78
|
+
registration = {
|
|
79
|
+
kind: object['kind'].trim(),
|
|
80
|
+
...(optionalText(object['description'], 'description', label, index) === undefined ? {} : { description: optionalText(object['description'], 'description', label, index) }),
|
|
81
|
+
...(optionalText(object['useWhen'], 'useWhen', label, index) === undefined ? {} : { useWhen: optionalText(object['useWhen'], 'useWhen', label, index) }),
|
|
82
|
+
...(optionalText(object['doc'], 'doc', label, index) === undefined ? {} : { doc: optionalText(object['doc'], 'doc', label, index) }),
|
|
83
|
+
...(object['display'] === true ? { display: true } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
throw new Error(`${label}[${index}] must be a string or component object; offending entry: ${describeEntry(entry)}`);
|
|
88
|
+
}
|
|
89
|
+
const kind = registration.kind;
|
|
90
|
+
if (!PAGE_KIND_PATTERN.test(kind))
|
|
91
|
+
throw new Error(`${label}[${index}] has invalid page kind ${describeEntry(kind)}; expected /^[a-z][a-z0-9-]{0,63}$/`);
|
|
92
|
+
if (BUILTIN_PAGE_KIND_SET.has(kind) || STRUCTURAL_PAGE_KINDS.includes(kind))
|
|
93
|
+
throw new Error(`${label}[${index}] collides with built-in page kind "${kind}"`);
|
|
94
|
+
if (seenKinds.has(kind))
|
|
95
|
+
throw new Error(`${label}[${index}] duplicates page kind "${kind}"`);
|
|
96
|
+
const tag = pageComponentTag(kind);
|
|
97
|
+
if (BUILTIN_PAGE_COMPONENT_TAGS.has(tag))
|
|
98
|
+
throw new Error(`${label}[${index}] kind "${kind}" derives JSX tag "${tag}", which collides with a built-in page component`);
|
|
99
|
+
if (seenTags.has(tag))
|
|
100
|
+
throw new Error(`${label}[${index}] kind "${kind}" derives JSX tag "${tag}", which duplicates another product page component`);
|
|
101
|
+
seenKinds.add(kind);
|
|
102
|
+
seenTags.add(tag);
|
|
103
|
+
return registration;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Concatenate every contributing layer into one active catalog, rejecting a
|
|
107
|
+
* kind — or a derived JSX tag — that two sources both register. There is no
|
|
108
|
+
* precedence here on purpose: two sources claiming one component kind is an
|
|
109
|
+
* authoring defect, and silently letting either win would ship a page component
|
|
110
|
+
* bound to a renderer nobody chose. Each layer's own entries are already
|
|
111
|
+
* validated by `normalizePageComponents`; this is the cross-source gate. */
|
|
112
|
+
export function mergePageComponentLayers(layers) {
|
|
113
|
+
const originByKind = new Map();
|
|
114
|
+
const claimByTag = new Map();
|
|
115
|
+
const out = [];
|
|
116
|
+
for (const layer of layers) {
|
|
117
|
+
for (const component of layer.components) {
|
|
118
|
+
const kindOrigin = originByKind.get(component.kind);
|
|
119
|
+
if (kindOrigin !== undefined) {
|
|
120
|
+
throw new Error(`page component kind "${component.kind}" is registered by both ${kindOrigin} and ${layer.origin}`);
|
|
121
|
+
}
|
|
122
|
+
const tag = pageComponentTag(component.kind);
|
|
123
|
+
const tagClaim = claimByTag.get(tag);
|
|
124
|
+
if (tagClaim !== undefined) {
|
|
125
|
+
throw new Error(`page component kind "${component.kind}" (${layer.origin}) derives JSX tag "${tag}", which duplicates kind "${tagClaim.kind}" from ${tagClaim.origin}`);
|
|
126
|
+
}
|
|
127
|
+
originByKind.set(component.kind, layer.origin);
|
|
128
|
+
claimByTag.set(tag, { kind: component.kind, origin: layer.origin });
|
|
129
|
+
out.push(component);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** STRICT install-time validation of a plugin-declared `page_components` block
|
|
135
|
+
* — the loud counterpart to a read path that must never ship a half-valid
|
|
136
|
+
* catalog. Returns one human-readable reason per defect; empty = valid. Used
|
|
137
|
+
* by the archive-bundle validator and by source plugin installs. */
|
|
138
|
+
export function invalidPageComponentsReasons(raw) {
|
|
139
|
+
try {
|
|
140
|
+
normalizePageComponents(raw);
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Names-only projection for page validation and rendering consumers. */
|
|
148
|
+
export function pageComponentKinds(components) {
|
|
149
|
+
return components.map((component) => component.kind);
|
|
150
|
+
}
|
|
151
|
+
/** Full built-in + product catalog for CLI discovery. */
|
|
152
|
+
export function pageComponentCatalog(components) {
|
|
153
|
+
return [
|
|
154
|
+
...BUILTIN_COMPONENTS,
|
|
155
|
+
...components.map((component) => ({
|
|
156
|
+
kind: component.kind,
|
|
157
|
+
tag: pageComponentTag(component.kind),
|
|
158
|
+
description: component.description ?? 'product-registered page component',
|
|
159
|
+
useWhen: component.useWhen ?? 'use when the product component matches the structured UI the user needs',
|
|
160
|
+
...(component.doc === undefined ? {} : { doc: component.doc }),
|
|
161
|
+
builtin: false,
|
|
162
|
+
structural: false,
|
|
163
|
+
})),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
/** Whether crtr or the product catalog registers a page component kind. */
|
|
167
|
+
export function productPageComponent(kind, components) {
|
|
168
|
+
return components.find((component) => component.kind === kind);
|
|
169
|
+
}
|
|
170
|
+
export function isRegisteredPageKind(kind, productComponents) {
|
|
171
|
+
return BUILTIN_PAGE_KIND_SET.has(kind) || productPageComponent(kind, productComponents) !== undefined;
|
|
172
|
+
}
|