@astryxdesign/cli 0.1.6 → 0.1.7-canary.04cd8f7

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 (107) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +115 -19
  3. package/docs/cli-integrations.doc.mjs +150 -0
  4. package/docs/getting-started.doc.mjs +9 -9
  5. package/docs/internationalization.doc.mjs +243 -0
  6. package/docs/layout.doc.dense.mjs +5 -0
  7. package/docs/migration.doc.mjs +18 -18
  8. package/docs/principles.doc.dense.mjs +5 -5
  9. package/docs/principles.doc.mjs +6 -6
  10. package/docs/principles.doc.zh.mjs +5 -5
  11. package/docs/styling-libraries.doc.mjs +3 -3
  12. package/docs/styling.doc.mjs +4 -4
  13. package/docs/theme.doc.dense.mjs +12 -12
  14. package/docs/theme.doc.mjs +7 -7
  15. package/docs/theme.doc.zh.mjs +10 -10
  16. package/docs/tokens.doc.dense.mjs +6 -7
  17. package/docs/tokens.doc.mjs +1 -1
  18. package/docs/tokens.doc.zh.mjs +6 -7
  19. package/docs/working-with-ai.doc.mjs +18 -18
  20. package/package.json +13 -10
  21. package/src/api/docOverlays.test.mjs +133 -0
  22. package/src/api/docs.mjs +14 -2
  23. package/src/api/doctor.mjs +3 -3
  24. package/src/api/integration-block-exports.test.mjs +240 -0
  25. package/src/api/template-suffix.test.mjs +246 -0
  26. package/src/api/template.mjs +104 -28
  27. package/src/api/validate-integration.mjs +0 -8
  28. package/src/codemods/__tests__/registry.test.mjs +1 -0
  29. package/src/codemods/ensure-jscodeshift.mjs +11 -27
  30. package/src/codemods/registry.mjs +1 -0
  31. package/src/codemods/run-codemod.mjs +1 -1
  32. package/src/codemods/runner.mjs +2 -2
  33. package/src/codemods/transforms/v0.1.7/__tests__/migrate-table-tableprops-to-direct-props.test.mjs +120 -0
  34. package/src/codemods/transforms/v0.1.7/__tests__/rename-table-renderprops-styles-to-xstyle.test.mjs +112 -0
  35. package/src/codemods/transforms/v0.1.7/index.mjs +27 -0
  36. package/src/codemods/transforms/v0.1.7/migrate-table-tableprops-to-direct-props.mjs +188 -0
  37. package/src/codemods/transforms/v0.1.7/rename-table-renderprops-styles-to-xstyle.mjs +197 -0
  38. package/src/commands/agent-docs.mjs +21 -8
  39. package/src/commands/agent-docs.test.mjs +31 -4
  40. package/src/commands/build-theme.mjs +10 -71
  41. package/src/commands/build.mjs +15 -15
  42. package/src/commands/component/index.mjs +4 -4
  43. package/src/commands/discover.mjs +7 -5
  44. package/src/commands/docs.mjs +4 -4
  45. package/src/commands/hook/index.mjs +4 -4
  46. package/src/commands/init.mjs +48 -152
  47. package/src/commands/init.next-steps.test.mjs +1 -1
  48. package/src/commands/interactive-guard.test.mjs +19 -22
  49. package/src/commands/json-contract.test.mjs +1 -1
  50. package/src/commands/layout.mjs +1 -1
  51. package/src/commands/search.mjs +4 -4
  52. package/src/commands/swizzle.mjs +11 -34
  53. package/src/commands/template.mjs +11 -31
  54. package/src/commands/upgrade.mjs +9 -6
  55. package/src/commands/upgrade.test.mjs +1 -1
  56. package/src/config.mjs +5 -14
  57. package/src/doc.mjs +27 -0
  58. package/src/doc.test.mjs +383 -0
  59. package/src/index.mjs +5 -6
  60. package/src/integration.mjs +4 -15
  61. package/src/lib/component-discovery.importpath.test.mjs +59 -0
  62. package/src/lib/component-discovery.mjs +15 -5
  63. package/src/lib/component-format.mjs +47 -14
  64. package/src/lib/component-format.test.mjs +95 -1
  65. package/src/lib/component-loader.mjs +104 -2
  66. package/src/lib/componentDocOverlay.test.mjs +111 -0
  67. package/src/lib/config-schema.mjs +0 -30
  68. package/src/lib/hook-format.mjs +8 -3
  69. package/src/lib/term-log.mjs +48 -0
  70. package/src/lib/xle/registry.mjs +0 -5
  71. package/src/schemas/doc-schema.mjs +226 -0
  72. package/src/schemas/template-schema.mjs +47 -0
  73. package/src/template.mjs +9 -67
  74. package/src/types/config.d.ts +11 -66
  75. package/src/types/doc.d.ts +23 -0
  76. package/src/types/integration.d.ts +7 -18
  77. package/src/types/template-api.d.ts +14 -50
  78. package/src/utils/package-manager.mjs +78 -0
  79. package/src/utils/package-manager.test.mjs +108 -1
  80. package/src/utils/path-safety.mjs +0 -18
  81. package/src/utils/update-check.mjs +2 -1
  82. package/templates/blocks/components/Avatar/AvatarGroup.tsx +5 -7
  83. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -6
  84. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +3 -5
  85. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +8 -6
  86. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -5
  87. package/templates/blocks/components/ChatComposerInput/ChatComposerInputControlledInput.tsx +1 -1
  88. package/templates/blocks/components/ChatComposerInput/ChatComposerInputDisabled.tsx +1 -1
  89. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMentionTrigger.tsx +1 -1
  90. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMultipleTriggers.tsx +1 -1
  91. package/templates/blocks/components/ChatComposerInput/ChatComposerInputShowcase.tsx +1 -1
  92. package/templates/blocks/components/ChatComposerInput/ChatComposerInputSlashCommands.tsx +1 -1
  93. package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
  94. package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
  95. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +14 -0
  96. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +41 -0
  97. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +13 -0
  98. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +78 -0
  99. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +14 -0
  100. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +38 -0
  101. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +14 -0
  102. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +47 -0
  103. package/templates/pages/ide/page.tsx +35 -41
  104. package/templates/pages/theme-showcase/page.tsx +7 -7
  105. package/templates/themes/neutral/neutralTheme.ts +63 -32
  106. package/src/utils/interactive.mjs +0 -76
  107. package/src/utils/interactive.test.mjs +0 -70
