@astryxdesign/cli 0.1.7 → 0.1.8-canary.4942fe4

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 (98) 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/getting-started.doc.mjs +9 -9
  5. package/docs/migration.doc.mjs +18 -18
  6. package/docs/principles.doc.dense.mjs +1 -1
  7. package/docs/principles.doc.mjs +6 -6
  8. package/docs/principles.doc.zh.mjs +1 -1
  9. package/docs/styling-libraries.doc.mjs +3 -3
  10. package/docs/styling.doc.mjs +4 -4
  11. package/docs/theme.doc.dense.mjs +2 -2
  12. package/docs/theme.doc.mjs +7 -7
  13. package/docs/theme.doc.zh.mjs +1 -1
  14. package/docs/tokens.doc.mjs +1 -1
  15. package/docs/working-with-ai.doc.mjs +19 -19
  16. package/package.json +11 -10
  17. package/scripts/postinstall.mjs +74 -0
  18. package/src/api/doctor.mjs +3 -3
  19. package/src/codemods/__tests__/registry.test.mjs +1 -0
  20. package/src/codemods/ensure-jscodeshift.mjs +11 -27
  21. package/src/codemods/registry.mjs +1 -0
  22. package/src/codemods/run-codemod.mjs +1 -1
  23. package/src/codemods/runner.mjs +2 -2
  24. package/src/codemods/transforms/v0.1.0/__tests__/drop-xds-prefix-imports.test.mjs +42 -7
  25. package/src/codemods/transforms/v0.1.0/__tests__/migrate-xds-module-specifiers.test.mjs +43 -0
  26. package/src/codemods/transforms/v0.1.0/drop-xds-prefix-imports.mjs +102 -3
  27. package/src/codemods/transforms/v0.1.0/migrate-xds-module-specifiers.mjs +72 -6
  28. package/src/codemods/transforms/v0.1.8/__tests__/rename-avatar-size-scale.test.mjs +161 -0
  29. package/src/codemods/transforms/v0.1.8/index.mjs +19 -0
  30. package/src/codemods/transforms/v0.1.8/rename-avatar-size-scale.mjs +234 -0
  31. package/src/commands/agent-docs.mjs +136 -20
  32. package/src/commands/agent-docs.test.mjs +123 -10
  33. package/src/commands/build-theme.mjs +10 -71
  34. package/src/commands/build.mjs +15 -15
  35. package/src/commands/cli-postinstall.test.mjs +42 -0
  36. package/src/commands/component/index.mjs +4 -4
  37. package/src/commands/discover.mjs +7 -5
  38. package/src/commands/docs.mjs +4 -4
  39. package/src/commands/hook/index.mjs +4 -4
  40. package/src/commands/init.mjs +48 -152
  41. package/src/commands/init.next-steps.test.mjs +1 -1
  42. package/src/commands/interactive-guard.test.mjs +19 -22
  43. package/src/commands/json-contract.test.mjs +2 -2
  44. package/src/commands/layout.mjs +1 -1
  45. package/src/commands/search.mjs +4 -4
  46. package/src/commands/setup-nudge.test.mjs +108 -0
  47. package/src/commands/swizzle.mjs +11 -34
  48. package/src/commands/template.mjs +11 -31
  49. package/src/commands/upgrade.mjs +138 -32
  50. package/src/commands/upgrade.test.mjs +155 -1
  51. package/src/index.mjs +40 -6
  52. package/src/lib/component-format.mjs +2 -1
  53. package/src/lib/term-log.mjs +48 -0
  54. package/src/types/upgrade.d.ts +39 -2
  55. package/src/utils/package-manager.mjs +78 -0
  56. package/src/utils/package-manager.test.mjs +108 -1
  57. package/src/utils/path-safety.mjs +0 -18
  58. package/src/utils/update-check.mjs +2 -1
  59. package/templates/blocks/components/Avatar/AvatarFallbackChain.tsx +4 -4
  60. package/templates/blocks/components/Avatar/AvatarGroup.tsx +2 -2
  61. package/templates/blocks/components/Avatar/AvatarInitialsFallback.tsx +1 -1
  62. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -4
  63. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +1 -1
  64. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +4 -4
  65. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -3
  66. package/templates/blocks/components/AvatarGroup/AvatarGroupShowcase.tsx +2 -2
  67. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowCustomText.tsx +1 -1
  68. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowDefault.tsx +1 -1
  69. package/templates/blocks/components/AvatarGroupOverflow/AvatarGroupOverflowShowcase.tsx +2 -2
  70. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotShowcase.tsx +3 -3
  71. package/templates/blocks/components/AvatarStatusDot/AvatarStatusDotVariants.tsx +3 -3
  72. package/templates/blocks/components/Carousel/CarouselSnap.tsx +1 -1
  73. package/templates/blocks/components/ChatMessage/ChatMessageAvatarName.tsx +2 -2
  74. package/templates/blocks/components/ChatMessage/ChatMessageMultiBubble.tsx +1 -1
  75. package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleGrouping.tsx +1 -1
  76. package/templates/blocks/components/ChatMessageBubble/ChatMessageBubbleMetadata.tsx +1 -1
  77. package/templates/blocks/components/ChatMessageList/ChatMessageListDensity.tsx +5 -9
  78. package/templates/blocks/components/ChatMessageList/ChatMessageListFullFeatured.tsx +1 -1
  79. package/templates/blocks/components/CodeBlock/CodeBlockTerminal.tsx +1 -1
  80. package/templates/blocks/components/HoverCard/HoverCardShowcase.tsx +1 -1
  81. package/templates/blocks/components/Item/ItemWithMedia.tsx +2 -2
  82. package/templates/blocks/components/ListItem/ListItemWithMedia.tsx +2 -2
  83. package/templates/blocks/components/Pagination/PaginationDotsCarousel.tsx +2 -6
  84. package/templates/blocks/components/Stack/StackFillItem.tsx +2 -6
  85. package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
  86. package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
  87. package/templates/blocks/components/TypeaheadItem/TypeaheadItemShowcase.tsx +1 -1
  88. package/templates/pages/ai-chat/page.tsx +4 -4
  89. package/templates/pages/dashboard-portfolio/page.tsx +3 -11
  90. package/templates/pages/detail-page/page.tsx +5 -12
  91. package/templates/pages/documentation-design/page.tsx +1 -1
  92. package/templates/pages/messaging-shell/page.tsx +6 -8
  93. package/templates/pages/table-grouped/page.tsx +9 -14
  94. package/templates/pages/table-page/page.tsx +7 -9
  95. package/templates/pages/table-page-heatmap-status/page.tsx +5 -13
  96. package/docs/integration-authoring.md +0 -105
  97. package/src/utils/interactive.mjs +0 -76
  98. package/src/utils/interactive.test.mjs +0 -70
