@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4

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 (188) hide show
  1. package/README.md +2 -1
  2. package/api/build/build.type.d.mts +2 -2
  3. package/api/build/build.type.mjs +2 -2
  4. package/api/component/component.type.d.mts +6 -6
  5. package/api/component/component.type.mjs +19 -19
  6. package/api/discover/discover.type.d.mts +4 -4
  7. package/api/discover/discover.type.mjs +10 -10
  8. package/api/docs/_adapter.d.mts +37 -24
  9. package/api/docs/_adapter.mjs +169 -83
  10. package/api/docs/compiled-topics.test.mjs +78 -0
  11. package/api/docs/detail/detail.mjs +14 -63
  12. package/api/docs/detail/section/section.d.mts +1 -1
  13. package/api/docs/detail/section/section.mjs +44 -20
  14. package/api/docs/detail/section/section.test.mjs +41 -0
  15. package/api/docs/docs.d.mts +7 -2
  16. package/api/docs/docs.doc.mjs +27 -10
  17. package/api/docs/docs.mjs +16 -9
  18. package/api/docs/docs.test.mjs +6 -0
  19. package/api/docs/docs.type.d.mts +40 -3
  20. package/api/docs/docs.type.mjs +36 -8
  21. package/api/docs/index/index.d.mts +18 -0
  22. package/api/docs/index/index.mjs +32 -0
  23. package/api/docs/index/index.test.mjs +62 -0
  24. package/api/docs/integrationDocs.test.mjs +106 -0
  25. package/api/doctor/doctor.d.mts +48 -0
  26. package/api/doctor/doctor.mjs +232 -0
  27. package/api/doctor/doctor.test.mjs +196 -0
  28. package/api/hook/hook.type.d.mts +3 -3
  29. package/api/hook/hook.type.mjs +11 -11
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.mjs +5 -3
  32. package/api/integration/add-contribution.test.mjs +4 -4
  33. package/api/integration/integration-authoring.type.d.mts +1 -1
  34. package/api/integration/pack-check.mjs +49 -7
  35. package/api/integration/pack-check.test.mjs +249 -0
  36. package/api/search/search.d.mts +1 -1
  37. package/api/search/search.mjs +5 -5
  38. package/api/search/search.type.d.mts +2 -2
  39. package/api/search/search.type.mjs +1 -1
  40. package/api/swizzle/swizzle.type.d.mts +2 -2
  41. package/api/swizzle/swizzle.type.mjs +2 -2
  42. package/api/template/template.d.mts +1 -1
  43. package/api/template/template.type.d.mts +6 -6
  44. package/api/template/template.type.mjs +12 -12
  45. package/api/theme/build/build.mjs +20 -6
  46. package/api/theme/build/build.test.mjs +127 -0
  47. package/api/theme/palette/generate/generate.mjs +1 -1
  48. package/api/theme/palette/generate/generator.d.mts +10 -13
  49. package/api/theme/palette/generate/generator.mjs +7 -3
  50. package/api/theme/theme.type.d.mts +170 -11
  51. package/api/theme/theme.type.mjs +94 -27
  52. package/api/upgrade/_adapter.mjs +71 -5
  53. package/api/upgrade/project-context.test.mjs +272 -0
  54. package/api/upgrade/upgrade.doc.mjs +4 -3
  55. package/api/upgrade/upgrade.type.d.mts +5 -5
  56. package/api/upgrade/upgrade.type.mjs +11 -11
  57. package/assets/codemods/integration-discovery.mjs +40 -2
  58. package/assets/codemods/integration-discovery.test.mjs +58 -0
  59. package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
  60. package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
  61. package/assets/docs/README.md +9 -0
  62. package/assets/docs/authoring.doc.mjs +14 -0
  63. package/assets/docs/cli-integrations.doc.mjs +86 -15
  64. package/assets/docs/styling-libraries.doc.mjs +1 -1
  65. package/assets/docs/working-with-ai.doc.mjs +1 -1
  66. package/authoring/_shared/contract.ts +22 -0
  67. package/authoring/codemod/codemod.doc.mjs +6 -1
  68. package/authoring/codemod/parse.d.mts +8 -8
  69. package/authoring/codemod/parse.mjs +8 -6
  70. package/authoring/config/parse.d.mts +13 -13
  71. package/authoring/config/parse.mjs +8 -8
  72. package/authoring/config/type.ts +3 -3
  73. package/authoring/debug/parse.d.mts +5 -5
  74. package/authoring/debug/parse.mjs +3 -3
  75. package/authoring/doctypes/_schema.d.mts +788 -23
  76. package/authoring/doctypes/_schema.mjs +492 -39
  77. package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
  78. package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
  79. package/authoring/doctypes/base/type.ts +40 -0
  80. package/authoring/doctypes/command/command.doc.mjs +3 -2
  81. package/authoring/doctypes/command/parse.d.mts +2 -2
  82. package/authoring/doctypes/command/parse.mjs +1 -1
  83. package/authoring/doctypes/command/type.ts +3 -2
  84. package/authoring/doctypes/component/component.doc.mjs +6 -3
  85. package/authoring/doctypes/component/parse.d.mts +2 -2
  86. package/authoring/doctypes/component/parse.mjs +1 -1
  87. package/authoring/doctypes/component/type.ts +4 -3
  88. package/authoring/doctypes/enum/parse.d.mts +2 -2
  89. package/authoring/doctypes/enum/parse.mjs +1 -1
  90. package/authoring/doctypes/enum/type.ts +3 -1
  91. package/authoring/doctypes/function/function.doc.mjs +4 -0
  92. package/authoring/doctypes/function/parse.d.mts +2 -2
  93. package/authoring/doctypes/function/parse.mjs +1 -1
  94. package/authoring/doctypes/function/type.ts +6 -2
  95. package/authoring/doctypes/hook/hook.doc.mjs +4 -0
  96. package/authoring/doctypes/hook/parse.d.mts +2 -2
  97. package/authoring/doctypes/hook/parse.mjs +1 -1
  98. package/authoring/doctypes/hook/type.ts +3 -2
  99. package/authoring/doctypes/legacy.d.mts +8 -6
  100. package/authoring/doctypes/legacy.mjs +5 -4
  101. package/authoring/doctypes/load-contract.test.mjs +207 -0
  102. package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
  103. package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
  104. package/authoring/doctypes/namespace/parse.d.mts +12 -0
  105. package/authoring/doctypes/namespace/parse.mjs +25 -0
  106. package/authoring/doctypes/namespace/parse.test.mjs +165 -0
  107. package/authoring/doctypes/namespace/type.ts +71 -0
  108. package/authoring/doctypes/parse.d.mts +20 -18
  109. package/authoring/doctypes/parse.mjs +16 -10
  110. package/authoring/doctypes/parse.test.mjs +77 -3
  111. package/authoring/doctypes/reference/parse.d.mts +2 -2
  112. package/authoring/doctypes/reference/parse.mjs +8 -5
  113. package/authoring/doctypes/reference/reference.doc.mjs +17 -4
  114. package/authoring/doctypes/reference/type.ts +51 -5
  115. package/authoring/doctypes/schema/parse.d.mts +2 -2
  116. package/authoring/doctypes/schema/parse.mjs +1 -1
  117. package/authoring/doctypes/schema/type.ts +3 -2
  118. package/authoring/doctypes/template/parse.d.mts +92 -1
  119. package/authoring/doctypes/template/parse.mjs +36 -2
  120. package/authoring/doctypes/template/parse.test.mjs +8 -2
  121. package/authoring/doctypes/template/template.doc.mjs +4 -0
  122. package/authoring/doctypes/template/type.ts +5 -2
  123. package/authoring/doctypes/types.ts +10 -9
  124. package/authoring/gap-report/parse.d.mts +10 -10
  125. package/authoring/gap-report/parse.mjs +6 -6
  126. package/authoring/gap-report/type.ts +1 -1
  127. package/authoring/identity/identity.doc.d.mts +9 -0
  128. package/authoring/identity/identity.doc.mjs +61 -0
  129. package/authoring/identity/type.ts +132 -0
  130. package/authoring/index.d.mts +1 -0
  131. package/authoring/index.d.ts +49 -17
  132. package/authoring/index.mjs +1 -0
  133. package/authoring/integration/integration.doc.mjs +13 -6
  134. package/authoring/integration/parse.d.mts +2 -2
  135. package/authoring/integration/parse.mjs +1 -1
  136. package/authoring/integration/parse.test.mjs +10 -1
  137. package/authoring/integration/schema.d.mts +6 -4
  138. package/authoring/integration/schema.mjs +9 -3
  139. package/authoring/integration/type.ts +23 -6
  140. package/authoring/shadcn/receipt.d.mts +6 -6
  141. package/clients/cli/commands/docs.doc.mjs +13 -3
  142. package/clients/cli/commands/docs.mjs +121 -21
  143. package/clients/cli/commands/docs.test.mjs +88 -0
  144. package/clients/cli/commands/integration-authoring.test.mjs +13 -9
  145. package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
  146. package/clients/cli/commands/upgrade.doc.mjs +2 -2
  147. package/clients/cli/formatters/index.mjs +162 -1
  148. package/clients/cli/formatters/index.test.mjs +91 -0
  149. package/clients/cli/lib/manifest.mjs +7 -2
  150. package/foundation/config/project.mjs +21 -6
  151. package/foundation/discovery/authoring-self-docs.d.mts +69 -0
  152. package/foundation/discovery/authoring-self-docs.mjs +214 -0
  153. package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
  154. package/foundation/discovery/component-discovery.d.mts +1 -1
  155. package/foundation/discovery/component-discovery.mjs +2 -1
  156. package/foundation/discovery/docs-discovery.d.mts +11 -4
  157. package/foundation/discovery/docs-discovery.mjs +208 -88
  158. package/foundation/discovery/docs-discovery.test.mjs +279 -13
  159. package/foundation/discovery/docs-output-budget.d.mts +28 -0
  160. package/foundation/discovery/docs-output-budget.mjs +50 -0
  161. package/foundation/discovery/docs-section-key.d.mts +98 -0
  162. package/foundation/discovery/docs-section-key.mjs +221 -0
  163. package/foundation/discovery/docs-section-key.test.mjs +224 -0
  164. package/foundation/discovery/template-adapter.mjs +2 -1
  165. package/foundation/discovery/theming-targets.test.mjs +4 -0
  166. package/foundation/doc-compiler/compile.d.mts +162 -0
  167. package/foundation/doc-compiler/compile.mjs +262 -0
  168. package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
  169. package/foundation/doc-compiler/ir.d.mts +9 -0
  170. package/foundation/doc-compiler/ir.mjs +287 -0
  171. package/foundation/doc-compiler/lenses.d.mts +33 -0
  172. package/foundation/doc-compiler/lenses.mjs +127 -0
  173. package/foundation/identity/provider-identity.d.mts +90 -0
  174. package/foundation/identity/provider-identity.mjs +320 -0
  175. package/foundation/identity/provider-identity.test.mjs +254 -0
  176. package/foundation/identity/providers.d.mts +7 -0
  177. package/foundation/identity/providers.mjs +16 -0
  178. package/foundation/integrations/autolink.mjs +12 -5
  179. package/foundation/integrations/integration-warnings.mjs +6 -0
  180. package/foundation/integrations/integrations.d.mts +46 -2
  181. package/foundation/integrations/integrations.mjs +167 -8
  182. package/foundation/integrations/integrations.test.mjs +384 -1
  183. package/foundation/integrations/provider-conflicts.test.mjs +125 -0
  184. package/foundation/integrations/validate-contributions.d.mts +2 -0
  185. package/foundation/integrations/validate-contributions.mjs +10 -0
  186. package/foundation/response/json-contract.test.mjs +46 -17
  187. package/foundation/response/response-types.doc.mjs +6 -1
  188. package/package.json +9 -11