@@ -0,0 +1,47 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {VisuallyHidden} from '@astryxdesign/core/VisuallyHidden';
6
+ import {Card} from '@astryxdesign/core/Card';
7
+ import {HStack, VStack} from '@astryxdesign/core/Layout';
8
+ import {Text} from '@astryxdesign/core/Text';
9
+ import {Icon} from '@astryxdesign/core/Icon';
10
+ import {ArrowUpIcon, ArrowDownIcon} from '@heroicons/react/24/solid';
11
+
12
+ const stats = [
13
+ {label: 'Revenue', value: '$48.2k', delta: '+12%', direction: 'up'},
14
+ {label: 'Churn', value: '2.1%', delta: '-4%', direction: 'down'},
15
+ ] as const;
16
+
17
+ export default function VisuallyHiddenSupplementaryContext() {
18
+ return (
19
+ <HStack gap={4} wrap="wrap">
20
+ {stats.map(({label, value, delta, direction}) => (
21
+ <Card key={label} variant="muted">
22
+ <VStack gap={1}>
23
+ <Text type="supporting" color="secondary">
24
+ {label}
25
+ </Text>
26
+ <Text type="display-3">{value}</Text>
27
+ <HStack gap={1} vAlign="center">
28
+ <Icon
29
+ icon={direction === 'up' ? ArrowUpIcon : ArrowDownIcon}
30
+ size="sm"
31
+ color={direction === 'up' ? 'accent' : 'secondary'}
32
+ />
33
+ <Text type="body">
34
+ {delta}
35
+ {/* The arrow is decorative; spell out the trend for AT. */}
36
+ <VisuallyHidden>
37
+ {direction === 'up' ? ' increase' : ' decrease'} from last
38
+ month
39
+ </VisuallyHidden>
40
+ </Text>
41
+ </HStack>
42
+ </VStack>
43
+ </Card>
44
+ ))}
45
+ </HStack>
46
+ );
47
+ }
@@ -69,8 +69,8 @@ const styles: Record<string, CSSProperties> = {
69
69
  propertyActions: {
70
70
  marginTop: 'auto',
71
71
  },
72
- terminalArea: {
73
- height: '100%',
72
+ terminalPanel: {
73
+ flexShrink: 0,
74
74
  overflow: 'hidden',
75
75
  },
76
76
  };
@@ -301,46 +301,40 @@ export default function ResizableWorkspacePage() {
301
301
  label="Resize terminal"
302
302
  />
303
303
  {!bottomPanel.isCollapsed && (
304
- <div
305
- style={{
306
- height: bottomPanel.size,
307
- flexShrink: 0,
308
- overflow: 'hidden',
309
- }}>
310
- <Stack
311
- direction="vertical"
312
- style={styles.contentFill}>
313
- <TabList
314
- value={activeTermTab}
315
- onChange={val => setActiveTermTab(val)}
304
+ <Stack
305
+ direction="vertical"
306
+ height={bottomPanel.size}
307
+ style={styles.terminalPanel}>
308
+ <TabList
309
+ value={activeTermTab}
310
+ onChange={val => setActiveTermTab(val)}
311
+ size="sm"
312
+ hasDivider={false}
313
+ style={styles.tabListPadding}>
314
+ <Tab label="Terminal" value="terminal" />
315
+ <Tab label="Problems" value="problems" />
316
+ <Tab label="Output" value="output" />
317
+ <Tab label="Debug" value="debug" />
318
+ </TabList>
319
+ <StackItem
320
+ size="fill"
321
+ style={styles.terminalWrapper}>
322
+ <CodeBlock
323
+ code={TERMINAL_OUTPUT}
324
+ language="bash"
325
+ container="section"
326
+ hasLanguageLabel={false}
327
+ hasCopyButton={false}
316
328
  size="sm"
317
- hasDivider={false}
318
- style={styles.tabListPadding}>
319
- <Tab label="Terminal" value="terminal" />
320
- <Tab label="Problems" value="problems" />
321
- <Tab label="Output" value="output" />
322
- <Tab label="Debug" value="debug" />
323
- </TabList>
324
- <StackItem
325
- size="fill"
326
- style={styles.terminalWrapper}>
327
- <CodeBlock
328
- code={TERMINAL_OUTPUT}
329
- language="bash"
330
- container="section"
331
- hasLanguageLabel={false}
332
- hasCopyButton={false}
333
- size="sm"
334
- style={{
335
- width: '100%',
336
- height: '100%',
337
- borderWidth: 0,
338
- borderRadius: 0,
339
- }}
340
- />
341
- </StackItem>
342
- </Stack>
343
- </div>
329
+ style={{
330
+ width: '100%',
331
+ height: '100%',
332
+ borderWidth: 0,
333
+ borderRadius: 0,
334
+ }}
335
+ />
336
+ </StackItem>
337
+ </Stack>
344
338
  )}
345
339
  </Stack>
346
340
  </LayoutContent>
@@ -282,14 +282,14 @@ const DEFAULT_PRODUCTS: ProductSpec[] = [
282
282
 
283
283
  // Neutral product photos, served from the shared astryx asset CDN so the
284
284
  // scaffolded template renders real imagery without needing local public assets.
285
- const NEUTRAL_CDN = 'https://lookaside.facebook.com/assets/astryx';
286
285
  const DEFAULT_IMAGES: Record<string, string> = {
287
- watch: `${NEUTRAL_CDN}/Neutral-Watch.png`,
288
- headphones: `${NEUTRAL_CDN}/Neutral-Headphones.png`,
289
- backpack: `${NEUTRAL_CDN}/Neutral-Backpack.png`,
290
- wallet: `${NEUTRAL_CDN}/Neutral-Wallet.png`,
291
- tumbler: `${NEUTRAL_CDN}/Neutral-Tumbler.png`,
292
- throw_: `${NEUTRAL_CDN}/Neutral-Blanket.png`,
286
+ watch: 'https://lookaside.facebook.com/assets/astryx/Neutral-Watch.png',
287
+ headphones:
288
+ 'https://lookaside.facebook.com/assets/astryx/Neutral-Headphones.png',
289
+ backpack: 'https://lookaside.facebook.com/assets/astryx/Neutral-Backpack.png',
290
+ wallet: 'https://lookaside.facebook.com/assets/astryx/Neutral-Wallet.png',
291
+ tumbler: 'https://lookaside.facebook.com/assets/astryx/Neutral-Tumbler.png',
292
+ throw_: 'https://lookaside.facebook.com/assets/astryx/Neutral-Blanket.png',
293
293
  };
294
294
 
295
295
  export interface ThemeShowcaseProps {
@@ -39,19 +39,19 @@ import {neutralIconRegistry} from './icons';
39
39
  const neutralSyntax = defineSyntaxTheme({
40
40
  name: 'xds-neutral',
41
41
  tokens: {
42
- keyword: ['#700084', '#efa8ff'], // purple T30/T80
43
- string: ['#005600', '#a6d2a2'], // green (sat T30 / pastel T80)
44
- comment: ['#737373', '#a3a3a3'], // neutral
45
- number: ['#6e3500', '#ffb37f'], // orange
46
- function: ['#00458c', '#a0caff'], // blue T30/T80 H=255
47
- type: ['#700084', '#efa8ff'], // purple
48
- variable: ['#171717', '#e5e5e5'], // near-black / near-white
49
- operator: ['#737373', '#a3a3a3'], // neutral
50
- constant: ['#6e3500', '#ffb37f'], // orange
51
- tag: ['#89001a', '#ffaeaa'], // red
52
- attribute: ['#584400', '#eec12f'], // yellow
53
- property: ['#005348', '#83dac9'], // teal
54
- punctuation: ['#a3a3a3', '#525252'],// neutral
42
+ keyword: ['#700084', '#efa8ff'], // purple T30/T80
43
+ string: ['#005600', '#a6d2a2'], // green (sat T30 / pastel T80)
44
+ comment: ['#737373', '#a3a3a3'], // neutral
45
+ number: ['#6e3500', '#ffb37f'], // orange
46
+ function: ['#00458c', '#a0caff'], // blue T30/T80 H=255
47
+ type: ['#700084', '#efa8ff'], // purple
48
+ variable: ['#171717', '#e5e5e5'], // near-black / near-white
49
+ operator: ['#737373', '#a3a3a3'], // neutral
50
+ constant: ['#6e3500', '#ffb37f'], // orange
51
+ tag: ['#89001a', '#ffaeaa'], // red
52
+ attribute: ['#584400', '#eec12f'], // yellow
53
+ property: ['#005348', '#83dac9'], // teal
54
+ punctuation: ['#a3a3a3', '#525252'], // neutral
55
55
  background: ['#fafafa', '#0a0a0a'],
56
56
  },
57
57
  });
@@ -127,39 +127,39 @@ export const neutralTheme = defineTheme({
127
127
  // All values use the OKLCH Neutral tonal palette (chroma=0).
128
128
  // =========================================================================
129
129
  '--color-background-surface': ['#ffffff', '#262626'],
130
- '--color-background-body': ['#f1f1f1', '#1b1b1b'],
131
- '--color-background-card': ['#ffffff', '#1b1b1b'],
130
+ '--color-background-body': ['#f1f1f1', '#1b1b1b'],
131
+ '--color-background-card': ['#ffffff', '#1b1b1b'],
132
132
  '--color-background-popover': ['#ffffff', '#1b1b1b'],
133
- '--color-background-muted': ['#f1f1f1', '#1b1b1b'],
133
+ '--color-background-muted': ['#f1f1f1', '#1b1b1b'],
134
134
 
135
135
  // Accent + neutral surface tints (sit alongside backgrounds)
136
- '--color-accent': ['#262626', '#ebebeb'],
136
+ '--color-accent': ['#262626', '#ebebeb'],
137
137
  '--color-accent-muted': ['#f1f1f1', '#262626'],
138
- '--color-neutral': ['#0000000F', '#FFFFFF1A'],
138
+ '--color-neutral': ['#0000000F', '#FFFFFF1A'],
139
139
 
140
140
  // Overlays (modal scrims, hover/pressed tints)
141
- '--color-overlay': ['#00000080', '#000000CC'],
142
- '--color-overlay-hover': ['#0000000D', '#FFFFFF0D'],
141
+ '--color-overlay': ['#00000080', '#000000CC'],
142
+ '--color-overlay-hover': ['#0000000D', '#FFFFFF0D'],
143
143
  '--color-overlay-pressed': ['#0000001A', '#FFFFFF1A'],
144
144
 
145
145
  // Text
146
- '--color-text-primary': ['#171717', '#fafafa'],
146
+ '--color-text-primary': ['#171717', '#fafafa'],
147
147
  '--color-text-secondary': ['#737373', '#a3a3a3'],
148
- '--color-text-disabled': ['#a3a3a3', '#525252'],
149
- '--color-text-accent': ['#262626', '#ebebeb'],
150
- '--color-on-dark': '#ffffff',
151
- '--color-on-light': '#171717',
148
+ '--color-text-disabled': ['#a3a3a3', '#525252'],
149
+ '--color-text-accent': ['#262626', '#ebebeb'],
150
+ '--color-on-dark': '#ffffff',
151
+ '--color-on-light': '#171717',
152
152
  // Contrast: neutral accent is near-black (L) / near-white (D)
153
- '--color-on-accent': ['#ffffff', '#171717'],
153
+ '--color-on-accent': ['#ffffff', '#171717'],
154
154
  '--color-on-success': ['#ffffff', '#171717'],
155
- '--color-on-error': ['#ffffff', '#171717'],
155
+ '--color-on-error': ['#ffffff', '#171717'],
156
156
  '--color-on-warning': '#171717',
157
157
 
158
158
  // Icon
159
- '--color-icon-accent': ['#262626', '#ebebeb'],
160
- '--color-icon-primary': ['#171717', '#fafafa'],
159
+ '--color-icon-accent': ['#262626', '#ebebeb'],
160
+ '--color-icon-primary': ['#171717', '#fafafa'],
161
161
  '--color-icon-secondary': ['#737373', '#a3a3a3'],
162
- '--color-icon-disabled': ['#a3a3a3', '#525252'],
162
+ '--color-icon-disabled': ['#a3a3a3', '#525252'],
163
163
 
164
164
  // Status / Sentiment — dark mode follows the issue #2150 rubric:
165
165
  //
@@ -372,8 +372,8 @@ export const neutralTheme = defineTheme({
372
372
  // =========================================================================
373
373
  button: {
374
374
  'variant:destructive': {
375
- backgroundColor: 'var(--color-error-muted)', // locked pastel red bg
376
- color: 'var(--color-error)', // locked T30 red — matches banner/input error text
375
+ backgroundColor: 'var(--color-error-muted)', // locked pastel red bg
376
+ color: 'var(--color-error)', // locked T30 red — matches banner/input error text
377
377
  },
378
378
  },
379
379
 
@@ -482,6 +482,37 @@ export const neutralTheme = defineTheme({
482
482
  },
483
483
  },
484
484
 
485
+ // =========================================================================
486
+ // StatusDot — fill uses the SAME vivid stops as the filled semantic Badge
487
+ // (and ProgressBar), so a dot and its badge read as one status language.
488
+ //
489
+ // The default component maps each variant to a raw semantic token
490
+ // (--color-success / --color-error / --color-warning / --color-icon-
491
+ // secondary), which in light mode are the dark T30/T40 stops meant to
492
+ // sit as TEXT on a pastel surface — as a solid dot they read muddy
493
+ // (dark green / maroon / brown). Redirect them to the badge fills.
494
+ //
495
+ // success → badge success bg (green T45 / dark-ramp T60)
496
+ // warning → badge warning bg (yellow T85, same hex both modes)
497
+ // error → badge error bg (red T55 / dark-ramp T60)
498
+ // accent → badge info bg (blue T50 / dark-ramp T60) — the
499
+ // StatusDot "accent" is the info/attention color, so it
500
+ // pairs with the info badge rather than --color-accent
501
+ // (near-black #262626, the darkest offender).
502
+ //
503
+ // `neutral` is intentionally NOT overridden: the neutral badge bg is a
504
+ // near-invisible light gray (--color-background-gray #e5e5e5 / 10% white
505
+ // wash), fine as a large pill but unreadable as an 8px dot. It keeps the
506
+ // component default's visible mid-gray (--color-icon-secondary), which is
507
+ // not among the "too dark" cases.
508
+ // =========================================================================
509
+ statusdot: {
510
+ 'variant:success': {backgroundColor: 'light-dark(#198100, #64af4c)'},
511
+ 'variant:warning': {backgroundColor: '#ffce2f'},
512
+ 'variant:error': {backgroundColor: 'light-dark(#e33f4a, #ff705d)'},
513
+ 'variant:accent': {backgroundColor: 'light-dark(#0074e2, #6d9cfe)'},
514
+ },
515
+
485
516
  // =========================================================================
486
517
  // Banner — sits on a hue-tinted surface with colored text/icon:
487
518
  // Light: pastel T90 bg (pulled from --color-{X}-muted / --color-background-blue)
@@ -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
- });