@@ -0,0 +1,42 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Guardrail tests for the @astryxdesign/cli postinstall nudge (layer 2).
5
+ *
6
+ * Tests the pure decision matrix (shouldNudge) — nudges for a real dependency
7
+ * install when not set up; stays quiet in the monorepo/source, during npx's
8
+ * transient fetch (path _npx or npm_command=exec), and once set up.
9
+ */
10
+
11
+ import {describe, it, expect} from 'vitest';
12
+ import {shouldNudge} from '../../scripts/postinstall.mjs';
13
+
14
+ const DEP = '/proj/node_modules/@astryxdesign/cli/scripts/postinstall.mjs'; // real dep install
15
+ const NPX = '/Users/x/.npm/_npx/a1b2/node_modules/@astryxdesign/cli/scripts/postinstall.mjs';
16
+ const REPO = '/repo/packages/cli/scripts/postinstall.mjs'; // monorepo/source
17
+
18
+ describe('cli postinstall — shouldNudge', () => {
19
+ it('nudges for a real dependency install when not set up', () => {
20
+ expect(shouldNudge({scriptPath: DEP, npmCommand: 'install', isSetUp: false})).toBe(true);
21
+ });
22
+
23
+ it('quiet in the monorepo/source (not under node_modules)', () => {
24
+ expect(shouldNudge({scriptPath: REPO, npmCommand: 'install', isSetUp: false})).toBe(false);
25
+ });
26
+
27
+ it('quiet during npx transient install (path contains _npx)', () => {
28
+ expect(shouldNudge({scriptPath: NPX, npmCommand: 'install', isSetUp: false})).toBe(false);
29
+ });
30
+
31
+ it('quiet during npx (npm_command=exec) — avoids double-nudge before init', () => {
32
+ expect(shouldNudge({scriptPath: DEP, npmCommand: 'exec', isSetUp: false})).toBe(false);
33
+ });
34
+
35
+ it('quiet once the project is already set up', () => {
36
+ expect(shouldNudge({scriptPath: DEP, npmCommand: 'install', isSetUp: true})).toBe(false);
37
+ });
38
+
39
+ it('quiet with no script path (defensive)', () => {
40
+ expect(shouldNudge({})).toBe(false);
41
+ });
42
+ });
@@ -18,7 +18,7 @@ import {
18
18
  formatBriefAll,
19
19
  } from '../../lib/component-format.mjs';
20
20
  import {resolveTheme} from '../../lib/resolve-theme.mjs';
21
- import {getRunPrefix} from '../../utils/package-manager.mjs';
21
+ import {getCliInvocation} from '../../utils/package-manager.mjs';
22
22
  import {jsonOut, humanLog} from '../../lib/json.mjs';
