@astryxdesign/cli 0.1.7 → 0.1.8-canary.0c4d8d7

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 (134) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +115 -19
  3. package/docs/cli-integrations.doc.mjs +150 -0
  4. package/docs/elevation.doc.mjs +79 -1
  5. package/docs/getting-started.doc.mjs +9 -9
  6. package/docs/migration.doc.mjs +18 -18
  7. package/docs/principles.doc.dense.mjs +1 -1
  8. package/docs/principles.doc.mjs +6 -6
  9. package/docs/principles.doc.zh.mjs +1 -1
  10. package/docs/styling-libraries.doc.mjs +3 -3
  11. package/docs/styling.doc.mjs +4 -4
  12. package/docs/theme.doc.dense.mjs +2 -2
  13. package/docs/theme.doc.mjs +7 -7
  14. package/docs/theme.doc.zh.mjs +1 -1
  15. package/docs/tokens.doc.mjs +1 -1
  16. package/docs/working-with-ai.doc.mjs +19 -19
  17. package/package.json +13 -11
  18. package/scripts/postinstall.mjs +74 -0
  19. package/src/api/doctor.mjs +3 -3
  20. package/src/api/error.mjs +2 -2
  21. package/src/codemods/__tests__/registry.test.mjs +1 -0
  22. package/src/codemods/ensure-jscodeshift.mjs +11 -27
  23. package/src/codemods/registry.mjs +1 -0
  24. package/src/codemods/run-codemod.mjs +1 -1
  25. package/src/codemods/runner.mjs +2 -2
  26. package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
  27. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
  28. package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
  29. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
  30. package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
  31. package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
  32. package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
  33. package/src/commands/agent-docs.mjs +136 -20
  34. package/src/commands/agent-docs.test.mjs +123 -10
  35. package/src/commands/build-theme.mjs +10 -71
  36. package/src/commands/build.mjs +15 -15
  37. package/src/commands/cli-postinstall.test.mjs +42 -0
  38. package/src/commands/component/index.mjs +4 -4
  39. package/src/commands/discover.mjs +7 -5
  40. package/src/commands/docs.mjs +4 -4
  41. package/src/commands/hook/index.mjs +4 -4
  42. package/src/commands/init.mjs +48 -152
  43. package/src/commands/init.next-steps.test.mjs +1 -1
  44. package/src/commands/interactive-guard.test.mjs +19 -22
  45. package/src/commands/json-contract.test.mjs +2 -2
  46. package/src/commands/layout.mjs +1 -1
  47. package/src/commands/search.mjs +4 -4
  48. package/src/commands/setup-nudge.test.mjs +108 -0
  49. package/src/commands/swizzle.mjs +12 -35
  50. package/src/commands/template.mjs +11 -31
  51. package/src/commands/upgrade.mjs +138 -32
  52. package/src/commands/upgrade.test.mjs +155 -1
  53. package/src/index.mjs +40 -6
  54. package/src/lib/cli-error.mjs +2 -2
  55. package/src/lib/component-format.mjs +2 -1
  56. package/src/lib/json-shim.mjs +1 -1
  57. package/src/lib/json.mjs +3 -3
  58. package/src/lib/term-log.mjs +48 -0
  59. package/src/types/api.d.ts +4 -9
  60. package/src/types/base.d.ts +24 -3
  61. package/src/types/build.d.ts +23 -0
  62. package/src/types/index.d.ts +1 -0
  63. package/src/types/swizzle.d.ts +4 -0
  64. package/src/types/template-api.d.ts +4 -1
  65. package/src/types/upgrade.d.ts +39 -2
  66. package/src/utils/package-manager.mjs +108 -4
  67. package/src/utils/package-manager.test.mjs +108 -1
  68. package/src/utils/path-safety.mjs +0 -18
  69. package/src/utils/update-check.mjs +5 -4
  70. package/src/utils/update-check.test.mjs +3 -3
  71. package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
  72. package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
  73. package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
  74. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
  75. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
  76. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
  77. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
  78. package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
  79. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
  80. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
  81. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
  82. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
  83. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
  84. package/templates/blocks/components/Banner/BannerFloating.doc.mjs +14 -0
  85. package/templates/blocks/components/Banner/BannerFloating.tsx +16 -0
  86. package/templates/blocks/components/Button/ButtonFloating.doc.mjs +14 -0
  87. package/templates/blocks/components/Button/ButtonFloating.tsx +37 -0
  88. package/templates/blocks/components/ButtonGroup/ButtonGroupFloating.doc.mjs +14 -0
  89. package/templates/blocks/components/ButtonGroup/ButtonGroupFloating.tsx +23 -0
  90. package/templates/blocks/components/Card/CardElevations.doc.mjs +14 -0
  91. package/templates/blocks/components/Card/CardElevations.tsx +32 -0
  92. package/templates/blocks/components/Card/ClickableCardElevated.doc.mjs +14 -0
  93. package/templates/blocks/components/Card/ClickableCardElevated.tsx +21 -0
  94. package/templates/blocks/components/Card/SelectableCardElevated.doc.mjs +14 -0
  95. package/templates/blocks/components/Card/SelectableCardElevated.tsx +37 -0
  96. package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
  97. package/templates/blocks/components/ChatComposer/ChatComposerFlat.doc.mjs +14 -0
  98. package/templates/blocks/components/ChatComposer/ChatComposerFlat.tsx +75 -0
  99. package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
  100. package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
  101. package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
  102. package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
  103. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
  104. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
  105. package/templates/blocks/components/CodeBlock/CodeBlockTerminal.tsx +1 -1
  106. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
  107. package/templates/blocks/components/IconButton/IconButtonFloating.doc.mjs +14 -0
  108. package/templates/blocks/components/IconButton/IconButtonFloating.tsx +37 -0
  109. package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
  110. package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
  111. package/templates/blocks/components/OverflowList/OverflowListCappedToolbar.doc.mjs +14 -0
  112. package/templates/blocks/components/OverflowList/OverflowListCappedToolbar.tsx +39 -0
  113. package/templates/blocks/components/OverflowList/OverflowListMultiRowTags.doc.mjs +14 -0
  114. package/templates/blocks/components/OverflowList/OverflowListMultiRowTags.tsx +44 -0
  115. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
  116. package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
  117. package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
  118. package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
  119. package/templates/blocks/components/Table/TableRowStatusTable.doc.mjs +14 -0
  120. package/templates/blocks/components/Table/TableRowStatusTable.tsx +59 -0
  121. package/templates/blocks/components/Thumbnail/ThumbnailElevated.doc.mjs +14 -0
  122. package/templates/blocks/components/Thumbnail/ThumbnailElevated.tsx +31 -0
  123. package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
  124. package/templates/pages/ai-chat/page.tsx +4 -4
  125. package/templates/pages/dashboard-portfolio/page.tsx +3 -11
  126. package/templates/pages/detail-page/page.tsx +5 -12
  127. package/templates/pages/documentation-design/page.tsx +1 -1
  128. package/templates/pages/messaging-shell/page.tsx +6 -8
  129. package/templates/pages/table-grouped/page.tsx +9 -14
  130. package/templates/pages/table-page/page.tsx +7 -9
  131. package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
  132. package/docs/integration-authoring.md +0 -105
  133. package/src/utils/interactive.mjs +0 -76
  134. package/src/utils/interactive.test.mjs +0 -70
