@crouter/api 0.3.387 → 0.3.388

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 (130) hide show
  1. package/dist/api/__tests__/integration/client.test.js +97 -0
  2. package/dist/api/client.d.ts +7 -0
  3. package/dist/api/client.js +40 -21
  4. package/dist/core/asset-root.d.ts +7 -0
  5. package/dist/core/asset-root.js +18 -0
  6. package/dist/core/canvas/boot-id.d.ts +6 -0
  7. package/dist/core/canvas/boot-id.js +26 -0
  8. package/dist/core/canvas/paths.d.ts +72 -0
  9. package/dist/core/canvas/paths.js +163 -0
  10. package/dist/core/canvas/pid.d.ts +391 -0
  11. package/dist/core/canvas/pid.js +948 -0
  12. package/dist/core/command-plugins/bundle.d.ts +149 -0
  13. package/dist/core/command-plugins/bundle.js +588 -0
  14. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  15. package/dist/core/command-plugins/endpoint.js +51 -0
  16. package/dist/core/config.d.ts +233 -0
  17. package/dist/core/config.js +1120 -0
  18. package/dist/core/env-name.d.ts +6 -0
  19. package/dist/core/env-name.js +9 -0
  20. package/dist/core/errors.d.ts +38 -0
  21. package/dist/core/errors.js +90 -0
  22. package/dist/core/events/emit.d.ts +6 -0
  23. package/dist/core/events/emit.js +42 -0
  24. package/dist/core/events/envelope.d.ts +2 -0
  25. package/dist/core/events/envelope.js +84 -0
  26. package/dist/core/events/errors.d.ts +4 -0
  27. package/dist/core/events/errors.js +69 -0
  28. package/dist/core/events/operation-id.d.ts +4 -0
  29. package/dist/core/events/operation-id.js +24 -0
  30. package/dist/core/events/serialize.d.ts +4 -0
  31. package/dist/core/events/serialize.js +199 -0
  32. package/dist/core/events/source.d.ts +16 -0
  33. package/dist/core/events/source.js +31 -0
  34. package/dist/core/events/types.d.ts +68 -0
  35. package/dist/core/events/types.js +11 -0
  36. package/dist/core/exclusive-lock.d.ts +34 -0
  37. package/dist/core/exclusive-lock.js +197 -0
  38. package/dist/core/fs-utils.d.ts +44 -0
  39. package/dist/core/fs-utils.js +208 -0
  40. package/dist/core/help.d.ts +309 -0
  41. package/dist/core/help.js +406 -0
  42. package/dist/core/human/page-catalog.d.ts +57 -0
  43. package/dist/core/human/page-catalog.js +172 -0
  44. package/dist/core/installed-plugins.d.ts +2 -0
  45. package/dist/core/installed-plugins.js +79 -0
  46. package/dist/core/io.d.ts +122 -0
  47. package/dist/core/io.js +373 -0
  48. package/dist/core/keybindings/attach-control.d.ts +49 -0
  49. package/dist/core/keybindings/attach-control.js +42 -0
  50. package/dist/core/keybindings/catalog.d.ts +18 -0
  51. package/dist/core/keybindings/catalog.js +257 -0
  52. package/dist/core/keybindings/types.d.ts +42 -0
  53. package/dist/core/keybindings/types.js +1 -0
  54. package/dist/core/layout.d.ts +26 -0
  55. package/dist/core/layout.js +94 -0
  56. package/dist/core/locked-file.d.ts +27 -0
  57. package/dist/core/locked-file.js +118 -0
  58. package/dist/core/log.d.ts +9 -0
  59. package/dist/core/log.js +89 -0
  60. package/dist/core/manifest.d.ts +5 -0
  61. package/dist/core/manifest.js +15 -0
  62. package/dist/core/plugin-env.d.ts +8 -0
  63. package/dist/core/plugin-env.js +31 -0
  64. package/dist/core/plugin-extensions.d.ts +29 -0
  65. package/dist/core/plugin-extensions.js +191 -0
  66. package/dist/core/plugin-swap-lock.d.ts +9 -0
  67. package/dist/core/plugin-swap-lock.js +31 -0
  68. package/dist/core/preview-result-path.d.ts +4 -0
  69. package/dist/core/preview-result-path.js +26 -0
  70. package/dist/core/profiles/env-store.d.ts +22 -0
  71. package/dist/core/profiles/env-store.js +163 -0
  72. package/dist/core/profiles/fuzzy-match.d.ts +19 -0
  73. package/dist/core/profiles/fuzzy-match.js +92 -0
  74. package/dist/core/profiles/manifest.d.ts +120 -0
  75. package/dist/core/profiles/manifest.js +529 -0
  76. package/dist/core/rate-limit-scope.d.ts +25 -0
  77. package/dist/core/rate-limit-scope.js +64 -0
  78. package/dist/core/render.d.ts +12 -0
  79. package/dist/core/render.js +138 -0
  80. package/dist/core/resolver.d.ts +14 -0
  81. package/dist/core/resolver.js +111 -0
  82. package/dist/core/runtime/branded-host.d.ts +25 -0
  83. package/dist/core/runtime/branded-host.js +264 -0
  84. package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
  85. package/dist/core/runtime/broker/daemon-ops.js +177 -0
  86. package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
  87. package/dist/core/runtime/broker/signal-stream.js +149 -0
  88. package/dist/core/scope.d.ts +32 -0
  89. package/dist/core/scope.js +184 -0
  90. package/dist/core/scoped-state/db.d.ts +17 -0
  91. package/dist/core/scoped-state/db.js +247 -0
  92. package/dist/core/scoped-state/migrate.d.ts +8 -0
  93. package/dist/core/scoped-state/migrate.js +187 -0
  94. package/dist/core/scoped-state/paths.d.ts +9 -0
  95. package/dist/core/scoped-state/paths.js +27 -0
  96. package/dist/core/scoped-state/profiles.d.ts +27 -0
  97. package/dist/core/scoped-state/profiles.js +93 -0
  98. package/dist/core/scoped-state/providers.d.ts +24 -0
  99. package/dist/core/scoped-state/providers.js +19 -0
  100. package/dist/core/scoped-state/schema.d.ts +6 -0
  101. package/dist/core/scoped-state/schema.js +43 -0
  102. package/dist/core/scoped-state/settings.d.ts +28 -0
  103. package/dist/core/scoped-state/settings.js +83 -0
  104. package/dist/core/spaces/open-beneath.d.ts +71 -0
  105. package/dist/core/spaces/open-beneath.js +581 -0
  106. package/dist/core/sqlite-statements.d.ts +4 -0
  107. package/dist/core/sqlite-statements.js +17 -0
  108. package/dist/core/subscription-state.d.ts +121 -0
  109. package/dist/core/subscription-state.js +287 -0
  110. package/dist/core/user-settings.d.ts +377 -0
  111. package/dist/core/user-settings.js +458 -0
  112. package/dist/daemon/broker-signals/bus.d.ts +30 -0
  113. package/dist/daemon/broker-signals/bus.js +87 -0
  114. package/dist/daemon/manage.d.ts +176 -0
  115. package/dist/daemon/manage.js +664 -0
  116. package/dist/daemon/pidfile.d.ts +8 -0
  117. package/dist/daemon/pidfile.js +37 -0
  118. package/dist/daemon/startup-policy.d.ts +1 -0
  119. package/dist/daemon/startup-policy.js +1 -0
  120. package/dist/native/linux.d.ts +29 -0
  121. package/dist/native/linux.js +20 -0
  122. package/dist/shared/env.d.ts +116 -0
  123. package/dist/shared/env.js +271 -0
  124. package/dist/shared/inbox-entry-body.d.ts +22 -0
  125. package/dist/shared/inbox-entry-body.js +116 -0
  126. package/dist/shared/working-activity.d.ts +9 -0
  127. package/dist/shared/working-activity.js +27 -0
  128. package/dist/types.d.ts +562 -0
  129. package/dist/types.js +186 -0
  130. 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
+ }
@@ -0,0 +1,2 @@
1
+ import type { InstalledPlugin, Scope } from '../types.js';
2
+ export declare function listInstalledPluginsInRoot(scope: Scope, scopeRootPath: string): InstalledPlugin[];