@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.
- package/CHANGELOG.md +33 -0
- package/README.md +115 -19
- package/docs/cli-integrations.doc.mjs +150 -0
- package/docs/getting-started.doc.mjs +9 -9
- package/docs/internationalization.doc.mjs +243 -0
- package/docs/layout.doc.dense.mjs +5 -0
- package/docs/migration.doc.mjs +18 -18
- package/docs/principles.doc.dense.mjs +5 -5
- package/docs/principles.doc.mjs +6 -6
- package/docs/principles.doc.zh.mjs +5 -5
- package/docs/styling-libraries.doc.mjs +3 -3
- package/docs/styling.doc.mjs +4 -4
- package/docs/theme.doc.dense.mjs +12 -12
- package/docs/theme.doc.mjs +7 -7
- package/docs/theme.doc.zh.mjs +10 -10
- package/docs/tokens.doc.dense.mjs +6 -7
- package/docs/tokens.doc.mjs +1 -1
- package/docs/tokens.doc.zh.mjs +6 -7
- package/docs/working-with-ai.doc.mjs +18 -18
- package/package.json +13 -10
- package/src/api/docOverlays.test.mjs +133 -0
- package/src/api/docs.mjs +14 -2
- package/src/api/doctor.mjs +3 -3
- package/src/api/integration-block-exports.test.mjs +240 -0
- package/src/api/template-suffix.test.mjs +246 -0
- package/src/api/template.mjs +104 -28
- package/src/api/validate-integration.mjs +0 -8
- package/src/codemods/__tests__/registry.test.mjs +1 -0
- package/src/codemods/ensure-jscodeshift.mjs +11 -27
- package/src/codemods/registry.mjs +1 -0
- package/src/codemods/run-codemod.mjs +1 -1
- package/src/codemods/runner.mjs +2 -2
- package/src/codemods/transforms/v0.1.7/__tests__/migrate-table-tableprops-to-direct-props.test.mjs +120 -0
- package/src/codemods/transforms/v0.1.7/__tests__/rename-table-renderprops-styles-to-xstyle.test.mjs +112 -0
- package/src/codemods/transforms/v0.1.7/index.mjs +27 -0
- package/src/codemods/transforms/v0.1.7/migrate-table-tableprops-to-direct-props.mjs +188 -0
- package/src/codemods/transforms/v0.1.7/rename-table-renderprops-styles-to-xstyle.mjs +197 -0
- package/src/commands/agent-docs.mjs +21 -8
- package/src/commands/agent-docs.test.mjs +31 -4
- package/src/commands/build-theme.mjs +10 -71
- package/src/commands/build.mjs +15 -15
- package/src/commands/component/index.mjs +4 -4
- package/src/commands/discover.mjs +7 -5
- package/src/commands/docs.mjs +4 -4
- package/src/commands/hook/index.mjs +4 -4
- package/src/commands/init.mjs +48 -152
- package/src/commands/init.next-steps.test.mjs +1 -1
- package/src/commands/interactive-guard.test.mjs +19 -22
- package/src/commands/json-contract.test.mjs +1 -1
- package/src/commands/layout.mjs +1 -1
- package/src/commands/search.mjs +4 -4
- package/src/commands/swizzle.mjs +11 -34
- package/src/commands/template.mjs +11 -31
- package/src/commands/upgrade.mjs +9 -6
- package/src/commands/upgrade.test.mjs +1 -1
- package/src/config.mjs +5 -14
- package/src/doc.mjs +27 -0
- package/src/doc.test.mjs +383 -0
- package/src/index.mjs +5 -6
- package/src/integration.mjs +4 -15
- package/src/lib/component-discovery.importpath.test.mjs +59 -0
- package/src/lib/component-discovery.mjs +15 -5
- package/src/lib/component-format.mjs +47 -14
- package/src/lib/component-format.test.mjs +95 -1
- package/src/lib/component-loader.mjs +104 -2
- package/src/lib/componentDocOverlay.test.mjs +111 -0
- package/src/lib/config-schema.mjs +0 -30
- package/src/lib/hook-format.mjs +8 -3
- package/src/lib/term-log.mjs +48 -0
- package/src/lib/xle/registry.mjs +0 -5
- package/src/schemas/doc-schema.mjs +226 -0
- package/src/schemas/template-schema.mjs +47 -0
- package/src/template.mjs +9 -67
- package/src/types/config.d.ts +11 -66
- package/src/types/doc.d.ts +23 -0
- package/src/types/integration.d.ts +7 -18
- package/src/types/template-api.d.ts +14 -50
- package/src/utils/package-manager.mjs +78 -0
- package/src/utils/package-manager.test.mjs +108 -1
- package/src/utils/path-safety.mjs +0 -18
- package/src/utils/update-check.mjs +2 -1
- package/templates/blocks/components/Avatar/AvatarGroup.tsx +5 -7
- package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -6
- package/templates/blocks/components/Avatar/AvatarUserCard.tsx +3 -5
- package/templates/blocks/components/Avatar/AvatarWithImage.tsx +8 -6
- package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -5
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputControlledInput.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputDisabled.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputMentionTrigger.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputMultipleTriggers.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputShowcase.tsx +1 -1
- package/templates/blocks/components/ChatComposerInput/ChatComposerInputSlashCommands.tsx +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
- package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +41 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +13 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +78 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +38 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +47 -0
- package/templates/pages/ide/page.tsx +35 -41
- package/templates/pages/theme-showcase/page.tsx +7 -7
- package/templates/themes/neutral/neutralTheme.ts +63 -32
- package/src/utils/interactive.mjs +0 -76
- 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
|
-
|
|
73
|
-
|
|
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
|
-
<
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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:
|
|
288
|
-
headphones:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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'],
|
|
43
|
-
string: ['#005600', '#a6d2a2'],
|
|
44
|
-
comment: ['#737373', '#a3a3a3'],
|
|
45
|
-
number: ['#6e3500', '#ffb37f'],
|
|
46
|
-
function: ['#00458c', '#a0caff'],
|
|
47
|
-
type: ['#700084', '#efa8ff'],
|
|
48
|
-
variable: ['#171717', '#e5e5e5'],
|
|
49
|
-
operator: ['#737373', '#a3a3a3'],
|
|
50
|
-
constant: ['#6e3500', '#ffb37f'],
|
|
51
|
-
tag: ['#89001a', '#ffaeaa'],
|
|
52
|
-
attribute: ['#584400', '#eec12f'],
|
|
53
|
-
property: ['#005348', '#83dac9'],
|
|
54
|
-
punctuation: ['#a3a3a3', '#525252']
|
|
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':
|
|
131
|
-
'--color-background-card':
|
|
130
|
+
'--color-background-body': ['#f1f1f1', '#1b1b1b'],
|
|
131
|
+
'--color-background-card': ['#ffffff', '#1b1b1b'],
|
|
132
132
|
'--color-background-popover': ['#ffffff', '#1b1b1b'],
|
|
133
|
-
'--color-background-muted':
|
|
133
|
+
'--color-background-muted': ['#f1f1f1', '#1b1b1b'],
|
|
134
134
|
|
|
135
135
|
// Accent + neutral surface tints (sit alongside backgrounds)
|
|
136
|
-
'--color-accent':
|
|
136
|
+
'--color-accent': ['#262626', '#ebebeb'],
|
|
137
137
|
'--color-accent-muted': ['#f1f1f1', '#262626'],
|
|
138
|
-
'--color-neutral':
|
|
138
|
+
'--color-neutral': ['#0000000F', '#FFFFFF1A'],
|
|
139
139
|
|
|
140
140
|
// Overlays (modal scrims, hover/pressed tints)
|
|
141
|
-
'--color-overlay':
|
|
142
|
-
'--color-overlay-hover':
|
|
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':
|
|
146
|
+
'--color-text-primary': ['#171717', '#fafafa'],
|
|
147
147
|
'--color-text-secondary': ['#737373', '#a3a3a3'],
|
|
148
|
-
'--color-text-disabled':
|
|
149
|
-
'--color-text-accent':
|
|
150
|
-
'--color-on-dark':
|
|
151
|
-
'--color-on-light':
|
|
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':
|
|
153
|
+
'--color-on-accent': ['#ffffff', '#171717'],
|
|
154
154
|
'--color-on-success': ['#ffffff', '#171717'],
|
|
155
|
-
'--color-on-error':
|
|
155
|
+
'--color-on-error': ['#ffffff', '#171717'],
|
|
156
156
|
'--color-on-warning': '#171717',
|
|
157
157
|
|
|
158
158
|
// Icon
|
|
159
|
-
'--color-icon-accent':
|
|
160
|
-
'--color-icon-primary':
|
|
159
|
+
'--color-icon-accent': ['#262626', '#ebebeb'],
|
|
160
|
+
'--color-icon-primary': ['#171717', '#fafafa'],
|
|
161
161
|
'--color-icon-secondary': ['#737373', '#a3a3a3'],
|
|
162
|
-
'--color-icon-disabled':
|
|
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)',
|
|
376
|
-
color: 'var(--color-error)',
|
|
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
|
-
});
|