@@ -337,7 +337,7 @@ function StreamMessageGroup({message}: {message: StreamMessage}) {
337
337
  return (
338
338
  <ChatMessage
339
339
  sender={isSelf ? 'user' : 'assistant'}
340
- avatar={isSelf ? undefined : <Avatar name={user.name} size="small" />}>
340
+ avatar={isSelf ? undefined : <Avatar name={user.name} size="md" />}>
341
341
  {message.bubbles.map((text, index) => (
342
342
  <ChatMessageBubble
343
343
  key={`${message.id}-${index}`}
@@ -395,7 +395,7 @@ export default function MessagingShellPage() {
395
395
 
396
396
  const workspaceRail = (
397
397
  <VStack gap={2} style={styles.rail}>
398
- <Avatar name="Astryx HQ" size="small" />
398
+ <Avatar name="Astryx HQ" size="md" />
399
399
  {RAIL_ITEMS.map(item => (
400
400
  <IconButton
401
401
  key={item.id}
@@ -416,7 +416,7 @@ export default function MessagingShellPage() {
416
416
  />
417
417
  <Avatar
418
418
  name={USERS.you.name}
419
- size="small"
419
+ size="md"
420
420
  status={<AvatarStatusDot variant="success" label="Online" />}
421
421
  />
422
422
  </VStack>
@@ -497,7 +497,7 @@ export default function MessagingShellPage() {
497
497
  startContent={
498
498
  <Avatar
499
499
  name={USERS[dm.userId].name}
500
- size="xsmall"
500
+ size="sm"
501
501
  status={
502
502
  <AvatarStatusDot
503
503
  variant={PRESENCE_VARIANT[dm.presence]}
@@ -600,9 +600,7 @@ export default function MessagingShellPage() {
600
600
  <ChatMessageList density="compact">
601
601
  <ChatMessage
602
602
  sender="assistant"
603
- avatar={
604
- <Avatar name={USERS[THREAD_ROOT.userId].name} size="small" />
605
- }>
603
+ avatar={<Avatar name={USERS[THREAD_ROOT.userId].name} size="md" />}>
606
604
  <ChatMessageBubble
607
605
  name={USERS[THREAD_ROOT.userId].name}
608
606
  metadata={
@@ -622,7 +620,7 @@ export default function MessagingShellPage() {
622
620
  <ChatMessage
623
621
  key={reply.id}
624
622
  sender="assistant"
625
- avatar={<Avatar name={USERS[reply.userId].name} size="small" />}>
623
+ avatar={<Avatar name={USERS[reply.userId].name} size="md" />}>
626
624
  <ChatMessageBubble
627
625
  name={USERS[reply.userId].name}
628
626
  metadata={
@@ -22,7 +22,10 @@ import {Badge} from '@astryxdesign/core/Badge';
22
22
  import {Avatar} from '@astryxdesign/core/Avatar';
23
23
  import {Selector} from '@astryxdesign/core/Selector';
24
24
  import {PowerSearch} from '@astryxdesign/core/PowerSearch';
25
- import type {PowerSearchConfig, PowerSearchFilter} from '@astryxdesign/core/PowerSearch';
25
+ import type {
26
+ PowerSearchConfig,
27
+ PowerSearchFilter,
28
+ } from '@astryxdesign/core/PowerSearch';
26
29
  import {Dialog, DialogHeader} from '@astryxdesign/core/Dialog';
27
30
  import {Popover} from '@astryxdesign/core/Popover';
28
31
  import {RadioList, RadioListItem} from '@astryxdesign/core/RadioList';
@@ -798,19 +801,15 @@ function TaskDetailPanel({
798
801
  </MetadataListItem>
799
802
  <MetadataListItem label="Assignee">
800
803
  <HStack gap={2} vAlign="center">
801
- <Avatar name={task.assignee} size="xsmall" />
804
+ <Avatar name={task.assignee} size="sm" />
802
805
  <Text type="body">{task.assignee}</Text>
803
806
  </HStack>
804
807
  </MetadataListItem>
805
808
  <MetadataListItem label="Project">
806
809
  {task.project || '\u2014'}
807
810
  </MetadataListItem>
808
- <MetadataListItem label="Created">
809
- {task.created}
810
- </MetadataListItem>
811
- <MetadataListItem label="Updated">
812
- {task.updated}
813
- </MetadataListItem>
811
+ <MetadataListItem label="Created">{task.created}</MetadataListItem>
812
+ <MetadataListItem label="Updated">{task.updated}</MetadataListItem>
814
813
  </MetadataList>
815
814
 
816
815
  {task.tags.length > 0 && (
@@ -942,11 +941,7 @@ export default function DataTableTemplate() {
942
941
  </RadioList>
943
942
  </VStack>
944
943
  }>
945
- <Button
946
- label="View Options"
947
- variant="secondary"
948
- size="md"
949
- />
944
+ <Button label="View Options" variant="secondary" size="md" />
950
945
  </Popover>
951
946
  </HStack>
952
947
  </VStack>
@@ -1066,7 +1061,7 @@ export default function DataTableTemplate() {
1066
1061
  </Text>
1067
1062
  </TableCell>
1068
1063
  <TableCell>
1069
- <Avatar name={task.assignee} size="xsmall" />
1064
+ <Avatar name={task.assignee} size="sm" />
1070
1065
  </TableCell>
1071
1066
  <TableCell>
1072
1067
  <DropdownMenu
@@ -16,7 +16,10 @@ import {Button} from '@astryxdesign/core/Button';
16
16
  import {IconButton} from '@astryxdesign/core/IconButton';
17
17
  import {Icon} from '@astryxdesign/core/Icon';
18
18
  import {Avatar} from '@astryxdesign/core/Avatar';
19
- import {PowerSearch, usePowerSearchConfig} from '@astryxdesign/core/PowerSearch';
19
+ import {
20
+ PowerSearch,
21
+ usePowerSearchConfig,
22
+ } from '@astryxdesign/core/PowerSearch';
20
23
  import type {PowerSearchFilter} from '@astryxdesign/core/PowerSearch';
21
24
  import {Table, proportional, pixel} from '@astryxdesign/core/Table';
22
25
  import type {TableColumn} from '@astryxdesign/core/Table';
@@ -353,7 +356,7 @@ const columns: TableColumn<DogRow>[] = [
353
356
  width: proportional(2),
354
357
  renderCell: (item: DogRow) => (
355
358
  <HStack gap={3} vAlign="center">
356
- <Avatar name={item.name} size="small" />
359
+ <Avatar name={item.name} size="md" />
357
360
  <VStack gap={0}>
358
361
  <Text type="body">{item.name}</Text>
359
362
  <Text type="supporting" color="secondary">
@@ -367,9 +370,7 @@ const columns: TableColumn<DogRow>[] = [
367
370
  key: 'biography',
368
371
  header: 'Biography',
369
372
  width: proportional(5),
370
- renderCell: (item: DogRow) => (
371
- <Text type="body">{item.biography}</Text>
372
- ),
373
+ renderCell: (item: DogRow) => <Text type="body">{item.biography}</Text>,
373
374
  },
374
375
  {
375
376
  key: 'age',
@@ -406,10 +407,7 @@ export default function TablePageTemplate() {
406
407
  icon={<Icon icon={ArrowDownTrayIcon} size="sm" />}
407
408
  variant="ghost"
408
409
  />
409
- <Button
410
- label="Add"
411
- icon={<Icon icon={PlusIcon} size="sm" />}
412
- />
410
+ <Button label="Add" icon={<Icon icon={PlusIcon} size="sm" />} />
413
411
  </HStack>
414
412
  </LayoutHeader>
415
413
  }
@@ -338,17 +338,13 @@ const columns: TableColumn<IncidentRow>[] = [
338
338
  key: 'product',
339
339
  header: 'Product',
340
340
  width: proportional(2),
341
- renderCell: (item: IncidentRow) => (
342
- <Text type="body">{item.product}</Text>
343
- ),
341
+ renderCell: (item: IncidentRow) => <Text type="body">{item.product}</Text>,
344
342
  },
345
343
  {
346
344
  key: 'title',
347
345
  header: 'Description',
348
346
  width: proportional(5),
349
- renderCell: (item: IncidentRow) => (
350
- <Text type="body">{item.title}</Text>
351
- ),
347
+ renderCell: (item: IncidentRow) => <Text type="body">{item.title}</Text>,
352
348
  },
353
349
  {
354
350
  key: 'startTime',
@@ -362,9 +358,7 @@ const columns: TableColumn<IncidentRow>[] = [
362
358
  key: 'duration',
363
359
  header: 'Duration',
364
360
  width: pixel(100),
365
- renderCell: (item: IncidentRow) => (
366
- <Text type="body">{item.duration}</Text>
367
- ),
361
+ renderCell: (item: IncidentRow) => <Text type="body">{item.duration}</Text>,
368
362
  },
369
363
  {
370
364
  key: 'oncall',
@@ -375,7 +369,7 @@ const columns: TableColumn<IncidentRow>[] = [
375
369
  <Avatar
376
370
  name={item.oncall}
377
371
  src={ONCALL_AVATARS[item.oncall]}
378
- size="xsmall"
372
+ size="sm"
379
373
  />
380
374
  <Text type="body">{item.oncall}</Text>
381
375
  </HStack>
@@ -396,9 +390,7 @@ const columns: TableColumn<IncidentRow>[] = [
396
390
  key: 'date',
397
391
  header: 'Date',
398
392
  width: pixel(110),
399
- renderCell: (item: IncidentRow) => (
400
- <Text type="body">{item.date}</Text>
401
- ),
393
+ renderCell: (item: IncidentRow) => <Text type="body">{item.date}</Text>,
402
394
  },
403
395
  ];
404
396
 
@@ -1,105 +0,0 @@
1
- # Authoring an Astryx Integration
2
-
3
- > **Status:** working notes. This should eventually move to the public wiki
4
- > alongside the rest of the integration-authoring guidance; it lives here for
5
- > now so it ships and is versioned with the CLI.
6
-
7
- An **Integration** is an npm package that contributes components, templates, and/or
8
- codemods to a consumer's design-system workflow. Consumers install the 3rd party
9
- package, add a line to their astryx.config file:
10
-
11
- ```js
12
- import {createConfig} from '@astryxdesign/cli/config';
13
-
14
- export default createConfig({
15
- integrations: ['@acme/astryx-widgets'],
16
- ...
17
- });
18
- ```
19
-
20
- Then the integration's components and templates will be surfaced alongside Astryx
21
- components in the Astryx CLI.
22
-
23
- ```sh
24
- astryx component AcmeCarousel --props
25
- astryx component --list --package @acme/astryx-widgets
26
- ```
27
-
28
- ## The Integration File
29
-
30
- In order to register your package as an Astryx Integration, create an
31
- `astryx.integration.{ts,mjs,js}` file as a sibling to your `package.json`. This file
32
- tells Astryx where to find your components, templates, codemods, etc.
33
-
34
- ```js
35
- // astryx.integration.{ts,mjs,js}
36
- import {createIntegration} from '@astryxdesign/cli/integration';
37
-
38
- export default createIntegration({
39
- components: './components',
40
- templates: './templates',
41
- codemods: './codemods',
42
- issuesUrl: 'https://github.com/acme/widgets/issues',
43
- });
44
- ```
45
-
46
- ## Components
47
-
48
- Your components themselves may be exported from your library as you see fit (consumers
49
- will still import them from your package) but Astryx CLI will look for a .doc.{ts,mjs,js}
50
- file with the same stem e.g. `AcmeCarousel.tsx` and `AcmeCarousel.doc.ts`.
51
-
52
- ```js
53
- // AcmeCarousel.doc.ts
54
- import {createComponentDoc} from '@astryxdesign/cli/doc';
55
-
56
- export default createComponentDoc({
57
- name: 'AcmeCarousel',
58
- description: '...',
59
- ...
60
- });
61
- ```
62
-
63
- ## Templates
64
-
65
- Templates are typically not exported from the package directly, but instead accessed
66
- via the Astryx CLI. Consumers can look through your templates and materialize them
67
- into their apps.
68
-
69
- You define a template with the `createPageTemplate` (for full pages) or `createBlockTemplate`
70
- (for smaller chunks). e.g. `AcmeLandingPage.tsx`, `AcmeLandingPage.template.ts`
71
-
72
- ```js
73
- // AcmeLandingPage.template.ts
74
- import {createPageTemplate} from '@astryxdesign/cli/template';
75
-
76
- export default createPageTemplate({
77
- ...
78
- });
79
- ```
80
-
81
- Note that, since the CLI needs access to the template source code, you need to make sure
82
- that it is included in your published package. This will also allow us to render previews
83
- of templates in the future by bundling your template into a doc site build.
84
-
85
- Typically, this is done via the package.json `exports` key.
86
-
87
- ```jsonc
88
- {
89
- "exports": {
90
- // ...
91
- "./templates/*.tsx": "./templates/*.tsx",
92
- },
93
- }
94
- ```
95
-
96
- In order to verify that it's working, you can test importing the template component like this:
97
-
98
- ```ts
99
- import('@acme/astryx-widgets/templates/AcmeLandingPage.tsx');
100
- ```
101
-
102
- Import **with the `.tsx` extension** — an extensionless specifier won't resolve
103
- under `moduleResolution: bundler`. The extensionful `"./templates/*.tsx"` export
104
- above is what lets that import type-check without consumers enabling
105
- `allowImportingTsExtensions`.
@@ -1,76 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Interactivity contract for the CLI.
5
- *
6
- * A single source of truth for "can this process prompt the user?". Several
7
- * commands launch @clack/prompts wizards; in a non-interactive context (CI,
8
- * piped stdin/stdout, no TTY) those prompts block forever. Historically each
9
- * command answered this question differently — some checked `stdout.isTTY`,
10
- * one checked `stdin.isTTY`, one added `!process.env.CI`, and some had no
11
- * guard at all. This module centralizes the check so every command behaves
12
- * identically.
13
- *
14
- * Two entry points, for two situations:
15
- *
16
- * - `requireInteractive()` — for commands whose prompt IS the work
17
- * (e.g. `astryx init`, `astryx theme`). With no TTY there is nothing to do, so
18
- * fail fast (exit 1) with actionable, non-interactive guidance.
19
- *
20
- * - `isInteractive()` — for commands with an OPTIONAL secondary prompt
21
- * that runs after the primary work has already succeeded; callers use
22
- * this to skip the prompt gracefully in non-interactive contexts.
23
- */
24
-
25
- /**
26
- * True when the process can safely run an interactive prompt.
27
- *
28
- * Requires BOTH stdin and stdout to be TTYs (clack reads stdin and renders to
29
- * stdout) and that we are not in a CI environment. A CI runner may allocate a
30
- * pseudo-TTY, so `process.env.CI` is an explicit override: never prompt in CI.
31
- *
32
- * @param {object} [env] - Override hook for tests.
33
- * @param {boolean} [env.stdinTTY=process.stdin.isTTY]
34
- * @param {boolean} [env.stdoutTTY=process.stdout.isTTY]
35
- * @param {boolean} [env.ci=Boolean(process.env.CI)]
36
- * @returns {boolean}
37
- */
38
- export function isInteractive({
39
- stdinTTY = Boolean(process.stdin && process.stdin.isTTY),
40
- stdoutTTY = Boolean(process.stdout && process.stdout.isTTY),
41
- ci = Boolean(process.env.CI),
42
- } = {}) {
43
- if (ci) return false;
44
- return stdinTTY && stdoutTTY;
45
- }
46
-
47
- /**
48
- * Guard for commands whose primary action is an interactive wizard. When the
49
- * process is non-interactive, prints an actionable error and exits 1 instead
50
- * of hanging on a prompt that will never receive input.
51
- *
52
- * @param {object} options
53
- * @param {string} options.command - Command name for the message, e.g. 'init'.
54
- * @param {string} options.hint - Concrete non-interactive invocation, e.g.
55
- * '`pnpm astryx init --all` or `--features agents,theme,template`'.
56
- * @param {boolean} [options.json=false] - When true, the command does not
57
- * support --json; we still exit 1 but skip the human-formatted guidance.
58
- * @param {object} [env] - Forwarded to isInteractive (test hook).
59
- * @returns {void} Returns when interactive; otherwise calls process.exit(1).
60
- */
61
- export function requireInteractive({command, hint, json = false} = {}, env) {
62
- if (isInteractive(env)) return;
63
- const name = command ? `astryx ${command}` : 'this command';
64
- console.error(
65
- `Error: \`${name}\` with no flags is interactive and requires a TTY.`,
66
- );
67
- if (hint) {
68
- console.error(`Run non-interactively with: ${hint}`);
69
- }
70
- if (!json) {
71
- console.error(
72
- 'Detected a non-interactive environment (no TTY, piped I/O, or CI=1).',
73
- );
74
- }
75
- process.exit(1);
76
- }
@@ -1,70 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Unit tests for the interactivity contract.
5
- *
6
- * isInteractive() is pure given its injected env, so these are fast unit
7
- * tests. The "does the command actually fail fast instead of hanging" proof
8
- * lives in the per-command subprocess tests (init/theme non-interactive).
9
- */
10
-
11
- import {describe, it, expect, vi, afterEach} from 'vitest';
12
- import {isInteractive, requireInteractive} from './interactive.mjs';
13
-
14
- describe('isInteractive', () => {
15
- it('is true only when stdin AND stdout are TTYs and not CI', () => {
16
- expect(isInteractive({stdinTTY: true, stdoutTTY: true, ci: false})).toBe(true);
17
- });
18
-
19
- it('is false when stdin is not a TTY (piped input)', () => {
20
- expect(isInteractive({stdinTTY: false, stdoutTTY: true, ci: false})).toBe(false);
21
- });
22
-
23
- it('is false when stdout is not a TTY (piped output)', () => {
24
- expect(isInteractive({stdinTTY: true, stdoutTTY: false, ci: false})).toBe(false);
25
- });
26
-
27
- it('is false in CI even with a pseudo-TTY on both streams', () => {
28
- expect(isInteractive({stdinTTY: true, stdoutTTY: true, ci: true})).toBe(false);
29
- });
30
- });
31
-
32
- describe('requireInteractive', () => {
33
- afterEach(() => {
34
- vi.restoreAllMocks();
35
- });
36
-
37
- it('returns (does not exit) when interactive', () => {
38
- const exit = vi.spyOn(process, 'exit').mockImplementation(() => {
39
- throw new Error('exit should not be called');
40
- });
41
- expect(() =>
42
- requireInteractive(
43
- {command: 'init', hint: '`astryx init --all`'},
44
- {stdinTTY: true, stdoutTTY: true, ci: false},
45
- ),
46
- ).not.toThrow();
47
- expect(exit).not.toHaveBeenCalled();
48
- });
49
-
50
- it('exits 1 with actionable guidance when non-interactive', () => {
51
- const exit = vi
52
- .spyOn(process, 'exit')
53
- .mockImplementation(() => {
54
- throw new Error('__exit__');
55
- });
56
- const err = vi.spyOn(console, 'error').mockImplementation(() => {});
57
- expect(() =>
58
- requireInteractive(
59
- {command: 'theme', hint: '`astryx theme <preset>`'},
60
- {stdinTTY: false, stdoutTTY: false, ci: false},
61
- ),
62
- ).toThrow('__exit__');
63
- expect(exit).toHaveBeenCalledWith(1);
64
- const output = err.mock.calls.map(c => c.join(' ')).join('\n');
65
- expect(output).toMatch(/requires a TTY/i);
66
- expect(output).toMatch(/astryx theme <preset>/);
67
- expect(output).toMatch(/`astryx theme`/);
68
- expect(output).not.toMatch(/\bxds\b/);
69
- });
70
- });