@astryxdesign/cli 0.1.2-canary.deb5aa0 → 0.1.2-canary.df46db7

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 (126) hide show
  1. package/docs/layout.doc.dense.mjs +90 -0
  2. package/docs/layout.doc.mjs +160 -0
  3. package/docs/principles.doc.dense.mjs +2 -2
  4. package/docs/principles.doc.mjs +4 -0
  5. package/docs/principles.doc.zh.mjs +2 -2
  6. package/package.json +7 -7
  7. package/src/api/blog.mjs +192 -0
  8. package/src/api/blog.test.mjs +144 -0
  9. package/src/api/index.mjs +1 -0
  10. package/src/commands/agent-docs.mjs +3 -0
  11. package/src/commands/blog.mjs +60 -0
  12. package/src/commands/build-theme.mjs +117 -0
  13. package/src/commands/build-theme.watch.test.mjs +157 -0
  14. package/src/index.mjs +1 -0
  15. package/src/lib/error-codes.mjs +8 -0
  16. package/src/lib/site.mjs +20 -0
  17. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.doc.mjs +14 -0
  18. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +28 -0
  19. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.doc.mjs +14 -0
  20. package/templates/blocks/components/Blockquote/BlockquoteTestimonials.tsx +34 -0
  21. package/templates/blocks/components/Blockquote/BlockquoteWithCite.doc.mjs +14 -0
  22. package/templates/blocks/components/Blockquote/BlockquoteWithCite.tsx +21 -0
  23. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.doc.mjs +14 -0
  24. package/templates/blocks/components/BreadcrumbItem/BreadcrumbItemBasic.tsx +15 -0
  25. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.doc.mjs +14 -0
  26. package/templates/blocks/components/ButtonGroup/ButtonGroupBasic.tsx +16 -0
  27. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.doc.mjs +19 -0
  28. package/templates/blocks/components/ChatDictationButton/ChatDictationButtonBasic.tsx +34 -0
  29. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.doc.mjs +14 -0
  30. package/templates/blocks/components/CheckboxListItem/CheckboxListItemBasic.tsx +30 -0
  31. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.doc.mjs +14 -0
  32. package/templates/blocks/components/CollapsibleGroup/CollapsibleGroupAccordion.tsx +31 -0
  33. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.doc.mjs +14 -0
  34. package/templates/blocks/components/CommandPaletteEmpty/CommandPaletteEmptyBasic.tsx +26 -0
  35. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.doc.mjs +14 -0
  36. package/templates/blocks/components/CommandPaletteFooter/CommandPaletteFooterBasic.tsx +32 -0
  37. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.doc.mjs +18 -0
  38. package/templates/blocks/components/CommandPaletteGroup/CommandPaletteGroupBasic.tsx +32 -0
  39. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.doc.mjs +14 -0
  40. package/templates/blocks/components/CommandPaletteItem/CommandPaletteItemBasic.tsx +27 -0
  41. package/templates/blocks/components/ContextMenu/ContextMenuBasic.doc.mjs +14 -0
  42. package/templates/blocks/components/ContextMenu/ContextMenuBasic.tsx +32 -0
  43. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.doc.mjs +14 -0
  44. package/templates/blocks/components/DialogHeader/DialogHeaderBasic.tsx +30 -0
  45. package/templates/blocks/components/DropdownMenu/DropdownMenuShowcase.tsx +0 -5
  46. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.doc.mjs +14 -0
  47. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemBasic.tsx +27 -0
  48. package/templates/blocks/components/DropdownMenuItem/DropdownMenuItemShowcase.tsx +1 -4
  49. package/templates/blocks/components/FieldLabel/FieldLabelBasic.doc.mjs +14 -0
  50. package/templates/blocks/components/FieldLabel/FieldLabelBasic.tsx +20 -0
  51. package/templates/blocks/components/FieldStatus/FieldStatusBasic.doc.mjs +14 -0
  52. package/templates/blocks/components/FieldStatus/FieldStatusBasic.tsx +23 -0
  53. package/templates/blocks/components/FileInput/FileInputBasic.doc.mjs +14 -0
  54. package/templates/blocks/components/FileInput/FileInputBasic.tsx +22 -0
  55. package/templates/blocks/components/GridSpan/GridSpanColumns.doc.mjs +14 -0
  56. package/templates/blocks/components/GridSpan/GridSpanColumns.tsx +38 -0
  57. package/templates/blocks/components/HStack/HStackBasic.doc.mjs +14 -0
  58. package/templates/blocks/components/HStack/HStackBasic.tsx +16 -0
  59. package/templates/blocks/components/InputGroup/InputGroupBasic.doc.mjs +14 -0
  60. package/templates/blocks/components/InputGroup/InputGroupBasic.tsx +27 -0
  61. package/templates/blocks/components/LayoutContent/LayoutContentBasic.doc.mjs +23 -0
  62. package/templates/blocks/components/LayoutContent/LayoutContentBasic.tsx +40 -0
  63. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.doc.mjs +22 -0
  64. package/templates/blocks/components/LayoutFooter/LayoutFooterActions.tsx +41 -0
  65. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.doc.mjs +23 -0
  66. package/templates/blocks/components/LayoutHeader/LayoutHeaderWithActions.tsx +40 -0
  67. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.doc.mjs +22 -0
  68. package/templates/blocks/components/LayoutPanel/LayoutPanelNavigation.tsx +37 -0
  69. package/templates/blocks/components/Lightbox/LightboxGallery.doc.mjs +14 -0
  70. package/templates/blocks/components/Lightbox/LightboxGallery.tsx +53 -0
  71. package/templates/blocks/components/Lightbox/LightboxShowcase.tsx +4 -3
  72. package/templates/blocks/components/Lightbox/LightboxVideo.doc.mjs +14 -0
  73. package/templates/blocks/components/Lightbox/LightboxVideo.tsx +27 -0
  74. package/templates/blocks/components/Lightbox/LightboxZoom.doc.mjs +14 -0
  75. package/templates/blocks/components/Lightbox/LightboxZoom.tsx +33 -0
  76. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.doc.mjs +14 -0
  77. package/templates/blocks/components/MetadataListItem/MetadataListItemBasic.tsx +19 -0
  78. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.doc.mjs +23 -0
  79. package/templates/blocks/components/MobileNavToggle/MobileNavToggleBasic.tsx +42 -0
  80. package/templates/blocks/components/MoreMenu/MoreMenuShowcase.tsx +0 -5
  81. package/templates/blocks/components/NavIcon/NavIconBasic.doc.mjs +14 -0
  82. package/templates/blocks/components/NavIcon/NavIconBasic.tsx +16 -0
  83. package/templates/blocks/components/RadioListItem/RadioListItemBasic.doc.mjs +14 -0
  84. package/templates/blocks/components/RadioListItem/RadioListItemBasic.tsx +30 -0
  85. package/templates/blocks/components/Resizable/ResizableSidebar.doc.mjs +25 -0
  86. package/templates/blocks/components/Resizable/ResizableSidebar.tsx +67 -0
  87. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.doc.mjs +14 -0
  88. package/templates/blocks/components/SegmentedControlItem/SegmentedControlItemBasic.tsx +21 -0
  89. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.doc.mjs +14 -0
  90. package/templates/blocks/components/SelectorOption/SelectorOptionBasic.tsx +39 -0
  91. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.doc.mjs +14 -0
  92. package/templates/blocks/components/SideNavCollapseButton/SideNavCollapseButtonBasic.tsx +55 -0
  93. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.doc.mjs +14 -0
  94. package/templates/blocks/components/SideNavHeading/SideNavHeadingBasic.tsx +38 -0
  95. package/templates/blocks/components/SideNavItem/SideNavItemBasic.doc.mjs +14 -0
  96. package/templates/blocks/components/SideNavItem/SideNavItemBasic.tsx +49 -0
  97. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.doc.mjs +14 -0
  98. package/templates/blocks/components/SideNavSection/SideNavSectionBasic.tsx +71 -0
  99. package/templates/blocks/components/StackItem/StackItemFill.doc.mjs +14 -0
  100. package/templates/blocks/components/StackItem/StackItemFill.tsx +28 -0
  101. package/templates/blocks/components/TabMenu/TabMenuBasic.doc.mjs +14 -0
  102. package/templates/blocks/components/TabMenu/TabMenuBasic.tsx +23 -0
  103. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.doc.mjs +14 -0
  104. package/templates/blocks/components/ToggleButtonGroup/ToggleButtonGroupVertical.tsx +47 -0
  105. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.doc.mjs +14 -0
  106. package/templates/blocks/components/TopNavHeading/TopNavHeadingBasic.tsx +22 -0
  107. package/templates/blocks/components/TopNavItem/TopNavItemBasic.doc.mjs +14 -0
  108. package/templates/blocks/components/TopNavItem/TopNavItemBasic.tsx +21 -0
  109. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.doc.mjs +20 -0
  110. package/templates/blocks/components/TopNavMegaMenu/TopNavMegaMenuBasic.tsx +46 -0
  111. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.doc.mjs +14 -0
  112. package/templates/blocks/components/TopNavMegaMenuFeaturedCard/TopNavMegaMenuFeaturedCardBasic.tsx +16 -0
  113. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.doc.mjs +14 -0
  114. package/templates/blocks/components/TopNavMegaMenuItem/TopNavMegaMenuItemBasic.tsx +26 -0
  115. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.doc.mjs +14 -0
  116. package/templates/blocks/components/TopNavMenu/TopNavMenuBasic.tsx +43 -0
  117. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.doc.mjs +14 -0
  118. package/templates/blocks/components/TypeaheadItem/TypeaheadItemBasic.tsx +43 -0
  119. package/templates/blocks/components/VStack/VStackBasic.doc.mjs +14 -0
  120. package/templates/blocks/components/VStack/VStackBasic.tsx +20 -0
  121. package/templates/pages/kanban-board/page.tsx +729 -0
  122. package/templates/pages/kanban-board/template.doc.mjs +12 -0
  123. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.doc.mjs +0 -14
  124. package/templates/blocks/components/TreeListBranches/TreeListBranchesShowcase.tsx +0 -64
  125. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.doc.mjs +0 -14
  126. package/templates/blocks/components/TreeListItem/TreeListItemShowcase.tsx +0 -60
