@astryxdesign/cli 0.1.2 → 0.1.3

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 (238) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +49 -55
  3. package/docs/layout.doc.dense.mjs +90 -0
  4. package/docs/layout.doc.mjs +160 -0
  5. package/docs/principles.doc.dense.mjs +2 -2
  6. package/docs/principles.doc.mjs +4 -0
  7. package/docs/principles.doc.zh.mjs +2 -2
  8. package/docs/theme.doc.mjs +2 -2
  9. package/package.json +13 -1
  10. package/src/api/blog.mjs +192 -0
  11. package/src/api/blog.test.mjs +144 -0
  12. package/src/api/component.mjs +253 -12
  13. package/src/api/discover.mjs +14 -7
  14. package/src/api/doctor.mjs +10 -25
  15. package/src/api/index.mjs +1 -0
  16. package/src/api/layout.mjs +11 -10
  17. package/src/api/layout.test.mjs +4 -1
  18. package/src/api/template-integration.test.mjs +225 -0
  19. package/src/api/template.mjs +242 -84
  20. package/src/api/validate-integration.mjs +370 -0
  21. package/src/api/validate-integration.test.mjs +222 -0
  22. package/src/codemod.mjs +93 -0
  23. package/src/codemod.test.mjs +134 -0
  24. package/src/codemods/__tests__/registry.test.mjs +1 -0
  25. package/src/codemods/__tests__/runner.test.mjs +103 -0
  26. package/src/codemods/integration-discovery.mjs +168 -0
  27. package/src/codemods/integration-discovery.test.mjs +234 -0
  28. package/src/codemods/integration-runner.mjs +109 -0
  29. package/src/codemods/registry.mjs +1 -0
  30. package/src/codemods/run-codemod.mjs +207 -0
  31. package/src/codemods/runner.mjs +73 -96
  32. package/src/codemods/transforms/v0.0.15/index.mjs +0 -13
  33. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-css-surfaces.test.mjs +67 -0
  34. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-declare-module.test.mjs +61 -0
  35. package/src/codemods/transforms/v0.1.0/__tests__/v0.1.0-ordering.test.mjs +104 -0
  36. package/src/codemods/transforms/{v0.0.15 → v0.1.0}/drop-xds-prefix-imports.mjs +15 -4
  37. package/src/codemods/transforms/v0.1.0/index.mjs +43 -6
  38. package/src/codemods/transforms/v0.1.0/migrate-xds-css-surfaces.mjs +77 -0
  39. package/src/codemods/transforms/v0.1.0/migrate-xds-declare-module.mjs +78 -0
  40. package/src/codemods/transforms/v0.1.3/__tests__/migrate-layout-components-to-experimental.test.mjs +360 -0
  41. package/src/codemods/transforms/v0.1.3/index.mjs +19 -0
  42. package/src/codemods/transforms/v0.1.3/migrate-layout-components-to-experimental.mjs +260 -0
  43. package/src/commands/agent-docs.mjs +4 -1
  44. package/src/commands/blog.mjs +60 -0
  45. package/src/commands/build-theme.import-path.test.mjs +2 -12
  46. package/src/commands/build-theme.mjs +117 -0
  47. package/src/commands/build-theme.prose.test.mjs +5 -15
  48. package/src/commands/build-theme.watch.test.mjs +149 -0
  49. package/src/commands/component/index.mjs +44 -10
  50. package/src/commands/component-ownership.test.mjs +227 -0
  51. package/src/commands/discover.mjs +5 -16
  52. package/src/commands/doctor.test.mjs +3 -3
  53. package/src/commands/ensure-core-built.mjs +120 -0
  54. package/src/commands/json-contract.test.mjs +0 -32
  55. package/src/commands/swizzle.mjs +224 -235
  56. package/src/commands/swizzle.path-safety.test.mjs +3 -3
  57. package/src/commands/swizzle.routing.test.mjs +279 -0
  58. package/src/commands/template.mjs +33 -37
  59. package/src/commands/upgrade.config-ordering.test.mjs +273 -0
  60. package/src/commands/upgrade.integration-policy.test.mjs +204 -0
  61. package/src/commands/upgrade.mjs +349 -170
  62. package/src/commands/validate-integration.mjs +110 -0
  63. package/src/commands/validate-integration.test.mjs +124 -0
  64. package/src/config.mjs +7 -20
  65. package/src/config.test.mjs +89 -13
  66. package/src/index.mjs +8 -3
  67. package/src/integration.mjs +19 -0
  68. package/src/lib/component-discovery.mjs +177 -0
  69. package/src/lib/config-cache.mjs +91 -0
  70. package/src/lib/config-cache.test.mjs +83 -0
  71. package/src/lib/config-schema.mjs +41 -55
  72. package/src/lib/error-codes.mjs +15 -12
  73. package/src/lib/integration-warnings.mjs +62 -0
  74. package/src/lib/integration-warnings.test.mjs +102 -0
  75. package/src/lib/integrations.mjs +92 -115
  76. package/src/lib/integrations.test.mjs +88 -107
  77. package/src/lib/manifest.mjs +5 -2
  78. package/src/lib/module-loader.mjs +80 -0
  79. package/src/lib/module-loader.test.mjs +106 -0
  80. package/src/lib/project.mjs +502 -0
  81. package/src/lib/project.test.mjs +308 -0
  82. package/src/lib/site.mjs +20 -0
  83. package/src/template.mjs +73 -0
  84. package/src/template.test.mjs +127 -0
  85. package/src/types/api.d.ts +2 -6
  86. package/src/types/base.d.ts +3 -9
  87. package/src/types/codemod.d.ts +81 -0
  88. package/src/types/component.d.ts +27 -2
  89. package/src/types/config.d.ts +56 -85
  90. package/src/types/error-codes.d.ts +5 -3
  91. package/src/types/index.d.ts +0 -1
  92. package/src/types/integration.d.ts +29 -0
  93. package/src/types/swizzle.d.ts +9 -2
  94. package/src/types/template-api.d.ts +54 -0
  95. package/src/types/template.d.ts +10 -7
  96. package/src/types/upgrade.d.ts +29 -0
  97. package/src/types/validate-integration.d.ts +24 -0
  98. package/src/utils/github.mjs +0 -237
  99. package/src/utils/interactive.mjs +2 -2
  100. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
  101. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
  102. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
  103. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
  104. package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
  105. package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
  106. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
  107. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
  108. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
  109. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
  110. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
  111. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
  112. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
  113. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
  114. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
  115. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
  116. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
  117. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
  118. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
  119. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
  120. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
  121. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
  122. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
  123. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
  124. package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
  125. package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
  126. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.doc.mjs +14 -0
  127. package/templates/blocks/components/DateRangeInput/DateRangeInputWithPresets.tsx +46 -0
  128. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.doc.mjs +14 -0
  129. package/templates/blocks/components/DateRangeInput/DateRangeInputWithValidation.tsx +52 -0
  130. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.doc.mjs +14 -0
  131. package/templates/blocks/components/DateTimeInput/DateTimeInputWithValidation.tsx +43 -0
  132. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
  133. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
  134. package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -6
  135. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
  136. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
  137. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -5
  138. package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
  139. package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
  140. package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
  141. package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
  142. package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
  143. package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
  144. package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
  145. package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
  146. package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
  147. package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
  148. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.doc.mjs +14 -0
  149. package/templates/blocks/components/Hooks/useKeyboardHintHookUsage.tsx +57 -0
  150. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.doc.mjs +1 -1
  151. package/templates/blocks/components/HoverCard/HoverCardInteractiveContent.tsx +9 -4
  152. package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
  153. package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
  154. package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
  155. package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
  156. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
  157. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
  158. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
  159. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
  160. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
  161. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
  162. package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
  163. package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
  164. package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
  165. package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
  166. package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
  167. package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
  168. package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
  169. package/templates/blocks/components/LinkProvider/LinkProviderCustomLink.doc.mjs +1 -1
  170. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
  171. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
  172. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
  173. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
  174. package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -6
  175. package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
  176. package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
  177. package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
  178. package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
  179. package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
  180. package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
  181. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
  182. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
  183. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
  184. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
  185. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
  186. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
  187. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
  188. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
  189. package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
  190. package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
  191. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
  192. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
  193. package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
  194. package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
  195. package/templates/blocks/components/Tab/TabWithSelectedIcon.doc.mjs +13 -0
  196. package/templates/blocks/components/Tab/TabWithSelectedIcon.tsx +39 -0
  197. package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
  198. package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
  199. package/templates/blocks/components/Table/StickyColumnsHookUsage.doc.mjs +1 -1
  200. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
  201. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
  202. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
  203. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
  204. package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
  205. package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
  206. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
  207. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
  208. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
  209. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
  210. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
  211. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
  212. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
  213. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
  214. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
  215. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
  216. package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
  217. package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
  218. package/templates/pages/kanban-board/page.tsx +729 -0
  219. package/templates/pages/kanban-board/template.doc.mjs +12 -0
  220. package/templates/pages/shell-nav/page.tsx +321 -0
  221. package/templates/pages/shell-nav/template.doc.mjs +12 -0
  222. package/templates/pages/shell-side-nav/page.tsx +241 -0
  223. package/templates/pages/shell-side-nav/template.doc.mjs +12 -0
  224. package/templates/pages/shell-top-nav/page.tsx +224 -0
  225. package/templates/pages/shell-top-nav/template.doc.mjs +12 -0
  226. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-config-surfaces.test.mjs +0 -116
  227. package/src/codemods/transforms/v0.1.0/migrate-xds-config-surfaces.mjs +0 -230
  228. package/src/commands/gap-report.mjs +0 -464
  229. package/src/commands/gap-report.test.mjs +0 -168
  230. package/src/commands/swizzle-gap-safety.test.mjs +0 -273
  231. package/src/lib/config.mjs +0 -113
  232. package/src/lib/config.test.mjs +0 -91
  233. package/src/types/gap-report.d.ts +0 -29
  234. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
  235. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
  236. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
  237. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
  238. /package/src/codemods/transforms/{v0.0.15 → v0.1.0}/__tests__/drop-xds-prefix-imports.test.mjs +0 -0