23
23
  import {cliError} from '../../lib/cli-error.mjs';
24
24
  import {ERROR_CODES} from '../../lib/error-codes.mjs';
@@ -39,7 +39,7 @@ export function registerComponent(program) {
39
39
  .option('--blocks', 'List example blocks: showcase, examples, and related')
40
40
  .option('--package <name>', 'Scope lookup to an external package (e.g. @acme/xds-widgets)')
41
41
  .action(async (name, options) => {
42
- const run = getRunPrefix();
42
+ const run = getCliInvocation();
43
43
  const zh = program.opts().zh || false;
44
44
  const dense = program.opts().dense || false;
45
45
  const lang = program.opts().lang || null;
@@ -142,7 +142,7 @@ export function registerComponent(program) {
142
142
  }
143
143
  humanLog('');
144
144
  humanLog(`Import from the path shown (e.g. import {Button} from '@astryxdesign/core/Button')`);
145
- humanLog(`Usage: ${run} astryx component <name>`);
145
+ humanLog(`Usage: ${run} component <name>`);
146
146
  humanLog('');
147
147
  }
148
148
  break;
@@ -165,7 +165,7 @@ export function registerComponent(program) {
165
165
  humanLog('');
166
166
  }
167
167
  humanLog(`Import from the path shown (e.g. import {Button} from '@astryxdesign/core/Button')`);
168
- humanLog(`Usage: ${run} astryx component <name>`);
168
+ humanLog(`Usage: ${run} component <name>`);
169
169
  humanLog('');
170
170
  break;
171
171
  }
@@ -14,6 +14,7 @@ import {formatFull, formatBrief, formatCompact} from '../lib/component-format.mj
14
14
  import {jsonOut, humanLog} from '../lib/json.mjs';
15
15
  import {cliError} from '../lib/cli-error.mjs';
16
16
  import {discover as discoverApi} from '../api/discover.mjs';
17
+ import {getCliInvocation} from '../utils/package-manager.mjs';
17
18
 
18
19
  export function registerDiscover(program) {
19
20
  program
@@ -25,6 +26,7 @@ export function registerDiscover(program) {
25
26
  const json = program.opts().json || false;
26
27
  const lang = program.opts().lang || null;
27
28
  const zh = program.opts().zh || false;
29
+ const run = getCliInvocation();
28
30
 
29
31
  let result;
30
32
  try {
@@ -80,9 +82,9 @@ export function registerDiscover(program) {
80
82
  humanLog('');
81
83
  }
82
84
  humanLog('Usage:');
83
- humanLog(' astryx discover <package> Browse a package');
84
- humanLog(' astryx discover <package>/Component View component docs');
85
- humanLog(' astryx discover <search> Search all packages');
85
+ humanLog(` ${run} discover <package> Browse a package`);
86
+ humanLog(` ${run} discover <package>/Component View component docs`);
87
+ humanLog(` ${run} discover <search> Search all packages`);
86
88
  humanLog('');
87
89
  }
88
90
  break;
@@ -99,7 +101,7 @@ export function registerDiscover(program) {
99
101
  humanLog('');
100
102
  for (const comp of result.data.components) humanLog(' ' + comp);
101
103
  humanLog('');
102
- humanLog('Usage: astryx discover ' + result.data.name + '/<ComponentName>');
104
+ humanLog(`Usage: ${run} discover ` + result.data.name + '/<ComponentName>');
103
105
  humanLog('');
104
106
  break;
105
107
  }
@@ -122,7 +124,7 @@ export function registerDiscover(program) {
122
124
  humanLog('Found ' + result.data.matches.length + ' matches for "' + result.data.query + '":');
123
125
  humanLog('');
124
126
  for (const m of result.data.matches) {
125
- humanLog(' astryx discover ' + m.package + '/' + m.component);
127
+ humanLog(` ${run} discover ` + m.package + '/' + m.component);
126
128
  }
127
129
  humanLog('');
128
130
  break;
@@ -12,7 +12,7 @@
12
12
  * astryx docs <topic> <section> Print one section
13
13
  */
14
14
 
15
- import {getRunPrefix} from '../utils/package-manager.mjs';
15
+ import {getCliInvocation} from '../utils/package-manager.mjs';
16
16
  import {jsonOut, humanLog} from '../lib/json.mjs';
17
17
  import {cliError} from '../lib/cli-error.mjs';
18
18
  import {docs as docsApi} from '../api/docs.mjs';
@@ -105,7 +105,7 @@ export function registerDocs(program) {
105
105
  .command('docs [topic] [section]')
106
106
  .description('Print reference docs')
107
107
  .action(async (topic, section) => {
108
- const run = getRunPrefix();
108
+ const run = getCliInvocation();
109
109
  const lang = program.opts().lang || null;
110
110
  const zh = program.opts().zh || false;
111
111
  const dense = program.opts().dense || false;
@@ -130,8 +130,8 @@ export function registerDocs(program) {
130
130
  for (const entry of result.data) {
131
131
  humanLog(` ${entry.topic.padEnd(14)} ${entry.description}`);
132
132
  }
133
- humanLog(`\nUsage: ${run} astryx docs <topic>`);
134
- humanLog(` ${run} astryx docs <topic> <section>\n`);
133
+ humanLog(`\nUsage: ${run} docs <topic>`);
134
+ humanLog(` ${run} docs <topic> <section>\n`);
135
135
  break;
136
136
  }
137
137
 
@@ -12,7 +12,7 @@ import {
12
12
  formatHookBrief,
13
13
  formatHookParams,
14
14
  } from '../../lib/hook-format.mjs';
15
- import {getRunPrefix} from '../../utils/package-manager.mjs';
15
+ import {getCliInvocation} from '../../utils/package-manager.mjs';
16
16
  import {jsonOut, humanLog} from '../../lib/json.mjs';
17
17
  import {cliError} from '../../lib/cli-error.mjs';
18
18
  import {ERROR_CODES} from '../../lib/error-codes.mjs';
@@ -27,7 +27,7 @@ export function registerHook(program) {
27
27
  .option('--category <category>', 'List hooks in a specific category')
28
28
  .option('--params', 'Print only the parameters table')
29
29
  .action(async (name, options) => {
30
- const run = getRunPrefix();
30
+ const run = getCliInvocation();
31
31
  const zh = program.opts().zh || false;
32
32
  const lang = program.opts().lang || null;
33
33
  const detailSource = program.getOptionValueSource('detail');
@@ -76,7 +76,7 @@ export function registerHook(program) {
76
76
  for (const h of hookNames) humanLog(` ${h}`);
77
77
  }
78
78
  humanLog('');
79
- humanLog(`Usage: ${run} astryx hook <name>`);
79
+ humanLog(`Usage: ${run} hook <name>`);
80
80
  humanLog('');
81
81
  }
82
82
  break;
@@ -93,7 +93,7 @@ export function registerHook(program) {
93
93
  }
94
94
  humanLog('');
95
95
  }
96
- humanLog(`Usage: ${run} astryx hook <name>`);
96
+ humanLog(`Usage: ${run} hook <name>`);
97
97
  humanLog('');
98
98
  break;
99
99
  }
@@ -1,33 +1,33 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file init command — Interactive initialization wizard + feature installer
4
+ * @file init command — non-interactive setup + feature installer
5
5
  *
6
- * Interactive: `astryx init` walks through all features
7
- * Non-interactive: `astryx init --features agents,theme,template`
6
+ * `astryx init` is non-interactive by default: it installs the AGENTS.md/
7
+ * CLAUDE.md cheat sheet with NO prompts, so it behaves identically for humans,
8
+ * AI agents, CI, and piped I/O — it never hangs or errors on a missing TTY.
9
+ * Non-interactive feature install: `astryx init --features agents,theme,template`
8
10
  * Re-runnable: safe to run multiple times, idempotent
9
11
  *
10
12
  * Features:
11
13
  * agents — Install AGENTS.md/CLAUDE.md cheat sheet for AI coding agents
12
- * theme — Scaffold a custom theme file
14
+ * theme — Point to the theme workflow (`astryx theme`)
13
15
  * template — Copy a starter page template
14
16
  */
15
17
 
16
- import * as p from '@clack/prompts';
17
18
  import * as path from 'node:path';
18
19
  import * as fs from 'node:fs';
19
20
  import {CLI_ROOT} from '../utils/paths.mjs';
20
21
  import {PathSafetyError} from '../utils/path-safety.mjs';
21
- import {getRunPrefix} from '../utils/package-manager.mjs';
22
+ import {getCliInvocation} from '../utils/package-manager.mjs';
22
23
  import {installAgentDocs, removeAgentDocs} from './agent-docs.mjs';
23
24
  import {listTemplates} from './template.mjs';
24
25
  import {humanLog} from '../lib/json.mjs';
25
26
  import {cliError} from '../lib/cli-error.mjs';
26
27
  import {ERROR_CODES} from '../lib/error-codes.mjs';
27
- import {requireInteractive} from '../utils/interactive.mjs';
28
28
 
29
29
  const VALID_FEATURES = ['agents', 'theme', 'template'];
30
- const run = getRunPrefix();
30
+ const run = getCliInvocation();
31
31
 
32
32
  /**
33
33
  * Build the "Next steps" lines printed at the end of `astryx init`.
@@ -40,10 +40,10 @@ const run = getRunPrefix();
40
40
  *
41
41
  * Exported for testing.
42
42
  *
43
- * @param {string} runPrefix package-manager run prefix (e.g. `npx`)
43
+ * @param {string} invocation install-aware CLI invocation stem (e.g. `npx astryx`, `pnpm exec astryx`, or `npx @astryxdesign/cli` for one-off runs)
44
44
  * @returns {string[]} ordered list of human-facing lines
45
45
  */
46
- export function getNextSteps(runPrefix) {
46
+ export function getNextSteps(invocation) {
47
47
  return [
48
48
  '',
49
49
  ' Next steps:',
@@ -54,23 +54,15 @@ export function getNextSteps(runPrefix) {
54
54
  " import { neutralTheme } from '@astryxdesign/theme-neutral/built'",
55
55
  " import '@astryxdesign/theme-neutral/theme.css'",
56
56
  ' <Theme theme={neutralTheme}>...</Theme>',
57
- ` For custom themes, run \`${runPrefix} astryx theme build <file>\` to generate the built artifacts.`,
58
- ` 4. ${runPrefix} astryx --help for all commands`,
57
+ ` For custom themes, run \`${invocation} theme build <file>\` to generate the built artifacts.`,
58
+ ` 4. ${invocation} --help for all commands`,
59
59
  '',
60
60
  ];
61
61
  }
62
62
 
63
- function isCancel(value) {
64
- if (p.isCancel(value)) {
65
- p.cancel('Setup cancelled.');
66
- process.exit(0);
67
- }
68
- return value;
69
- }
70
-
71
63
  // ─── Feature: agents ─────────────────────────────────────────────────────────
72
64
 
73
- function runAgents(targetDir, {interactive = true, agent, agentDocsPath} = {}) {
65
+ function runAgents(targetDir, {agent, agentDocsPath} = {}) {
74
66
  try {
75
67
  const paths = agentDocsPath
76
68
  ? Array.isArray(agentDocsPath)
@@ -79,114 +71,55 @@ function runAgents(targetDir, {interactive = true, agent, agentDocsPath} = {}) {
79
71
  : undefined;
80
72
  const written = installAgentDocs(targetDir, {agent, paths});
81
73
  const summary = written.join(', ');
82
- if (interactive) {
83
- p.log.success(`AI agent docs installed → ${summary}`);
84
- } else {
85
- humanLog(`✓ AI agent docs installed → ${summary}`);
86
- }
74
+ humanLog(`✓ AI agent docs installed → ${summary}`);
87
75
  } catch (err) {
88
76
  // PathSafetyError carries a precise, user-actionable message —
89
77
  // surface it instead of the generic "could not install" warning so
90
78
  // misconfigured --agent-docs-path values aren't silently swallowed.
91
79
  if (err instanceof PathSafetyError) {
92
- const msg = `Error: ${err.message}`;
93
- if (interactive) {
94
- p.log.error(msg);
95
- } else {
96
- console.error(msg);
97
- }
80
+ console.error(`Error: ${err.message}`);
98
81
  process.exitCode = 1;
99
82
  return;
100
83
  }
101
- const msg = `Could not install agent docs. Try again with \`${run} astryx init --features agents\`.`;
102
- if (interactive) {
103
- p.log.warning(msg);
104
- } else {
105
- console.error(msg);
106
- }
84
+ console.error(`Could not install agent docs. Try again with \`${run} init --features agents\`.`);
107
85
  }
108
86
  }
109
87
 
110
88
  // ─── Feature: theme ──────────────────────────────────────────────────────────
111
89
 
112
- async function runTheme({interactive = true} = {}) {
113
- if (!interactive) {
114
- humanLog(`✓ Theme scaffolding requires interactive mode. Run \`${run} astryx theme\` instead.`);
115
- return;
116
- }
117
-
118
- p.note(
119
- 'Create a custom theme with your brand colors.\n' +
120
- `Run \`${run} astryx theme\` for the full theme wizard.\n` +
121
- `Run \`${run} astryx theme --list\` to see existing themes.`,
122
- 'Themes',
123
- );
90
+ function runTheme() {
91
+ humanLog(`✓ For a custom theme, run \`${run} theme\` (browse) or \`${run} theme add <slug>\` (scaffold).`);
124
92
  }
125
93
 
126
94
  // ─── Feature: template ───────────────────────────────────────────────────────
127
95
 
128
- async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
96
+ function runTemplate(targetDir, {templateName} = {}) {
129
97
  const templates = listTemplates();
130
98
  if (templates.length === 0) return;
131
99
 
132
- if (!interactive) {
133
- if (!templateName) {
134
- // Point agents at the build workflow rather than dumping page-template
135
- // names `build` surfaces pages AND blocks AND components for an idea,
136
- // and `build` with no args is the full how-to-build playbook.
137
- humanLog('✓ To build UI, use these commands:');
138
- humanLog('');
139
- humanLog(` ${run} astryx build "<what you're building>" build a page kit: closest template + blocks + components`);
140
- humanLog(` ${run} astryx build the how-to-build workflow (read this first)`);
141
- humanLog(` ${run} astryx search <query> find anything — components, docs, templates, blocks`);
142
- humanLog('');
143
- return;
144
- }
145
-
146
- if (!templates.includes(templateName)) {
147
- cliError(`Unknown template "${templateName}". Available: ${templates.join(', ')}`, {code: ERROR_CODES.ERR_UNKNOWN_TEMPLATE});
148
- return;
149
- }
150
-
151
- const outputDir = path.resolve(targetDir, `./src/pages/${templateName}`);
152
- const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateName, 'page.tsx');
153
- fs.mkdirSync(outputDir, {recursive: true});
154
- fs.copyFileSync(srcPath, path.join(outputDir, 'page.tsx'));
155
- humanLog(`✓ Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
100
+ if (!templateName) {
101
+ // Point agents at the build workflow rather than dumping page-template
102
+ // names `build` surfaces pages AND blocks AND components for an idea,
103
+ // and `build` with no args is the full how-to-build playbook.
104
+ humanLog('✓ To build UI, use these commands:');
105
+ humanLog('');
106
+ humanLog(` ${run} build "<what you're building>" build a page — kit: closest template + blocks + components`);
107
+ humanLog(` ${run} build the how-to-build workflow (read this first)`);
108
+ humanLog(` ${run} search <query> find anything components, docs, templates, blocks`);
109
+ humanLog('');
156
110
  return;
157
111
  }
158
112
 
159
- const TEMPLATE_OPTIONS = [
160
- {value: 'skip', label: 'Skip No template'},
161
- {value: 'blank', label: 'Blank Page — Minimal scaffold'},
162
- {value: 'table', label: 'Table Page — Data table with actions'},
163
- {value: 'login', label: 'Login Page — Auth form with inputs'},
164
- ];
165
-
166
- const templateChoice = isCancel(
167
- await p.select({
168
- message: 'Start with a page template?',
169
- options: TEMPLATE_OPTIONS,
170
- }),
171
- );
172
-
173
- if (templateChoice === 'skip') return;
174
-
175
- const targetPath = isCancel(
176
- await p.text({
177
- message: 'Where should the template be created?',
178
- initialValue: `./src/pages/${templateChoice}`,
179
- placeholder: `./src/pages/${templateChoice}`,
180
- }),
181
- );
182
-
183
- const outputDir = path.resolve(targetDir, targetPath);
184
- const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateChoice, 'page.tsx');
113
+ if (!templates.includes(templateName)) {
114
+ cliError(`Unknown template "${templateName}". Available: ${templates.join(', ')}`, {code: ERROR_CODES.ERR_UNKNOWN_TEMPLATE});
115
+ return;
116
+ }
185
117
 
118
+ const outputDir = path.resolve(targetDir, `./src/pages/${templateName}`);
119
+ const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateName, 'page.tsx');
186
120
  fs.mkdirSync(outputDir, {recursive: true});
187
121
  fs.copyFileSync(srcPath, path.join(outputDir, 'page.tsx'));
188
-
189
- p.log.success(`Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
122
+ humanLog(`✓ Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
190
123
  }
191
124
 
192
125
  // ─── Command ─────────────────────────────────────────────────────────────────
@@ -200,7 +133,7 @@ export function registerInit(program) {
200
133
  .option('--remove-agents', 'Remove AI agent docs from all agent doc files')
201
134
  .option('--agent <tool>', 'Target AI tool for agent docs: claude, cursor, codex, hermes, all')
202
135
  .option('--agent-docs-path <path...>', 'Explicit file path(s) for agent docs')
203
- .action(async (options) => {
136
+ .action((options) => {
204
137
  const targetDir = process.cwd();
205
138
 
206
139
  // Remove mode
@@ -224,62 +157,25 @@ export function registerInit(program) {
224
157
 
225
158
  for (const feature of features) {
226
159
  if (feature === 'agents') runAgents(targetDir, {
227
- interactive: false,
228
160
  agent: options.agent,
229
161
  agentDocsPath: options.agentDocsPath,
230
162
  });
231
- if (feature === 'theme') await runTheme({interactive: false});
232
- if (feature === 'template') await runTemplate(targetDir, {interactive: false});
163
+ if (feature === 'theme') runTheme();
164
+ if (feature === 'template') runTemplate(targetDir, {});
233
165
  }
234
166
  return;
235
167
  }
236
168
 
237
- // Interactive wizard
238
- //
239
- // Guard: this wizard blocks on prompts. In a non-interactive context
240
- // (CI, piped stdin/stdout, no TTY) it would hang forever. Fail fast
241
- // with actionable guidance via the shared interactivity contract.
242
- requireInteractive({
243
- command: 'init',
244
- hint: `\`${run} astryx init --all\` or \`--features agents,theme,template\``,
169
+ // No flags: TTY-free default. `astryx init` installs the AI agent cheat
170
+ // sheet with NO prompts, so it behaves identically for humans, agents, CI,
171
+ // and piped I/O it never hangs or errors on a missing TTY. (A guided
172
+ // `--interactive` setup may return later as an explicit opt-in.)
173
+ runAgents(targetDir, {
174
+ agent: options.agent,
175
+ agentDocsPath: options.agentDocsPath,
245
176
  });
246
-
247
- p.intro('Welcome to the design system');
248
-
249
- p.note(
250
- 'A design system for building internal tools\nwith 300+ React components.',
251
- 'About',
252
- );
253
-
254
- // Feature: agents
255
- const shouldInstallAgents = isCancel(
256
- await p.confirm({
257
- message: 'Install AI agent support? (adds a design system cheat sheet to AGENTS.md)',
258
- initialValue: true,
259
- }),
260
- );
261
-
262
- if (shouldInstallAgents) {
263
- const s = p.spinner();
264
- s.start('Installing agent docs');
265
- runAgents(targetDir);
266
- s.stop('Done');
267
- }
268
-
269
- // Feature: swizzle awareness
270
- p.note(
271
- `You can customize any component with:\n ${run} astryx swizzle Button\n ${run} astryx swizzle --list`,
272
- 'Component Customization',
273
- );
274
-
275
- // Feature: template
276
- await runTemplate(targetDir);
277
-
278
- // Feature: theme awareness
279
- await runTheme();
280
-
281
- // Outro
282
- p.outro('Design system initialized!');
177
+ humanLog('');
178
+ humanLog(` Tip: \`${run} init --all\` also points you to the theme and page-building workflows.`);
283
179
 
284
180
  for (const line of getNextSteps(run)) {
285
181
  humanLog(line);
@@ -23,7 +23,7 @@ import {describe, it, expect} from 'vitest';
23
23
  import {getNextSteps} from './init.mjs';
24
24
 
25
25
  describe('init Next steps theme guidance', () => {
26
- const text = getNextSteps('npx').join('\n');
26
+ const text = getNextSteps('npx astryx').join('\n');
27
27
 
28
28
  it('mentions the base CSS imports so the app is not left unstyled', () => {
29
29
  expect(text).toContain("'@astryxdesign/core/reset.css'");
@@ -1,15 +1,13 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file Subprocess no-hang tests for the interactivity contract.
4
+ * @file Subprocess no-hang tests for `astryx init`.
5
5
  *
6
- * The wizard command `init` blocks on @clack/prompts. In a
7
- * non-interactive context it must fail fast (exit 1) instead of hanging.
8
- * These tests spawn the CLI as a real subprocess with stdin/stdout NOT a TTY
9
- * (stdio 'ignore'/'pipe'), which is exactly the CI / piped condition. If the
10
- * guard were missing, spawnSync would hit the timeout (signal SIGTERM,
11
- * status null) — so asserting `signal === null && status === 1` proves the
12
- * process exited cleanly rather than hanging.
6
+ * TTY was removed from the CLI: `init` is non-interactive by default and must
7
+ * run cleanly (exit 0, writing the agent cheat sheet) instead of hanging on a
8
+ * prompt. These tests spawn the CLI with stdin/stdout NOT a TTY (the CI / piped
9
+ * / agent condition). A hang would show as signal SIGTERM + status null; we
10
+ * assert `signal === null && status === 0` to prove it exits cleanly and works.
13
11
  */
14
12
 
15
13
  import {describe, it, expect, beforeEach, afterEach} from 'vitest';
@@ -42,25 +40,24 @@ afterEach(() => {
42
40
  fs.rmSync(tmpDir, {recursive: true, force: true});
43
41
  });
44
42
 
45
- describe('init non-interactive safety', () => {
46
- it('fails fast (exit 1, no hang) and writes no files', () => {
43
+ describe('init is non-interactive by default (no TTY needed)', () => {
44
+ it('runs cleanly (exit 0, no hang) and writes agent docs', () => {
47
45
  const r = runCli(['init']);
48
- expect(r.signal).toBeNull();
49
- expect(r.status).toBe(1);
50
- expect(fs.readdirSync(tmpDir)).toEqual([]);
46
+ expect(r.signal).toBeNull(); // did not hang
47
+ expect(r.status).toBe(0); // succeeded without a TTY
48
+ expect(fs.readdirSync(tmpDir).length).toBeGreaterThan(0); // wrote the cheat sheet
51
49
  });
52
50
 
53
- it('prints actionable guidance (--all / --features)', () => {
54
- const out = (() => {
55
- const r = runCli(['init']);
56
- return r.stderr + r.stdout;
57
- })();
58
- expect(out).toMatch(/requires a TTY/i);
59
- expect(out).toMatch(/--all/);
60
- expect(out).toMatch(/--features/);
51
+ it('writes an agent-doc file containing the ASTRYX cheat-sheet marker', () => {
52
+ runCli(['init']);
53
+ // init injects into existing agent-doc files if present, else creates AGENTS.md
54
+ const candidates = ['AGENTS.md', 'CLAUDE.md', '.cursorrules', path.join('.claude', 'CLAUDE.md')];
55
+ const doc = candidates.find(f => fs.existsSync(path.join(tmpDir, f)));
56
+ expect(doc).toBeTruthy();
57
+ expect(fs.readFileSync(path.join(tmpDir, doc), 'utf8')).toMatch(/ASTRYX:START/);
61
58
  });
62
59
 
63
- it('still runs --features agents non-interactively (guard does not over-catch)', () => {
60
+ it('still runs --features agents non-interactively', () => {
64
61
  const r = runCli(['init', '--features', 'agents']);
65
62
  expect(r.signal).toBeNull();
66
63
  expect(r.status).toBe(0);
@@ -45,7 +45,7 @@ function runCli(args, {cwd} = {}) {
45
45
 
46
46
  function parseJson(stdout) {
47
47
  // CLI emits a single JSON document. If anything else snuck onto stdout
48
- // (clack output, console.log strings, etc.) JSON.parse will throw —
48
+ // (log output, console.log strings, etc.) JSON.parse will throw —
49
49
  // which is exactly the failure mode we want to catch in tests.
50
50
  return JSON.parse(stdout);
51
51
  }
@@ -61,7 +61,7 @@ afterEach(() => {
61
61
  });
62
62
 
63
63
  describe('--json contract: rejects before side effects', () => {
64
- it('astryx init --json --features agents does not write .claude/CLAUDE.md', () => {
64
+ it('astryx init --json --features agents does not write agent docs', () => {
65
65
  const before = fs.readdirSync(tmpDir);
66
66
  expect(before).toEqual([]);
67
67
 
@@ -20,7 +20,7 @@ import {layoutExpand, layoutCheck, layoutGrammar} from '../api/layout.mjs';
20
20
  /** Resolve the expression from arg, --file, or stdin ('-'). */
21
21
  async function readExpression(expr, options) {
22
22
  if (options.file) return fs.readFileSync(options.file, 'utf-8');
23
- if (expr === '-' || (!expr && !process.stdin.isTTY)) {
23
+ if (expr === '-') {
24
24
  const chunks = [];
25
25
  for await (const chunk of process.stdin) chunks.push(chunk);
26
26
  return Buffer.concat(chunks).toString('utf-8');
@@ -16,7 +16,7 @@
16
16
  * astryx search button --json Typed JSON envelope
17
17
  */
18
18
 
19
- import {getRunPrefix} from '../utils/package-manager.mjs';
19
+ import {getCliInvocation, formatCliCommand} from '../utils/package-manager.mjs';
20
20
  import {jsonOut, humanLog} from '../lib/json.mjs';
21
21
  import {cliError} from '../lib/cli-error.mjs';
22
22
  import {search as searchApi, SEARCH_DOMAINS} from '../api/search.mjs';
@@ -63,7 +63,7 @@ export function registerSearch(program) {
63
63
  if (json) return jsonOut(result.type, result.data);
64
64
 
65
65
  // ── Text output ──────────────────────────────────────────────
66
- const run = getRunPrefix();
66
+ const run = getCliInvocation();
67
67
  const {query: q, results} = result.data;
68
68
 
69
69
  // No matches is a valid, successful outcome — clean message, exit 0.
@@ -71,7 +71,7 @@ export function registerSearch(program) {
71
71
  humanLog('');
72
72
  humanLog(`No results for "${q}".`);
73
73
  humanLog('');
74
- humanLog(`Try a broader term, or browse: ${run} astryx component --list`);
74
+ humanLog(`Try a broader term, or browse: ${run} component --list`);
75
75
  humanLog('');
76
76
  return;
77
77
  }
@@ -86,7 +86,7 @@ export function registerSearch(program) {
86
86
  if (r.description) {
87
87
  humanLog(` ${r.description}`);
88
88
  }
89
- humanLog(` → ${run} ${r.command}`);
89
+ humanLog(` → ${formatCliCommand(r.command)}`);
90
90
  if (options.detail) {
91
91
  if (r.import) humanLog(` import: ${r.import}`);
92
92
  humanLog(` match: ${r.reason} (score ${r.score})`);