@@ -180,6 +180,9 @@ export function generateCompressedIndex(version, {coreDir, runPrefix = getRunPre
180
180
  // Rules — the top error-preventers.
181
181
  lines.push('RULES:');
182
182
  lines.push('- No <div> — components do all layout/spacing. Full page → AppShell; sidebar nav → SideNav.');
183
+ lines.push('- Frame first: pick the shell (AppShell / Layout+LayoutPanel) and budget regions in px BEFORE writing content (`astryx docs layout`).');
184
+ lines.push('- Dense data = rows (Table, List/Item) edge-to-edge — never Card-wrapped list items. Card = dashboard widgets, galleries, settings groups only.');
185
+ lines.push('- Status → StatusDot/Token; Badge only for counts and enumerated states, never decoration.');
183
186
  // Styling guidance tailored to the project's configured system — never
184
187
  // recommend a path that isn't compiled here (xstyle needs the StyleX compiler;
185
188
  // utilities need Tailwind). Tokens are always the source of truth.
@@ -0,0 +1,60 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file blog command (hidden) — read the Astryx blog from the published feed.
5
+ *
6
+ * Hidden on purpose: it's not part of the documented command surface and does
7
+ * not appear in --help or the manifest. It reads the blog the same way any
8
+ * feed reader would — over the public RSS feed — and prints a post's plaintext
9
+ * (.txt) variant. Nothing about the blog's structure has to change for this to
10
+ * work; the CLI is just a consumer of the feed.
11
+ *
12
+ * astryx blog List posts from the feed
13
+ * astryx blog <slug> Print a post as plaintext
14
+ */
15
+
16
+ import {getRunPrefix} from '../utils/package-manager.mjs';
17
+ import {humanLog} from '../lib/json.mjs';
18
+ import {cliError} from '../lib/cli-error.mjs';
19
+ import {blog as blogApi} from '../api/blog.mjs';
20
+
21
+ function formatList({feedUrl, posts}, run) {
22
+ const lines = [`\nAstryx blog · feed: ${feedUrl}\n`];
23
+ if (posts.length === 0) {
24
+ lines.push('No posts found in the feed.');
25
+ return lines.join('\n');
26
+ }
27
+ for (const p of posts) {
28
+ lines.push(` ${p.slug}`);
29
+ lines.push(` ${p.title}`);
30
+ if (p.type) lines.push(` ${p.type}`);
31
+ if (p.textUrl) lines.push(` ${p.textUrl}`);
32
+ lines.push('');
33
+ }
34
+ lines.push(`Read one: ${run} astryx blog <slug>`);
35
+ return lines.join('\n');
36
+ }
37
+
38
+ export function registerBlog(program) {
39
+ program
40
+ .command('blog [slug]', {hidden: true})
41
+ .description('Read the Astryx blog from the published feed')
42
+ .action(async slug => {
43
+ const run = getRunPrefix();
44
+ let result;
45
+ try {
46
+ result = await blogApi(slug);
47
+ } catch (e) {
48
+ cliError(e.message, {suggestions: e.suggestions || [], code: e.code});
49
+ return;
50
+ }
51
+
52
+ if (result.type === 'blog.list') {
53
+ humanLog(formatList(result.data, run));
54
+ } else {
55
+ // blog.detail — print the feed URL, then the plaintext body.
56
+ humanLog(`Feed: ${result.data.feedUrl}\n`);
57
+ humanLog(result.data.text);
58
+ }
59
+ });
60
+ }
@@ -15,6 +15,7 @@
15
15
  import * as fs from 'node:fs';
16
16
  import * as path from 'node:path';
17
17
  import {pathToFileURL, fileURLToPath} from 'node:url';
18
+ import {spawn} from 'node:child_process';
18
19
  import {createJiti} from 'jiti';
19
20
  import {getRunPrefix} from '../utils/package-manager.mjs';
20
21
  import {
@@ -597,6 +598,104 @@ function validatePrivateVars(themeDef) {
597
598
  return errors;
598
599
  }
599
600
 
601
+ /**
602
+ * Path to this CLI's real entry (bin/astryx.mjs), resolved from this module's
603
+ * location (src/commands/build-theme.mjs → ../../bin/astryx.mjs). Used to
604
+ * re-invoke `theme build` as a child process in watch mode.
605
+ */
606
+ function resolveCliBin() {
607
+ const commandsDir = path.dirname(fileURLToPath(import.meta.url));
608
+ return path.resolve(commandsDir, '../../bin/astryx.mjs');
609
+ }
610
+
611
+ /**
612
+ * Run a single `theme build` as a child process, reusing the exact
613
+ * single-build code path (and its error handling) rather than duplicating it.
614
+ * Resolves with the child's exit code; never rejects.
615
+ *
616
+ * @param {string} file - The theme file argument, as the user passed it.
617
+ * @param {object} options - Parsed command options (only `out` is forwarded).
618
+ * @returns {Promise<number>}
619
+ */
620
+ function runThemeBuildOnceChild(file, options) {
621
+ const cliBin = resolveCliBin();
622
+ const args = [cliBin, 'theme', 'build', file];
623
+ if (options.out) args.push('--out', options.out);
624
+ return new Promise(resolve => {
625
+ const child = spawn(process.execPath, args, {
626
+ stdio: 'inherit',
627
+ env: process.env,
628
+ });
629
+ child.on('close', code => resolve(code ?? 0));
630
+ child.on('error', () => resolve(1));
631
+ });
632
+ }
633
+
634
+ /**
635
+ * Watch a theme file and rebuild on change. Runs an initial build, then
636
+ * rebuilds (debounced) whenever the file changes, until interrupted with
637
+ * Ctrl-C. Each rebuild runs in a child process so a build error (which the
638
+ * single-build path reports via a hard exit) is contained and the watcher
639
+ * keeps running.
640
+ *
641
+ * @param {string} file - The theme file argument, as the user passed it.
642
+ * @param {string} filePath - Absolute path to the theme file.
643
+ * @param {object} options - Parsed command options.
644
+ * @returns {Promise<void>} Resolves when the watcher is stopped (Ctrl-C).
645
+ */
646
+ async function runThemeBuildWatch(file, filePath, options) {
647
+ const rel = path.relative(process.cwd(), filePath);
648
+
649
+ // Initial build.
650
+ await runThemeBuildOnceChild(file, options);
651
+
652
+ humanLog(`\n👀 Watching ${rel} for changes — press Ctrl-C to stop.`);
653
+
654
+ let building = false;
655
+ let queued = false;
656
+ let debounce = null;
657
+
658
+ const rebuild = async () => {
659
+ if (building) {
660
+ // Coalesce changes that land mid-build into a single follow-up run.
661
+ queued = true;
662
+ return;
663
+ }
664
+ building = true;
665
+ humanLog(`\n♻️ Change detected — rebuilding ${rel}...`);
666
+ await runThemeBuildOnceChild(file, options);
667
+ building = false;
668
+ humanLog(`\n👀 Watching ${rel} for changes — press Ctrl-C to stop.`);
669
+ if (queued) {
670
+ queued = false;
671
+ rebuild();
672
+ }
673
+ };
674
+
675
+ // Some editors replace the file (rename) rather than writing in place, which
676
+ // can drop the watch. Watch the containing directory and filter to our file
677
+ // so edits survive atomic-save/rename.
678
+ const watchDir = path.dirname(filePath);
679
+ const baseName = path.basename(filePath);
680
+ const watcher = fs.watch(watchDir, (_eventType, changed) => {
681
+ if (changed && changed !== baseName) return;
682
+ clearTimeout(debounce);
683
+ // Debounce: editors often emit several events per save.
684
+ debounce = setTimeout(rebuild, 100);
685
+ });
686
+
687
+ await new Promise(resolve => {
688
+ const stop = () => {
689
+ clearTimeout(debounce);
690
+ watcher.close();
691
+ humanLog('\nStopped watching.');
692
+ resolve();
693
+ };
694
+ process.once('SIGINT', stop);
695
+ process.once('SIGTERM', stop);
696
+ });
697
+ }
698
+
600
699
  export function registerTheme(program) {
601
700
  const theme = program
602
701
  .command('theme')
@@ -621,6 +720,10 @@ export function registerTheme(program) {
621
720
  .command('build <file>')
622
721
  .description('Compile a defineTheme file to CSS + JS')
623
722
  .option('-o, --out <path>', 'Output CSS file path')
723
+ .option(
724
+ '-w, --watch',
725
+ 'Rebuild automatically when the theme file changes (Ctrl-C to stop)',
726
+ )
624
727
  .action(async (file, options) => {
625
728
  const filePath = path.resolve(process.cwd(), file);
626
729
  const json = program.opts().json || false;
@@ -630,6 +733,20 @@ export function registerTheme(program) {
630
733
  return;
631
734
  }
632
735
 
736
+ // Watch mode: run an initial build, then rebuild on every change to the
737
+ // theme file. Watch is a human-interactive, long-running mode — it is not
738
+ // supported in --json (machine) mode, which expects a single envelope.
739
+ if (options.watch) {
740
+ if (json) {
741
+ cliError('--watch is not supported with --json', {
742
+ code: ERROR_CODES.ERR_THEME_INVALID,
743
+ });
744
+ return;
745
+ }
746
+ await runThemeBuildWatch(file, filePath, options);
747
+ return;
748
+ }
749
+
633
750
  if (!json) humanLog(`\nBuilding theme from ${path.relative(process.cwd(), filePath)}...`);
634
751
 
635
752
  // Extract theme definition
@@ -0,0 +1,157 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Tests for `astryx theme build --watch` (#3375).
5
+ *
6
+ * Watch mode runs an initial build, then rebuilds whenever the theme file
7
+ * changes, until interrupted. Each rebuild runs in a child process so a build
8
+ * error (reported by the single-build path via a hard exit) is contained and
9
+ * the watcher keeps running.
10
+ *
11
+ * Building `astryx theme build` requires a compiled @astryxdesign/core, so this
12
+ * suite builds core once in beforeAll (mirrors build-theme.prose.test.mjs).
13
+ */
14
+
15
+ import {describe, it, expect, beforeAll, beforeEach, afterEach} from 'vitest';
16
+ import {execFileSync, spawn} from 'node:child_process';
17
+ import * as fs from 'node:fs';
18
+ import * as path from 'node:path';
19
+ import * as os from 'node:os';
20
+ import {fileURLToPath} from 'node:url';
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const CLI_BIN = path.resolve(__dirname, '../../bin/astryx.mjs');
24
+ const REPO_ROOT = path.resolve(__dirname, '../../../..');
25
+ const CORE_THEME_ENTRY = path.join(
26
+ REPO_ROOT,
27
+ 'packages/core/dist/theme/index.js',
28
+ );
29
+
30
+ function runCli(args, cwd) {
31
+ try {
32
+ const out = execFileSync('node', [CLI_BIN, ...args], {
33
+ cwd,
34
+ encoding: 'utf-8',
35
+ stdio: ['ignore', 'pipe', 'pipe'],
36
+ env: {...process.env, FORCE_COLOR: '0'},
37
+ });
38
+ return {code: 0, stdout: out, stderr: ''};
39
+ } catch (e) {
40
+ return {
41
+ code: e.status ?? 1,
42
+ stdout: e.stdout?.toString() ?? '',
43
+ stderr: e.stderr?.toString() ?? '',
44
+ };
45
+ }
46
+ }
47
+
48
+ /** Poll until `predicate()` is true or the timeout elapses. */
49
+ async function waitFor(predicate, {timeout = 8000, interval = 50} = {}) {
50
+ const start = Date.now();
51
+ for (;;) {
52
+ if (predicate()) return true;
53
+ if (Date.now() - start > timeout) return false;
54
+ await new Promise(r => setTimeout(r, interval));
55
+ }
56
+ }
57
+
58
+ beforeAll(() => {
59
+ if (!fs.existsSync(CORE_THEME_ENTRY)) {
60
+ execFileSync('pnpm', ['-F', '@astryxdesign/core', 'build'], {
61
+ cwd: REPO_ROOT,
62
+ stdio: 'pipe',
63
+ timeout: 180_000,
64
+ });
65
+ }
66
+ }, 200_000);
67
+
68
+ let tmpDir;
69
+ beforeEach(() => {
70
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-build-theme-watch-'));
71
+ });
72
+ afterEach(() => {
73
+ fs.rmSync(tmpDir, {recursive: true, force: true});
74
+ });
75
+
76
+ describe('theme build --watch', () => {
77
+ it('advertises the flag in --help', () => {
78
+ const result = runCli(['theme', 'build', '--help'], process.cwd());
79
+ const out = result.stdout + result.stderr;
80
+ expect(out).toMatch(/--watch/);
81
+ });
82
+
83
+ it('rejects --watch together with --json (single-envelope contract)', () => {
84
+ const themeFile = path.join(tmpDir, 'wt.mjs');
85
+ fs.writeFileSync(
86
+ themeFile,
87
+ `export default { name: 'wt', tokens: { '--color-bg': '#fff' } };\n`,
88
+ );
89
+ const result = runCli(
90
+ ['--json', 'theme', 'build', path.relative(tmpDir, themeFile), '--watch'],
91
+ tmpDir,
92
+ );
93
+ expect(result.code).not.toBe(0);
94
+ expect(result.stdout + result.stderr).toMatch(/watch/i);
95
+ });
96
+
97
+ it('builds initially, rebuilds on change, and stops cleanly on SIGINT', async () => {
98
+ const themeFile = path.join(tmpDir, 'wt.mjs');
99
+ const cssFile = path.join(tmpDir, 'wt.css');
100
+ fs.writeFileSync(
101
+ themeFile,
102
+ `export default { name: 'wt', tokens: { '--color-bg': '#ffffff' } };\n`,
103
+ );
104
+
105
+ const child = spawn(
106
+ process.execPath,
107
+ [CLI_BIN, 'theme', 'build', 'wt.mjs', '--watch'],
108
+ {cwd: tmpDir, env: {...process.env, FORCE_COLOR: '0'}},
109
+ );
110
+ let stdout = '';
111
+ child.stdout.on('data', d => (stdout += d.toString()));
112
+ child.stderr.on('data', d => (stdout += d.toString()));
113
+
114
+ try {
115
+ // Initial build produces the CSS.
116
+ const built = await waitFor(() => fs.existsSync(cssFile));
117
+ expect(built).toBe(true);
118
+ const firstCss = fs.readFileSync(cssFile, 'utf-8');
119
+ expect(firstCss).toMatch(/#ffffff/);
120
+
121
+ // Wait until the watcher is actually watching before editing, so the
122
+ // change isn't missed.
123
+ await waitFor(() => /Watching/i.test(stdout));
124
+
125
+ // Change the theme — the token value changes so the CSS must change.
126
+ fs.writeFileSync(
127
+ themeFile,
128
+ `export default { name: 'wt', tokens: { '--color-bg': '#010203' } };\n`,
129
+ );
130
+
131
+ // The rebuilt CSS should reflect the new value.
132
+ const rebuilt = await waitFor(() => {
133
+ try {
134
+ return fs.readFileSync(cssFile, 'utf-8').includes('#010203');
135
+ } catch {
136
+ return false;
137
+ }
138
+ });
139
+ expect(rebuilt).toBe(true);
140
+ expect(stdout).toMatch(/rebuild/i);
141
+ } finally {
142
+ // SIGINT must stop the watcher and exit cleanly.
143
+ child.kill('SIGINT');
144
+ }
145
+
146
+ const exited = await new Promise(resolve => {
147
+ let done = false;
148
+ child.on('exit', () => {
149
+ done = true;
150
+ resolve(true);
151
+ });
152
+ setTimeout(() => resolve(done), 4000);
153
+ });
154
+ expect(exited).toBe(true);
155
+ expect(stdout).toMatch(/Stopped watching/);
156
+ }, 30_000);
157
+ });
package/src/index.mjs CHANGED
@@ -245,6 +245,7 @@ const commands = [
245
245
  {name: 'init', path: './commands/init.mjs', register: 'registerInit'},
246
246
  {name: 'component', path: './commands/component/index.mjs', register: 'registerComponent'},
247
247
  {name: 'docs', path: './commands/docs.mjs', register: 'registerDocs'},
248
+ {name: 'blog', path: './commands/blog.mjs', register: 'registerBlog'},
248
249
  {name: 'swizzle', path: './commands/swizzle.mjs', register: 'registerSwizzle'},
249
250
  // agent-docs folded into init — functions still importable from agent-docs.mjs
250
251
  {name: 'template', path: './commands/template.mjs', register: 'registerTemplate'},
@@ -75,6 +75,8 @@
75
75
  * | 'ERR_INVALID_VERSION'
76
76
  * | 'ERR_DEP_MISSING'
77
77
  * | 'ERR_GH_CLI'
78
+ * | 'ERR_UNKNOWN_POST'
79
+ * | 'ERR_FETCH_FAILED'
78
80
  * | 'ERR_LAYOUT_PARSE'
79
81
  * | 'ERR_LAYOUT_INVALID'
80
82
  * )} ErrorCode
@@ -181,6 +183,12 @@ export const ERROR_CODES = Object.freeze({
181
183
  /** GitHub CLI (`gh`) is not installed or not authenticated. */
182
184
  ERR_GH_CLI: 'ERR_GH_CLI',
183
185
 
186
+ // ── Blog (read via the published RSS feed) ───────────────────────
187
+ /** No blog post matched the requested slug in the feed. */
188
+ ERR_UNKNOWN_POST: 'ERR_UNKNOWN_POST',
189
+ /** A network fetch (RSS feed or post text) failed. */
190
+ ERR_FETCH_FAILED: 'ERR_FETCH_FAILED',
191
+
184
192
  // ── Layout expressions (XLE/XLO) ─────────────────────────────────
185
193
  /** A layout expression failed to parse (syntax error, with line/col). */
186
194
  ERR_LAYOUT_PARSE: 'ERR_LAYOUT_PARSE',
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file site.mjs — the canonical Astryx site origin for CLI code.
5
+ *
6
+ * The published Astryx site is the single source of truth for docs, the blog,
7
+ * and the MCP endpoint. Anything in the CLI that needs to reach the site reads
8
+ * this constant, so the origin is defined in exactly one place. It is
9
+ * intentionally not user-configurable: the CLI should never be pointed at an
10
+ * arbitrary host.
11
+ *
12
+ * (The docsite has its own SITE_URL in apps/docsite; the CLI is a separately
13
+ * published package and can't import from the app, so the origin is mirrored
14
+ * here for CLI use.)
15
+ */
16
+
17
+ export const SITE_URL = 'https://astryx.atmeta.com';
18
+
19
+ /** The site's origin (scheme + host), used for same-origin checks. */
20
+ export const SITE_ORIGIN = new URL(SITE_URL).origin;
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'AvatarStatusDot',
7
+ name: 'AvatarStatusDot — Variants',
8
+ displayName: 'AvatarStatusDot — Variants',
9
+ description:
10
+ 'Presence dots on Avatars using the success, neutral, and error variants. Pass an AvatarStatusDot to the Avatar status prop; the dot sizes itself to match the Avatar.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Avatar', 'AvatarStatusDot', 'HStack'],
14
+ };
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {Avatar, AvatarStatusDot} from '@astryxdesign/core/Avatar';
6
+ import {HStack} from '@astryxdesign/core/Layout';
7
+
8
+ export default function AvatarStatusDotVariants() {
9
+ return (
10
+ <HStack gap={4} vAlign="center">
11
+ <Avatar
12
+ name="Ana Silva"
13
+ size="large"
14
+ status={<AvatarStatusDot variant="success" label="Online" />}
15
+ />
16
+ <Avatar
17
+ name="Ben Okafor"
18
+ size="large"
19
+ status={<AvatarStatusDot variant="neutral" label="Away" />}
20
+ />
21
+ <Avatar
22
+ name="Cleo Marsh"
23
+ size="large"
24
+ status={<AvatarStatusDot variant="error" label="Do not disturb" />}
25
+ />
26
+ </HStack>
27
+ );
28
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Blockquote',
7
+ name: 'Blockquote — Testimonials',
8
+ displayName: 'Blockquote — Testimonials',
9
+ description:
10
+ 'Multiple quotes arranged in a card grid for a testimonials section. Combine with Card and Grid to create social-proof layouts.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['Blockquote', 'Card', 'Grid', 'GridSpan'],
14
+ };
@@ -0,0 +1,34 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {Blockquote} from '@astryxdesign/core/Blockquote';
6
+ import {Grid, GridSpan} from '@astryxdesign/core/Grid';
7
+ import {Card} from '@astryxdesign/core/Card';
8
+
9
+ export default function BlockquoteTestimonials() {
10
+ return (
11
+ <Grid columns={2} gap={3} style={{maxWidth: 600}}>
12
+ <Card padding={4}>
13
+ <Blockquote cite="Sarah K., Lead Engineer">
14
+ Shipping UI has never been this fast. Our team went from design to
15
+ production in a single afternoon.
16
+ </Blockquote>
17
+ </Card>
18
+ <Card padding={4}>
19
+ <Blockquote cite="Marcus T., Product Designer">
20
+ The token system means every new screen just looks right — no manual
21
+ color or spacing decisions.
22
+ </Blockquote>
23
+ </Card>
24
+ <GridSpan columns={2}>
25
+ <Card padding={4}>
26
+ <Blockquote cite="Priya L., Engineering Manager">
27
+ Onboarding a new engineer used to take a week of design review. Now
28
+ they're shipping accessible, polished components on day one.
29
+ </Blockquote>
30
+ </Card>
31
+ </GridSpan>
32
+ </Grid>
33
+ );
34
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'Blockquote',
7
+ name: 'Blockquote — With Attribution',
8
+ displayName: 'Blockquote — With Attribution',
9
+ description:
10
+ 'A plain quote and a quote with a cite attribution. Use cite to credit the original author or source.',
11
+ isReady: true,
12
+ aspectRatio: 4 / 3,
13
+ componentsUsed: ['Blockquote', 'Layout'],
14
+ };
@@ -0,0 +1,21 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {Blockquote} from '@astryxdesign/core/Blockquote';
6
+ import {VStack} from '@astryxdesign/core/Layout';
7
+
8
+ export default function BlockquoteWithCite() {
9
+ return (
10
+ <VStack gap={4} style={{maxWidth: 500}}>
11
+ <Blockquote>
12
+ Design is not just what it looks like and feels like. Design is how it
13
+ works.
14
+ </Blockquote>
15
+ <Blockquote cite="Steve Jobs">
16
+ The people who are crazy enough to think they can change the world are
17
+ the ones who do.
18
+ </Blockquote>
19
+ </VStack>
20
+ );
21
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'BreadcrumbItem',
7
+ name: 'BreadcrumbItem — Basic',
8
+ displayName: 'BreadcrumbItem — Basic',
9
+ description:
10
+ 'Breadcrumb links inside a Breadcrumbs trail. Ancestor pages get an href; mark the last item with isCurrent to render it as plain text for the current page.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['Breadcrumbs', 'BreadcrumbItem'],
14
+ };
@@ -0,0 +1,15 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {Breadcrumbs, BreadcrumbItem} from '@astryxdesign/core/Breadcrumbs';
6
+
7
+ export default function BreadcrumbItemBasic() {
8
+ return (
9
+ <Breadcrumbs>
10
+ <BreadcrumbItem href="/">Home</BreadcrumbItem>
11
+ <BreadcrumbItem href="/settings">Settings</BreadcrumbItem>
12
+ <BreadcrumbItem isCurrent>Profile</BreadcrumbItem>
13
+ </Breadcrumbs>
14
+ );
15
+ }
@@ -0,0 +1,14 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'ButtonGroup',
7
+ name: 'ButtonGroup — Basic',
8
+ displayName: 'ButtonGroup — Basic',
9
+ description:
10
+ 'Three related actions joined into a single connected control. Provide a group label for accessibility and keep all buttons the same variant so they read as one unit.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: ['ButtonGroup', 'Button'],
14
+ };
@@ -0,0 +1,16 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ import {ButtonGroup} from '@astryxdesign/core/ButtonGroup';
6
+ import {Button} from '@astryxdesign/core/Button';
7
+
8
+ export default function ButtonGroupBasic() {
9
+ return (
10
+ <ButtonGroup label="Text editing actions">
11
+ <Button label="Copy" />
12
+ <Button label="Cut" />
13
+ <Button label="Paste" />
14
+ </ButtonGroup>
15
+ );
16
+ }
@@ -0,0 +1,19 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /** @type {import('../../../../../core/src/docs-types').TemplateDoc} */
4
+ export const doc = {
5
+ type: 'block',
6
+ exampleFor: 'ChatDictationButton',
7
+ name: 'ChatDictationButton — Basic',
8
+ displayName: 'ChatDictationButton — Basic',
9
+ description:
10
+ 'A dictation button wired to useChatDictation and placed in the sendActions slot of a ChatComposer. Click the microphone to transcribe speech into the input.',
11
+ isReady: true,
12
+ aspectRatio: 16 / 9,
13
+ componentsUsed: [
14
+ 'Chat',
15
+ 'ChatDictationButton',
16
+ 'ChatComposer',
17
+ 'ChatComposerInput',
18
+ ],
19
+ };