@@ -1,464 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file gap-report command — File a gap report for missing Astryx capabilities
5
- *
6
- * Interactive and non-interactive modes for reporting gaps in the design system.
7
- * Creates GitHub issues with the `gap-report` label.
8
- *
9
- * Includes `gap-report setup` subcommand for configuring gap report delivery.
10
- */
11
-
12
- import * as fs from 'node:fs';
13
- import * as path from 'node:path';
14
- import * as p from '@clack/prompts';
15
- import {
16
- buildGapReportPreview,
17
- checkGhCli,
18
- createGapReport,
19
- loadGapReportConfig,
20
- GAP_CATEGORIES,
21
- } from '../utils/github.mjs';
22
- import {getRunPrefix} from '../utils/package-manager.mjs';
23
- import {jsonOut, jsonError, humanLog} from '../lib/json.mjs';
24
- import {cliError} from '../lib/cli-error.mjs';
25
- import {ERROR_CODES} from '../lib/error-codes.mjs';
26
-
27
- /**
28
- * Decide whether we're in a context safe to actually file a report.
29
- *
30
- * Returns true ONLY when the user has explicitly opted in via --commit, OR
31
- * we're in an interactive TTY session AND --dry-run was not requested.
32
- *
33
- * Defaults are intentionally conservative: in CI, when piped, when --json is
34
- * set, or when neither --commit nor a TTY is present, we dry-run.
35
- */
36
- export function shouldActuallyFile({
37
- commit = false,
38
- dryRun = false,
39
- json = false,
40
- isTTY = process.stdout.isTTY,
41
- } = {}) {
42
- if (dryRun) return false;
43
- if (commit) return true;
44
- if (json) return false;
45
- if (!isTTY) return false;
46
- // Interactive TTY without --commit: caller must show a confirmation prompt.
47
- return true;
48
- }
49
-
50
- /**
51
- * Render a human-readable preview of the gap report that would be filed.
52
- */
53
- export function formatPreview(preview) {
54
- const lines = [];
55
- if (preview.mode === 'github') {
56
- lines.push(`Would file GitHub issue on ${preview.repo}:`);
57
- } else if (preview.mode === 'custom') {
58
- lines.push(`Would invoke custom command: ${preview.command}`);
59
- } else {
60
- lines.push('Gap reporting is disabled (no action would be taken).');
61
- return lines.join('\n');
62
- }
63
- lines.push('');
64
- lines.push(` Title: ${preview.title}`);
65
- lines.push('');
66
- lines.push(' Body:');
67
- for (const ln of preview.body.split('\n')) {
68
- lines.push(` ${ln}`);
69
- }
70
- lines.push('');
71
- lines.push(' Labels: gap-report');
72
- return lines.join('\n');
73
- }
74
-
75
- function isCancel(value) {
76
- if (p.isCancel(value)) {
77
- p.cancel('Gap report cancelled.');
78
- process.exit(0);
79
- }
80
- return value;
81
- }
82
-
83
- /**
84
- * Update or insert the gapReport key in astryx.config.mjs.
85
- * Preserves existing keys (e.g. theme).
86
- */
87
- function writeGapReportConfig(targetDir, gapReportValue) {
88
- const configPath = path.join(targetDir, 'astryx.config.mjs');
89
-
90
- if (fs.existsSync(configPath)) {
91
- let content = fs.readFileSync(configPath, 'utf-8');
92
-
93
- // Replace existing gapReport key (handles both `false` and `{ command: '...' }`)
94
- if (/gapReport\s*:/.test(content)) {
95
- content = content.replace(
96
- /gapReport\s*:\s*(?:\{[^}]*\}|false|true)\s*,?/s,
97
- `gapReport: ${gapReportValue},`,
98
- );
99
- } else {
100
- // Insert before the closing `};`
101
- content = content.replace(
102
- /(\n};?\s*)$/,
103
- `\n gapReport: ${gapReportValue},$1`,
104
- );
105
- }
106
-
107
- fs.writeFileSync(configPath, content);
108
- } else {
109
- const content = `/**
110
- * Astryx Configuration
111
- * Generated by \`astryx gap-report setup\`
112
- */
113
- export default {
114
- gapReport: ${gapReportValue},
115
- };
116
- `;
117
- fs.writeFileSync(configPath, content);
118
- }
119
-
120
- return configPath;
121
- }
122
-
123
- export function registerGapReport(program) {
124
- const gapCmd = program
125
- .command('gap-report')
126
- .description('Report a gap in the design system');
127
-
128
- // --- setup subcommand ---
129
- gapCmd
130
- .command('setup')
131
- .description('Configure where gap reports are sent')
132
- .action(async () => {
133
- p.intro('Gap report setup');
134
-
135
- const config = await loadGapReportConfig();
136
- const currentMode = !config.enabled
137
- ? 'disabled'
138
- : config.command
139
- ? 'custom'
140
- : 'github';
141
-
142
- const mode = isCancel(
143
- await p.select({
144
- message: `How should gap reports be delivered? (current: ${currentMode})`,
145
- options: [
146
- {
147
- value: 'github',
148
- label: 'GitHub Issues',
149
- hint: 'Creates issues on facebookexperimental/xds (requires gh CLI)',
150
- },
151
- {
152
- value: 'custom',
153
- label: 'Custom command',
154
- hint: 'Run a script that receives the report as JSON on stdin',
155
- },
156
- {
157
- value: 'disabled',
158
- label: 'Disabled',
159
- hint: 'Turn off gap reporting entirely',
160
- },
161
- ],
162
- }),
163
- );
164
-
165
- let configValue;
166
- const targetDir = process.cwd();
167
-
168
- if (mode === 'disabled') {
169
- configValue = 'false';
170
- writeGapReportConfig(targetDir, configValue);
171
- p.log.success('Gap reporting disabled.');
172
- } else if (mode === 'custom') {
173
- const command = isCancel(
174
- await p.text({
175
- message: 'Path to script:',
176
- placeholder: './scripts/report-gap.sh',
177
- validate: val => {
178
- if (!val.trim()) return 'Script path is required';
179
- },
180
- }),
181
- );
182
-
183
- configValue = `{ command: '${command.trim()}' }`;
184
- writeGapReportConfig(targetDir, configValue);
185
-
186
- p.log.success(`Gap reports will be sent to: ${command.trim()}`);
187
- p.note(
188
- 'Your script receives JSON on stdin with these fields:\n' +
189
- ' component, category, categoryLabel, intention,\n' +
190
- ' detail, source, timestamp\n\n' +
191
- 'Anything printed to stdout is shown to the user.',
192
- 'Script contract',
193
- );
194
- } else {
195
- // GitHub mode — remove gapReport key or set to default
196
- const configPath = path.join(targetDir, 'astryx.config.mjs');
197
- if (fs.existsSync(configPath)) {
198
- let content = fs.readFileSync(configPath, 'utf-8');
199
- // Remove the gapReport line entirely to use default
200
- content = content.replace(
201
- /\s*gapReport\s*:\s*(?:\{[^}]*\}|false|true)\s*,?/s,
202
- '',
203
- );
204
- fs.writeFileSync(configPath, content);
205
- }
206
-
207
- if (!checkGhCli()) {
208
- p.log.warning(
209
- 'gh CLI is not installed or not authenticated.\n' +
210
- 'Install from https://cli.github.com and run `gh auth login`.',
211
- );
212
- } else {
213
- p.log.success(
214
- 'Gap reports will create GitHub issues on facebookexperimental/xds.',
215
- );
216
- }
217
- }
218
-
219
- p.outro('Configuration saved to astryx.config.mjs');
220
- });
221
-
222
- // --- main gap-report command ---
223
- gapCmd
224
- .option('--component <name>', 'Component name')
225
- .option('--category <category>', 'Gap category')
226
- .option('--reason <reason>', 'What capability was missing')
227
- .option('--list-categories', 'List valid gap categories')
228
- .option(
229
- '--dry-run',
230
- 'Print the issue that would be filed without contacting GitHub. Default in CI / non-TTY / --json mode.',
231
- )
232
- .option(
233
- '--commit',
234
- 'Actually file the issue. Required in non-interactive mode (CI, piped, --json).',
235
- )
236
- .addHelpText(
237
- 'after',
238
- '\nSafety:\n' +
239
- ' Set ASTRYX_GAP_REPORT=off to disable gap reporting entirely.\n' +
240
- ' In non-interactive mode (CI, piped stdout, --json), gap-report is\n' +
241
- ' dry-run by default. Pass --commit to actually file an issue.\n' +
242
- ' In interactive mode, you will always be shown a confirmation prompt.\n',
243
- )
244
- .action(async options => {
245
- const json = program.opts().json || false;
246
-
247
- if (options.listCategories) {
248
- if (json) return jsonOut('gap-report.categories', GAP_CATEGORIES);
249
- for (const cat of GAP_CATEGORIES) {
250
- humanLog(` ${cat.value.padEnd(20)} ${cat.label}`);
251
- }
252
- return;
253
- }
254
-
255
- const config = await loadGapReportConfig();
256
- if (!config.enabled) {
257
- if (json)
258
- return jsonError(
259
- 'Gap reporting is disabled',
260
- undefined,
261
- ERROR_CODES.ERR_GAP_REPORT_FAILED,
262
- );
263
- humanLog(
264
- `Gap reporting is disabled (ASTRYX_GAP_REPORT=off or astryx.config.mjs).\n` +
265
- `Run \`${getRunPrefix()} astryx gap-report setup\` to configure.`,
266
- );
267
- return;
268
- }
269
-
270
- // We only need gh available when we'd actually file. For dry-run, skip
271
- // the gh check so users can preview output in CI without `gh` installed.
272
- const willFile = shouldActuallyFile({
273
- commit: options.commit,
274
- dryRun: options.dryRun,
275
- json,
276
- });
277
-
278
- if (willFile && !config.command && !checkGhCli()) {
279
- const msg =
280
- 'GitHub CLI (gh) is not installed or not authenticated. ' +
281
- 'Install it from https://cli.github.com and run `gh auth login`. ' +
282
- `Or run \`${getRunPrefix()} astryx gap-report setup\` to configure a custom command.`;
283
- cliError(msg, {code: ERROR_CODES.ERR_GH_CLI});
284
- return;
285
- }
286
-
287
- const isNonInteractive =
288
- options.component && options.category && options.reason;
289
-
290
- if (isNonInteractive) {
291
- // Non-interactive mode for LLM/script use
292
- const validCategory = GAP_CATEGORIES.find(
293
- c => c.value === options.category,
294
- );
295
- if (!validCategory) {
296
- cliError(
297
- `Invalid category "${options.category}". Valid: ${GAP_CATEGORIES.map(c => c.value).join(', ')}`,
298
- {code: ERROR_CODES.ERR_UNKNOWN_CATEGORY},
299
- );
300
- return;
301
- }
302
-
303
- const preview = await buildGapReportPreview({
304
- component: options.component,
305
- category: options.category,
306
- intention: options.reason,
307
- source: 'cli',
308
- });
309
-
310
- if (!willFile) {
311
- // DRY-RUN: print what would be filed and exit cleanly.
312
- if (json) {
313
- return jsonOut('gap-report.dryRun', {
314
- dryRun: true,
315
- wouldFile: preview.mode !== 'disabled',
316
- mode: preview.mode,
317
- title: preview.title,
318
- body: preview.body,
319
- repo: preview.repo,
320
- command: preview.command || null,
321
- hint:
322
- 'Re-run with --commit to actually file. ' +
323
- 'Default is dry-run in non-interactive contexts.',
324
- });
325
- }
326
- humanLog(formatPreview(preview));
327
- humanLog(
328
- '\n[dry-run] Nothing was filed. Re-run with --commit to file this report.',
329
- );
330
- return;
331
- }
332
-
333
- try {
334
- const url = await createGapReport({
335
- component: options.component,
336
- category: options.category,
337
- intention: options.reason,
338
- source: 'cli',
339
- });
340
- if (json)
341
- return jsonOut('gap-report.file', {
342
- filed: !!url,
343
- url: url || null,
344
- });
345
- if (url) {
346
- humanLog(`\n✓ Gap report filed: ${url}\n`);
347
- } else {
348
- humanLog('\nGap reporting is disabled via configuration.\n');
349
- }
350
- } catch (err) {
351
- cliError(`Filing gap report failed: ${err.message}`, {
352
- code: ERROR_CODES.ERR_GAP_REPORT_FAILED,
353
- });
354
- return;
355
- }
356
- return;
357
- }
358
-
359
- // --json without --component/--category/--reason can't go interactive —
360
- // emit a structured error explaining the required flags.
361
- if (json) {
362
- return jsonError(
363
- 'gap-report --json requires --component, --category, and --reason. ' +
364
- 'Run with --list-categories to see valid categories.',
365
- undefined,
366
- ERROR_CODES.ERR_INVALID_ARGUMENT,
367
- );
368
- }
369
-
370
- // Interactive mode
371
- p.intro('Report a gap');
372
-
373
- const component = isCancel(
374
- await p.text({
375
- message: 'Which component is this about?',
376
- placeholder: 'e.g. Button, Table, or "general"',
377
- validate: val => {
378
- if (!val.trim()) return 'Component name is required';
379
- },
380
- }),
381
- );
382
-
383
- const category = isCancel(
384
- await p.select({
385
- message: 'What kind of gap is this?',
386
- options: GAP_CATEGORIES,
387
- }),
388
- );
389
-
390
- const intention = isCancel(
391
- await p.text({
392
- message: 'What were you trying to achieve?',
393
- placeholder:
394
- 'e.g. "Need a compact variant for use in dense data tables"',
395
- validate: val => {
396
- if (!val.trim())
397
- return 'Please describe what you were trying to do';
398
- },
399
- }),
400
- );
401
-
402
- const detail = isCancel(
403
- await p.text({
404
- message: 'Any additional context? (optional)',
405
- placeholder: 'Press Enter to skip',
406
- }),
407
- );
408
-
409
- const previewArgs = {
410
- component: component.trim(),
411
- category,
412
- intention: intention.trim(),
413
- detail: detail?.trim() || undefined,
414
- source: 'interactive',
415
- };
416
-
417
- const preview = await buildGapReportPreview(previewArgs);
418
-
419
- // Always show the user exactly what would be filed before sending.
420
- p.note(
421
- `${preview.mode === 'github' ? `Repo: ${preview.repo}` : `Custom command: ${preview.command}`}\n\n` +
422
- `Title:\n ${preview.title}\n\n` +
423
- `Body:\n${preview.body
424
- .split('\n')
425
- .map(l => ` ${l}`)
426
- .join('\n')}`,
427
- 'Preview — this is exactly what will be filed',
428
- );
429
-
430
- if (options.dryRun) {
431
- p.outro('[dry-run] Nothing filed.');
432
- return;
433
- }
434
-
435
- const confirm = isCancel(
436
- await p.confirm({
437
- message: 'File this gap report now?',
438
- initialValue: false,
439
- }),
440
- );
441
-
442
- if (!confirm) {
443
- p.outro('Cancelled — nothing was filed.');
444
- return;
445
- }
446
-
447
- const s = p.spinner();
448
- s.start('Filing gap report');
449
-
450
- try {
451
- const url = await createGapReport(previewArgs);
452
- s.stop(url ? 'Gap report filed' : 'Gap reporting is disabled');
453
- if (url) {
454
- p.log.success(url);
455
- }
456
- } catch (err) {
457
- s.stop('Failed to file gap report');
458
- p.log.error(err.message);
459
- process.exit(1);
460
- }
461
-
462
- p.outro('Thanks for the feedback!');
463
- });
464
- }
@@ -1,168 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * Tests for gap-report safety gates: dry-run-by-default in non-interactive
5
- * mode, --commit required to actually file, --no-report suppression in swizzle,
6
- * and ASTRYX_GAP_REPORT=off honored everywhere.
7
- *
8
- * Regression cases for issues #2370 and #2371: real gap-report issues filed
9
- * unintentionally in CI/non-TTY runs.
10
- */
11
-
12
- import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest';
13
- import {shouldActuallyFile, formatPreview} from './gap-report.mjs';
14
-
15
- describe('shouldActuallyFile', () => {
16
- it('returns false in non-TTY mode by default (dry-run by default in CI)', () => {
17
- expect(shouldActuallyFile({isTTY: false})).toBe(false);
18
- });
19
-
20
- it('returns false when --json is set, even with TTY', () => {
21
- expect(shouldActuallyFile({isTTY: true, json: true})).toBe(false);
22
- });
23
-
24
- it('returns false when --dry-run is explicitly set', () => {
25
- expect(shouldActuallyFile({isTTY: true, dryRun: true})).toBe(false);
26
- });
27
-
28
- it('returns true in TTY mode without dry-run/json (interactive prompts will gate)', () => {
29
- expect(shouldActuallyFile({isTTY: true})).toBe(true);
30
- });
31
-
32
- it('returns true with --commit even in non-TTY mode', () => {
33
- expect(shouldActuallyFile({isTTY: false, commit: true})).toBe(true);
34
- });
35
-
36
- it('--commit overrides --json being set', () => {
37
- expect(shouldActuallyFile({isTTY: false, commit: true, json: true})).toBe(
38
- true,
39
- );
40
- });
41
-
42
- it('--dry-run wins over --commit (safety)', () => {
43
- expect(shouldActuallyFile({isTTY: true, commit: true, dryRun: true})).toBe(
44
- false,
45
- );
46
- });
47
- });
48
-
49
- describe('formatPreview', () => {
50
- it('renders a github-mode preview with title, body, and repo', () => {
51
- const out = formatPreview({
52
- mode: 'github',
53
- repo: 'facebookexperimental/xds',
54
- title: '[gap] Button: missing compact variant',
55
- body: '## User Intention\n\nNeed a compact variant',
56
- });
57
- expect(out).toContain(
58
- 'Would file GitHub issue on facebookexperimental/xds',
59
- );
60
- expect(out).toContain('[gap] Button');
61
- expect(out).toContain('Need a compact variant');
62
- expect(out).toContain('Labels: gap-report');
63
- });
64
-
65
- it('renders disabled mode without filing instructions', () => {
66
- const out = formatPreview({mode: 'disabled'});
67
- expect(out).toContain('Gap reporting is disabled');
68
- expect(out).not.toContain('Would file');
69
- });
70
-
71
- it('renders custom-command mode', () => {
72
- const out = formatPreview({
73
- mode: 'custom',
74
- command: './scripts/report.sh',
75
- title: 't',
76
- body: 'b',
77
- });
78
- expect(out).toContain('Would invoke custom command: ./scripts/report.sh');
79
- });
80
- });
81
-
82
- describe('ASTRYX_GAP_REPORT=off env var', () => {
83
- let originalEnv;
84
- beforeEach(() => {
85
- originalEnv = process.env.ASTRYX_GAP_REPORT;
86
- });
87
- afterEach(() => {
88
- if (originalEnv === undefined) delete process.env.ASTRYX_GAP_REPORT;
89
- else process.env.ASTRYX_GAP_REPORT = originalEnv;
90
- vi.resetModules();
91
- });
92
-
93
- it('disables gap reporting when set to "off"', async () => {
94
- process.env.ASTRYX_GAP_REPORT = 'off';
95
- vi.resetModules();
96
- const {loadGapReportConfig, createGapReport} =
97
- await import('../utils/github.mjs');
98
- await expect(loadGapReportConfig()).resolves.toMatchObject({
99
- enabled: false,
100
- });
101
-
102
- // createGapReport must return null and never invoke gh.
103
- const result = await createGapReport({
104
- component: 'Button',
105
- category: 'other',
106
- intention: 'test',
107
- });
108
- expect(result).toBeNull();
109
- });
110
-
111
- it('also accepts "false" and "0" as disable values', async () => {
112
- process.env.ASTRYX_GAP_REPORT = 'false';
113
- vi.resetModules();
114
- let mod = await import('../utils/github.mjs');
115
- await expect(mod.loadGapReportConfig()).resolves.toMatchObject({
116
- enabled: false,
117
- });
118
-
119
- process.env.ASTRYX_GAP_REPORT = '0';
120
- vi.resetModules();
121
- mod = await import('../utils/github.mjs');
122
- await expect(mod.loadGapReportConfig()).resolves.toMatchObject({
123
- enabled: false,
124
- });
125
- });
126
- });
127
-
128
- describe('buildGapReportPreview', () => {
129
- let originalEnv;
130
- beforeEach(() => {
131
- originalEnv = process.env.ASTRYX_GAP_REPORT;
132
- // Force github mode (default) for these tests.
133
- delete process.env.ASTRYX_GAP_REPORT;
134
- });
135
- afterEach(() => {
136
- if (originalEnv === undefined) delete process.env.ASTRYX_GAP_REPORT;
137
- else process.env.ASTRYX_GAP_REPORT = originalEnv;
138
- });
139
-
140
- it('renders title and body without invoking gh', async () => {
141
- vi.resetModules();
142
- const {buildGapReportPreview} = await import('../utils/github.mjs');
143
- const preview = await buildGapReportPreview({
144
- component: 'Button',
145
- category: 'missing_variant',
146
- intention: 'Need compact variant for tables',
147
- });
148
- expect(preview.mode).toBe('github');
149
- expect(preview.enabled).toBe(true);
150
- expect(preview.title).toBe('[gap] Button: Need compact variant for tables');
151
- expect(preview.body).toContain('| **Component** | Button |');
152
- expect(preview.body).toContain('Need compact variant for tables');
153
- expect(preview.repo).toBe('facebookexperimental/xds');
154
- });
155
-
156
- it('reports disabled mode when env var is off', async () => {
157
- process.env.ASTRYX_GAP_REPORT = 'off';
158
- vi.resetModules();
159
- const {buildGapReportPreview} = await import('../utils/github.mjs');
160
- const preview = await buildGapReportPreview({
161
- component: 'Button',
162
- category: 'other',
163
- intention: 'x',
164
- });
165
- expect(preview.mode).toBe('disabled');
166
- expect(preview.enabled).toBe(false);
167
- });
168
- });