@@ -3,9 +3,10 @@
3
3
  /**
4
4
  * Public type surface for an Astryx integration manifest
5
5
  * (`astryx.integration.{ts,mjs,js}`, sibling to the integration package's
6
- * package.json). Identity (name/version) comes from package.json, not the
7
- * manifest. Authors write a plain object against {@link AstryxIntegration};
8
- * the CLI validates it via `parseIntegration` at the load boundary.
6
+ * package.json). Identity defaults to the package name, but `providerId` can keep
7
+ * a stable logical identity across an explicit package rename. Authors write a
8
+ * plain object against {@link AstryxIntegration}; the CLI validates it via
9
+ * `parseIntegration` at the load boundary.
9
10
  *
10
11
  * The manifest module may also carry `debug` and `gapReport` NAMED exports.
11
12
  * They are not fields here on purpose: a CLI released before a given manifest
@@ -13,11 +14,20 @@
13
14
  * named export is simply not read. See the `cli-integrations` doc topic.
14
15
  */
15
16
  export interface AstryxIntegration {
17
+ /** Stable logical provider ID. Omit to use package.json#name. Set this only
18
+ * when a package rename must preserve existing artifact IDs. */
19
+ providerId?: string;
16
20
  /** Relative path to the components/docs root (resolved to absolute). */
17
21
  components?: string;
18
22
  /** Relative path to the templates root (resolved to absolute). */
19
23
  templates?: string;
20
- /** Relative path to the codemods root (resolved to absolute). */
24
+ /** Relative path to the codemods root (resolved to absolute).
25
+ * The root uses a version-folder-first layout:
26
+ * `<codemodsRoot>/<version>/<id>.<ext>`, where `<version>` is an exact
27
+ * semver string (e.g. `0.2.0`, no `v` prefix) and `<id>` is a kebab-case
28
+ * module basename. Each module default-exports a codemod envelope stamped
29
+ * `type: 'code'` or `type: 'config'`. Codemod ids must be unique within
30
+ * a package across all versions. */
21
31
  codemods?: string;
22
32
  /** Relative path to the reference-docs (topics) root (resolved to
23
33
  * absolute). Every `{topic}.doc.{ts,mjs,js}` under it is a topic the CLI
@@ -25,7 +35,14 @@ export interface AstryxIntegration {
25
35
  * `replace` or `extend` a built-in topic; see the ReferenceDoc type. */
26
36
  docs?: string;
27
37
  /** Relative path to the source-theme catalog root (resolved to absolute).
28
- * The root contains `manifest.json` plus one directory per theme slug. */
38
+ * The root contains `manifest.json` plus one directory per theme slug.
39
+ * `manifest.json` is `{ "version": 1, "themes": [...] }` where each
40
+ * entry requires `slug`, `displayName`, `description` (string),
41
+ * `maintained` (boolean), `entry` (source file relative to `themes/<slug>/`),
42
+ * `exportName` (a valid JS identifier naming the runtime export), and
43
+ * `files` (non-empty array of filenames relative to `themes/<slug>/`).
44
+ * Every file listed must exist on disk; the entry file must also appear
45
+ * in `files`. */
29
46
  themes?: string;
30
47
  /** Static package guidance appended to the CLI-owned managed agent block. */
31
48
  agentDocs?: {
@@ -49,4 +66,4 @@ export type {
49
66
  GapReportCategory,
50
67
  GapReportTarget,
51
68
  GapReportHandlerReceipt,
52
- } from '../gap-report/type';
69
+ } from '../gap-report/type.js';
@@ -63,7 +63,7 @@ export function createRegistryReceipt(input: {
63
63
  name: string;
64
64
  path: string;
65
65
  aliases: string[];
66
- kind: "block" | "page" | "example" | "showcase";
66
+ kind: "page" | "block" | "example" | "showcase";
67
67
  };
68
68
  source: {
69
69
  package: "@astryxdesign/cli";
@@ -83,7 +83,7 @@ export function createRegistryReceipt(input: {
83
83
  name: string;
84
84
  path: string;
85
85
  aliases: string[];
86
- kind: "block" | "page" | "example" | "showcase";
86
+ kind: "page" | "block" | "example" | "showcase";
87
87
  };
88
88
  source: {
89
89
  package: "@astryxdesign/cli";
@@ -112,7 +112,7 @@ export function parseRegistryReceipt(input: unknown): {
112
112
  name: string;
113
113
  path: string;
114
114
  aliases: string[];
115
- kind: "block" | "page" | "example" | "showcase";
115
+ kind: "page" | "block" | "example" | "showcase";
116
116
  };
117
117
  source: {
118
118
  package: "@astryxdesign/cli";
@@ -132,7 +132,7 @@ export function parseRegistryReceipt(input: unknown): {
132
132
  name: string;
133
133
  path: string;
134
134
  aliases: string[];
135
- kind: "block" | "page" | "example" | "showcase";
135
+ kind: "page" | "block" | "example" | "showcase";
136
136
  };
137
137
  source: {
138
138
  package: "@astryxdesign/cli";
@@ -165,8 +165,8 @@ export const registryReceiptSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
165
165
  path: z.ZodString;
166
166
  aliases: z.ZodArray<z.ZodString>;
167
167
  kind: z.ZodEnum<{
168
- block: "block";
169
168
  page: "page";
169
+ block: "block";
170
170
  example: "example";
171
171
  showcase: "showcase";
172
172
  }>;
@@ -190,8 +190,8 @@ export const registryReceiptSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
190
190
  path: z.ZodString;
191
191
  aliases: z.ZodArray<z.ZodString>;
192
192
  kind: z.ZodEnum<{
193
- block: "block";
194
193
  page: "page";
194
+ block: "block";
195
195
  example: "example";
196
196
  showcase: "showcase";
197
197
  }>;
@@ -16,22 +16,32 @@ export const doc = {
16
16
  summary: 'Print reference docs',
17
17
  description:
18
18
  'Reads the reference docs: with no topic it lists every topic; a topic prints that ' +
19
- 'full doc; a topic plus a section returns the first section whose title contains the ' +
20
- '(case-insensitive) query.',
19
+ 'full doc; `--index` lists its sections instead, each with the key to read it by; a ' +
20
+ 'topic plus a section prints that section (by key, exact title, or a unique part of ' +
21
+ 'a title).',
21
22
  fn: 'docs',
22
23
  args: [
23
24
  {name: 'topic', param: 'topic', required: false},
24
25
  {name: 'section', param: 'section', required: false},
25
26
  ],
27
+ options: [
28
+ {
29
+ flag: '--index',
30
+ param: 'options.index',
31
+ description: "List the topic's sections and their keys instead of printing the whole topic",
32
+ },
33
+ ],
26
34
  examples: [
27
35
  {label: 'List topics', cli: 'astryx docs'},
28
36
  {label: 'One topic as JSON', cli: 'astryx docs spacing --json'},
37
+ {label: "A topic's sections", cli: 'astryx docs theme --index'},
38
+ {label: 'One section', cli: 'astryx docs theme quick-start'},
29
39
  ],
30
40
  exitCodes: [
31
41
  {code: 0, when: 'success'},
32
42
  {
33
43
  code: 1,
34
- when: 'unknown topic, or a section that matches no title in the topic',
44
+ when: 'unknown topic, or a section that matches no section or more than one',
35
45
  },
36
46
  ],
37
47
  related: ['search', 'component', 'hook', 'template'],
@@ -3,18 +3,29 @@
3
3
  /**
4
4
  * @file docs command — Print Astryx reference docs
5
5
  *
6
- * Auto-discovers .doc.mjs files from the docs/ directory.
6
+ * A topic prints its whole doc; `--index` lists its sections instead, so a
7
+ * reader can open one section by its key.
7
8
  * Supports --detail (full|compact|brief) and --lang (en|zh|dense).
8
9
  *
9
10
  * Usage:
10
11
  * astryx docs List available topics
11
- * astryx docs <topic> Print full doc
12
+ * astryx docs <topic> Print the whole topic
13
+ * astryx docs <topic> --index List the topic's sections
12
14
  * astryx docs <topic> <section> Print one section
13
15
  */
14
16
 
15
17
  import {getCliInvocation} from '../../../foundation/env/package-manager.mjs';
16
18
  import {jsonOut} from '../../../foundation/response/json.mjs';
17
- import {emit, section, records, text, code} from '../formatters/index.mjs';
19
+ import {
20
+ emit,
21
+ section,
22
+ records,
23
+ text,
24
+ code,
25
+ wrapText,
26
+ displayWidth,
27
+ WRAP_WIDTH,
28
+ } from '../formatters/index.mjs';
18
29
  import {cliError} from '../lib/cli-error.mjs';
19
30
  import {defineCommand} from '../lib/define-command.mjs';
20
31
  import {resultSet} from '../../../foundation/debug/index.mjs';
@@ -41,6 +52,28 @@ function formatTable(headers, rows) {
41
52
  return `${head}\n${sep}\n${body}`;
42
53
  }
43
54
 
55
+ /**
56
+ * A table too wide for {@link WRAP_WIDTH}: one `header: cell` line per cell and
57
+ * a blank line between rows, so nothing runs off the side of a terminal.
58
+ * @param {string[]} headers
59
+ * @param {string[][]} rows
60
+ * @returns {string}
61
+ */
62
+ function formatTableVertical(headers, rows) {
63
+ const width = Math.max(...headers.map(h => h.length)) + 2;
64
+ return rows
65
+ .map(row =>
66
+ headers
67
+ .map((h, i) =>
68
+ wrapText(`${`${h}:`.padEnd(width)}${row[i] ?? ''}`, {
69
+ indent: ' '.repeat(width),
70
+ }),
71
+ )
72
+ .join('\n'),
73
+ )
74
+ .join('\n\n');
75
+ }
76
+
44
77
  /**
45
78
  * @param {string[]} headers
46
79
  * @param {string[][]} rows
@@ -58,7 +91,7 @@ function formatTableCompact(headers, rows) {
58
91
  function formatBlock(block, detail) {
59
92
  switch (block.type) {
60
93
  case 'prose':
61
- return block.text;
94
+ return wrapText(block.text);
62
95
 
63
96
  case 'heading':
64
97
  return `${'#'.repeat(block.level || 3)} ${block.text}`;
@@ -77,16 +110,37 @@ function formatBlock(block, detail) {
77
110
  if (detail === 'compact') {
78
111
  return formatTableCompact(block.headers, block.rows);
79
112
  }
80
- return formatTable(block.headers, block.rows);
113
+ {
114
+ const table = formatTable(block.headers, block.rows);
115
+ return table.split('\n').some(line => displayWidth(line) > WRAP_WIDTH)
116
+ ? formatTableVertical(block.headers, block.rows)
117
+ : table;
118
+ }
81
119
 
82
120
  case 'list': {
83
- const prefix = block.style === 'ordered' ? (/** @type {number} */ i) => `${i + 1}. `
84
- : block.style === 'dont' ? () => 'x '
85
- : block.style === 'do' ? () => '+ '
86
- : () => '- ';
87
- return block.items.map((item, i) => `${prefix(i)}${item}`).join('\n');
121
+ const prefix =
122
+ block.style === 'ordered'
123
+ ? (/** @type {number} */ i) => `${i + 1}. `
124
+ : block.style === 'dont'
125
+ ? () => 'x '
126
+ : block.style === 'do'
127
+ ? () => '+ '
128
+ : () => '- ';
129
+ return block.items
130
+ .map((item, i) => {
131
+ const head = prefix(i);
132
+ return wrapText(`${head}${item}`, {indent: ' '.repeat(head.length)});
133
+ })
134
+ .join('\n');
88
135
  }
89
136
 
137
+ case 'workflow':
138
+ case 'collection':
139
+ case 'reference':
140
+ throw new Error(
141
+ `Documentation block "${block.type}" requires the compiled graph renderer.`,
142
+ );
143
+
90
144
  default:
91
145
  return null;
92
146
  }
@@ -107,7 +161,8 @@ function formatSection(section, detail) {
107
161
  return `${section.title}: ${first.split('\n')[0]}`;
108
162
  }
109
163
 
110
- const heading = detail === 'compact' ? `[${section.title}]` : `## ${section.title}`;
164
+ const heading =
165
+ detail === 'compact' ? `[${section.title}]` : `## ${section.title}`;
111
166
  return `${heading}\n\n${blocks.join('\n\n')}`;
112
167
  }
113
168
 
@@ -118,25 +173,51 @@ function formatSection(section, detail) {
118
173
  */
119
174
  function formatReferenceFull(docs, detail) {
120
175
  if (detail === 'brief') {
121
- const header = `${docs.title}: ${docs.description}`;
176
+ const header = wrapText(`${docs.title}: ${docs.description}`);
122
177
  const sections = docs.sections.map(s => formatSection(s, detail));
123
178
  return `${header}\n${sections.join('\n')}`;
124
179
  }
125
180
 
126
- const header = detail === 'compact'
127
- ? `# ${docs.title}\n${docs.description}`
128
- : `# ${docs.title}\n\n${docs.description}`;
181
+ const description = wrapText(docs.description);
182
+ const header =
183
+ detail === 'compact'
184
+ ? `# ${docs.title}\n${description}`
185
+ : `# ${docs.title}\n\n${description}`;
129
186
  const sections = docs.sections.map(s => formatSection(s, detail));
130
187
  const sep = detail === 'compact' ? '\n\n' : '\n\n';
131
188
  return `${header}\n\n${sections.join(sep)}`;
132
189
  }
133
190
 
191
+ /**
192
+ * A topic's section index: what the topic is, one line per section with the
193
+ * key to read it by, and how to read further.
194
+ * @param {import('../../../api/docs/docs.type.mjs').DocsIndex} index
195
+ * @param {string} run
196
+ */
197
+ function emitIndex(index, run) {
198
+ emit(
199
+ section(index.title, index.description ? wrapText(index.description) : undefined),
200
+ records(index.sections, {
201
+ fields: ['id', 'title', 'summary'],
202
+ layout: 'inline',
203
+ overflow: 'truncate',
204
+ }),
205
+ text(
206
+ [
207
+ `Read one section: ${run} docs ${index.name} <section>`,
208
+ `Read everything: ${run} docs ${index.name}`,
209
+ ].join('\n'),
210
+ ),
211
+ );
212
+ }
213
+
134
214
  /**
135
215
  * What the run answered with. A named topic (or one of its sections) resolves
136
216
  * or throws, so it is always a direct match of one doc; the bare form lists
137
217
  * every topic there is.
138
218
  *
139
219
  * @param {import('../../../api/docs/docs.type.mjs').DocsListResponse
220
+ * | import('../../../api/docs/docs.type.mjs').DocsIndexResponse
140
221
  * | import('../../../api/docs/docs.type.mjs').DocsDetailResponse
141
222
  * | import('../../../api/docs/docs.type.mjs').DocsDetailSectionResponse} result
142
223
  * @returns {import('../../../foundation/debug/command-result.mjs').CommandResult}
@@ -155,7 +236,11 @@ function summarize(result) {
155
236
  export function registerDocs(program) {
156
237
  defineCommand(program, docsCommand, {
157
238
  fn: docsFn,
158
- action: async (/** @type {string | undefined} */ topic, /** @type {string | undefined} */ sectionName) => {
239
+ action: async (
240
+ /** @type {string | undefined} */ topic,
241
+ /** @type {string | undefined} */ sectionName,
242
+ /** @type {{index?: boolean}} */ options = {},
243
+ ) => {
159
244
  const run = getCliInvocation();
160
245
  const lang = program.opts().lang || null;
161
246
  const zh = program.opts().zh || false;
@@ -165,11 +250,17 @@ export function registerDocs(program) {
165
250
 
166
251
  let result;
167
252
  try {
168
- result = await docsApi(topic, sectionName, {lang, zh, dense});
253
+ result = await docsApi(topic, sectionName, {
254
+ lang,
255
+ zh,
256
+ dense,
257
+ index: Boolean(options.index),
258
+ });
169
259
  } catch (e) {
170
260
  // docs API throws structured errors with {name, reason} suggestions —
171
261
  // pass them through untouched so the CLI envelope matches the API.
172
- const err = /** @type {import('../../../api/error.mjs').AstryxError} */ (e);
262
+ const err =
263
+ /** @type {import('../../../api/error.mjs').AstryxError} */ (e);
173
264
  return cliError(err.message, {
174
265
  suggestions: err.suggestions || [],
175
266
  code: err.code,
@@ -188,17 +279,26 @@ export function registerDocs(program) {
188
279
  // description), then the usage footer as plain prose.
189
280
  emit(
190
281
  section('Available docs'),
191
- records(result.data, {fields: ['topic', 'description']}),
282
+ records(result.data, {
283
+ fields: ['topic', 'description'],
284
+ layout: 'inline',
285
+ }),
192
286
  text(
193
287
  [
194
- `Usage: ${run} docs <topic>`,
195
- ` ${run} docs <topic> <section>`,
288
+ `Usage: ${run} docs <topic> read the whole topic`,
289
+ ` ${run} docs <topic> --index list its sections`,
290
+ ` ${run} docs <topic> <section> read one section`,
196
291
  ].join('\n'),
197
292
  ),
198
293
  );
199
294
  break;
200
295
  }
201
296
 
297
+ case 'docs.index': {
298
+ emitIndex(result.data, run);
299
+ break;
300
+ }
301
+
202
302
  case 'docs.detail': {
203
303
  emit(code(formatReferenceFull(result.data, detail)));
204
304
  break;
@@ -6,6 +6,8 @@ import * as path from 'node:path';
6
6
  import * as os from 'node:os';
7
7
  import {Command} from 'commander';
8
8
  import {registerDocs} from './docs.mjs';
9
+ import {runCli} from '../../../test-utils/run-cli.mjs';
10
+ import {displayWidth} from '../formatters/index.mjs';
9
11
 
10
12
  let tmpDir;
11
13
 
@@ -100,3 +102,89 @@ describe('migration docs', () => {
100
102
  expect(output).toContain('Map shadcn and Radix Primitives');
101
103
  });
102
104
  });
105
+
106
+ describe('progressive reads', () => {
107
+ const SLOW = 60_000;
108
+ /** @param {string} out */
109
+ const widest = out => Math.max(...out.split('\n').map(line => line.length));
110
+
111
+ it('lists every topic on one line each', async () => {
112
+ const {status, stdout} = await runCli(['docs']);
113
+ expect(status).toBe(0);
114
+ expect(stdout).toMatch(/^principles +\S/m);
115
+ expect(widest(stdout)).toBeLessThanOrEqual(120);
116
+ }, SLOW);
117
+
118
+ it("prints a topic's section index with the keys to read by", async () => {
119
+ const {status, stdout} = await runCli(['docs', 'theme', '--index']);
120
+ expect(status).toBe(0);
121
+ expect(stdout).toMatch(/^quick-start +Quick Start/m);
122
+ expect(stdout).toContain('docs theme <section>');
123
+ expect(stdout).toMatch(/Read everything: +\S.* docs theme$/m);
124
+ expect(widest(stdout)).toBeLessThanOrEqual(120);
125
+ }, SLOW);
126
+
127
+ it('prints one section by its key', async () => {
128
+ const {status, stdout} = await runCli(['docs', 'theme', 'quick-start']);
129
+ expect(status).toBe(0);
130
+ expect(stdout).toMatch(/^## Quick Start/m);
131
+ }, SLOW);
132
+
133
+ it('prints the whole topic by default, as before', async () => {
134
+ const index = await runCli(['docs', 'theme', '--index']);
135
+ const full = await runCli(['docs', 'theme']);
136
+ expect(full.status).toBe(0);
137
+ expect(full.stdout).toMatch(/^## Quick Start/m);
138
+ expect(full.stdout.length).toBeGreaterThan(index.stdout.length * 3);
139
+ expect((await runCli(['--detail', 'full', 'docs', 'theme'])).stdout).toBe(
140
+ full.stdout,
141
+ );
142
+ expect(widest(full.stdout.replace(/```[\s\S]*?```/g, ''))).toBeLessThanOrEqual(
143
+ 120,
144
+ );
145
+ }, SLOW);
146
+
147
+ it('returns the matching envelopes as JSON', async () => {
148
+ const envelope = async args => JSON.parse((await runCli([...args, '--json'])).stdout);
149
+ expect((await envelope(['docs', 'theme'])).type).toBe('docs.detail');
150
+ expect((await envelope(['docs', 'theme', '--index'])).type).toBe(
151
+ 'docs.index',
152
+ );
153
+ expect((await envelope(['docs', 'theme', 'quick-start'])).type).toBe(
154
+ 'docs.detail.section',
155
+ );
156
+ }, SLOW);
157
+ });
158
+
159
+ describe('text width in every language', () => {
160
+ const SLOW = 60_000;
161
+ /** Widest line outside code blocks, in terminal columns. */
162
+ const widest = out => {
163
+ let inCode = false;
164
+ let max = 0;
165
+ for (const line of out.split('\n')) {
166
+ if (/^\s*```/.test(line)) {
167
+ inCode = !inCode;
168
+ continue;
169
+ }
170
+ // A single unbreakable token (a long URL) cannot wrap without breaking it.
171
+ const oneToken = !/\s/.test(line.trim());
172
+ if (!inCode && !line.startsWith('#') && !oneToken) {
173
+ max = Math.max(max, displayWidth(line));
174
+ }
175
+ }
176
+ return max;
177
+ };
178
+
179
+ it.each([
180
+ [['docs', 'theme', '--index', '--lang', 'zh']],
181
+ [['docs', 'theme', '--lang', 'zh']],
182
+ [['--detail', 'full', 'docs', 'theme', '--lang', 'zh']],
183
+ [['--detail', 'full', 'docs', 'internationalization']],
184
+ [['--detail', 'full', 'docs', 'styling']],
185
+ ])('%j fits in 120 columns', async args => {
186
+ const {status, stdout} = await runCli(args);
187
+ expect(status).toBe(0);
188
+ expect(widest(stdout)).toBeLessThanOrEqual(120);
189
+ }, SLOW);
190
+ });
@@ -120,7 +120,7 @@ describe('integration authoring CLI', () => {
120
120
  );
121
121
  });
122
122
 
123
- it('packs the generated contribution and proves the consumer inventory', async () => {
123
+ it('pack-check rejects a generated component without an exports map (no-map false green)', async () => {
124
124
  const added = await runCli(
125
125
  ['integration', 'add', 'component', 'AcmeWidget', '--json'],
126
126
  tmpDir,
@@ -131,19 +131,23 @@ describe('integration authoring CLI', () => {
131
131
  ['integration', 'pack', '--check', '--json'],
132
132
  tmpDir,
133
133
  );
134
- expect(checked.status).toBe(0);
135
- expect(parseEnvelope(checked.stdout)).toMatchObject({
134
+ // Without an exports map, the extensionless import cannot resolve —
135
+ // pack-check must fail, not false-green.
136
+ expect(checked.status).not.toBe(0);
137
+ const envelope = parseEnvelope(checked.stdout);
138
+ expect(envelope).toMatchObject({
136
139
  type: 'integration.pack-check',
137
140
  data: {
138
141
  name: '@acme/widgets',
139
- version: '1.0.0',
140
- packable: true,
141
- contributions: {
142
- local: {components: ['AcmeWidget']},
143
- packed: {components: ['AcmeWidget']},
144
- },
142
+ packable: false,
145
143
  },
146
144
  });
145
+ expect(envelope.data.issues).toContainEqual(
146
+ expect.objectContaining({
147
+ severity: 'error',
148
+ message: expect.stringContaining('AcmeWidget'),
149
+ }),
150
+ );
147
151
  });
148
152
 
149
153
  it('requires the explicit --check gate on pack', async () => {
@@ -18,7 +18,11 @@ export const doc = {
18
18
  'Without --out it prints a preview. With --out it writes a candidate file and detached ' +
19
19
  'receipt. --preview writes a standardized, self-contained HTML review artifact. ' +
20
20
  'TypeScript output is directly importable and contains no generator dependency. ' +
21
- 'JSON is also supported. Existing author-owned files are left untouched unless --overwrite is explicit.',
21
+ 'JSON is also supported. Existing author-owned files are left untouched unless --overwrite is explicit. ' +
22
+ 'When used in a theme integration, keep the palette request under the theme slug, ' +
23
+ 'write the candidate and receipt under that same slug, import the candidate from the theme source, ' +
24
+ "and list all three paths in the theme catalog entry's `files` array " +
25
+ 'so `astryx theme add` copies them into the consumer project.',
22
26
  fn: 'themePaletteGenerate',
23
27
  args: [{name: 'config', param: 'configPath', required: true}],
24
28
  options: [
@@ -42,15 +46,15 @@ export const doc = {
42
46
  examples: [
43
47
  {
44
48
  label: 'Preview candidate JSON',
45
- cli: 'astryx theme palette generate palette.config.json',
49
+ cli: 'astryx theme palette generate themes/ocean/palette.config.json',
46
50
  },
47
51
  {
48
52
  label: 'Write candidate and receipt',
49
- cli: 'astryx theme palette generate palette.config.json --out ocean.palette.ts',
53
+ cli: 'astryx theme palette generate themes/ocean/palette.config.json --out themes/ocean/tokens/ocean.palette.ts',
50
54
  },
51
55
  {
52
56
  label: 'Write candidate, receipt, and review preview',
53
- cli: 'astryx theme palette generate palette.config.json --out ocean.palette.ts --preview ocean.palette.html',
57
+ cli: 'astryx theme palette generate themes/ocean/palette.config.json --out themes/ocean/tokens/ocean.palette.ts --preview themes/ocean/tokens/ocean.palette.html',
54
58
  },
55
59
  ],
56
60
  exitCodes: [
@@ -53,10 +53,10 @@ export const doc = {
53
53
  'Exclude named codemods (repeatable). Re-run past a failed codemod by skipping it.',
54
54
  },
55
55
  {
56
- flag: '--integration <package-or-file>',
56
+ flag: '--integration <package>',
57
57
  param: 'options.integration',
58
58
  description:
59
- 'Explicit integration package name or integration file path (repeatable)',
59
+ 'Explicit integration specifier (repeatable). Resolved beneath node_modules; absolute paths and `.` or `..` segments are rejected.',
60
60
  default: [],
61
61
  },
62
62
  {