@astryxdesign/cli 0.1.7-canary.fb9c68c → 0.1.7

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 (52) hide show
  1. package/README.md +19 -115
  2. package/docs/getting-started.doc.mjs +9 -9
  3. package/docs/integration-authoring.md +105 -0
  4. package/docs/migration.doc.mjs +18 -18
  5. package/docs/principles.doc.dense.mjs +1 -1
  6. package/docs/principles.doc.mjs +6 -6
  7. package/docs/principles.doc.zh.mjs +1 -1
  8. package/docs/styling-libraries.doc.mjs +3 -3
  9. package/docs/styling.doc.mjs +4 -4
  10. package/docs/theme.doc.dense.mjs +2 -2
  11. package/docs/theme.doc.mjs +7 -7
  12. package/docs/theme.doc.zh.mjs +1 -1
  13. package/docs/tokens.doc.mjs +1 -1
  14. package/docs/working-with-ai.doc.mjs +18 -18
  15. package/package.json +10 -11
  16. package/src/api/doctor.mjs +3 -3
  17. package/src/codemods/ensure-jscodeshift.mjs +27 -11
  18. package/src/codemods/run-codemod.mjs +1 -1
  19. package/src/codemods/runner.mjs +2 -2
  20. package/src/commands/agent-docs.mjs +14 -56
  21. package/src/commands/agent-docs.test.mjs +4 -11
  22. package/src/commands/build-theme.mjs +71 -10
  23. package/src/commands/build.mjs +15 -15
  24. package/src/commands/component/index.mjs +4 -4
  25. package/src/commands/discover.mjs +5 -7
  26. package/src/commands/docs.mjs +4 -4
  27. package/src/commands/hook/index.mjs +4 -4
  28. package/src/commands/init.mjs +152 -48
  29. package/src/commands/init.next-steps.test.mjs +1 -1
  30. package/src/commands/interactive-guard.test.mjs +22 -19
  31. package/src/commands/json-contract.test.mjs +1 -1
  32. package/src/commands/layout.mjs +1 -1
  33. package/src/commands/search.mjs +4 -4
  34. package/src/commands/swizzle.mjs +34 -11
  35. package/src/commands/template.mjs +31 -11
  36. package/src/commands/upgrade.mjs +6 -9
  37. package/src/commands/upgrade.test.mjs +1 -1
  38. package/src/index.mjs +6 -40
  39. package/src/lib/component-format.mjs +1 -2
  40. package/src/utils/interactive.mjs +76 -0
  41. package/src/utils/interactive.test.mjs +70 -0
  42. package/src/utils/package-manager.mjs +0 -78
  43. package/src/utils/package-manager.test.mjs +1 -108
  44. package/src/utils/path-safety.mjs +18 -0
  45. package/src/utils/update-check.mjs +1 -2
  46. package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
  47. package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +7 -2
  48. package/docs/cli-integrations.doc.mjs +0 -150
  49. package/scripts/postinstall.mjs +0 -74
  50. package/src/commands/cli-postinstall.test.mjs +0 -42
  51. package/src/commands/setup-nudge.test.mjs +0 -108
  52. package/src/lib/term-log.mjs +0 -48
@@ -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 {getCliInvocation} from '../../utils/package-manager.mjs';
21
+ import {getRunPrefix} 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 = getCliInvocation();
42
+ const run = getRunPrefix();
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} component <name>`);
145
+ humanLog(`Usage: ${run} astryx 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} component <name>`);
168
+ humanLog(`Usage: ${run} astryx component <name>`);
169
169
  humanLog('');
170
170
  break;
171
171
  }
@@ -14,7 +14,6 @@ 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';
18
17
 
19
18
  export function registerDiscover(program) {
20
19
  program
@@ -26,7 +25,6 @@ export function registerDiscover(program) {
26
25
  const json = program.opts().json || false;
27
26
  const lang = program.opts().lang || null;
28
27
  const zh = program.opts().zh || false;
29
- const run = getCliInvocation();
30
28
 
31
29
  let result;
32
30
  try {
@@ -82,9 +80,9 @@ export function registerDiscover(program) {
82
80
  humanLog('');
83
81
  }
84
82
  humanLog('Usage:');
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`);
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');
88
86
  humanLog('');
89
87
  }
90
88
  break;
@@ -101,7 +99,7 @@ export function registerDiscover(program) {
101
99
  humanLog('');
102
100
  for (const comp of result.data.components) humanLog(' ' + comp);
103
101
  humanLog('');
104
- humanLog(`Usage: ${run} discover ` + result.data.name + '/<ComponentName>');
102
+ humanLog('Usage: astryx discover ' + result.data.name + '/<ComponentName>');
105
103
  humanLog('');
106
104
  break;
107
105
  }
@@ -124,7 +122,7 @@ export function registerDiscover(program) {
124
122
  humanLog('Found ' + result.data.matches.length + ' matches for "' + result.data.query + '":');
125
123
  humanLog('');
126
124
  for (const m of result.data.matches) {
127
- humanLog(` ${run} discover ` + m.package + '/' + m.component);
125
+ humanLog(' astryx discover ' + m.package + '/' + m.component);
128
126
  }
129
127
  humanLog('');
130
128
  break;
@@ -12,7 +12,7 @@
12
12
  * astryx docs <topic> <section> Print one section
13
13
  */
14
14
 
15
- import {getCliInvocation} from '../utils/package-manager.mjs';
15
+ import {getRunPrefix} 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 = getCliInvocation();
108
+ const run = getRunPrefix();
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} docs <topic>`);
134
- humanLog(` ${run} docs <topic> <section>\n`);
133
+ humanLog(`\nUsage: ${run} astryx docs <topic>`);
134
+ humanLog(` ${run} astryx 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 {getCliInvocation} from '../../utils/package-manager.mjs';
15
+ import {getRunPrefix} 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 = getCliInvocation();
30
+ const run = getRunPrefix();
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} hook <name>`);
79
+ humanLog(`Usage: ${run} astryx 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} hook <name>`);
96
+ humanLog(`Usage: ${run} astryx 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 — non-interactive setup + feature installer
4
+ * @file init command — Interactive initialization wizard + feature installer
5
5
  *
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`
6
+ * Interactive: `astryx init` walks through all features
7
+ * Non-interactive: `astryx init --features agents,theme,template`
10
8
  * Re-runnable: safe to run multiple times, idempotent
11
9
  *
12
10
  * Features:
13
11
  * agents — Install AGENTS.md/CLAUDE.md cheat sheet for AI coding agents
14
- * theme — Point to the theme workflow (`astryx theme`)
12
+ * theme — Scaffold a custom theme file
15
13
  * template — Copy a starter page template
16
14
  */
17
15
 
16
+ import * as p from '@clack/prompts';
18
17
  import * as path from 'node:path';
19
18
  import * as fs from 'node:fs';
20
19
  import {CLI_ROOT} from '../utils/paths.mjs';
21
20
  import {PathSafetyError} from '../utils/path-safety.mjs';
22
- import {getCliInvocation} from '../utils/package-manager.mjs';
21
+ import {getRunPrefix} from '../utils/package-manager.mjs';
23
22
  import {installAgentDocs, removeAgentDocs} from './agent-docs.mjs';
24
23
  import {listTemplates} from './template.mjs';
25
24
  import {humanLog} from '../lib/json.mjs';
26
25
  import {cliError} from '../lib/cli-error.mjs';
27
26
  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 = getCliInvocation();
30
+ const run = getRunPrefix();
31
31
 
32
32
  /**
33
33
  * Build the "Next steps" lines printed at the end of `astryx init`.
@@ -40,10 +40,10 @@ const run = getCliInvocation();
40
40
  *
41
41
  * Exported for testing.
42
42
  *
43
- * @param {string} invocation install-aware CLI invocation stem (e.g. `npx astryx`, `pnpm exec astryx`, or `npx @astryxdesign/cli` for one-off runs)
43
+ * @param {string} runPrefix package-manager run prefix (e.g. `npx`)
44
44
  * @returns {string[]} ordered list of human-facing lines
45
45
  */
46
- export function getNextSteps(invocation) {
46
+ export function getNextSteps(runPrefix) {
47
47
  return [
48
48
  '',
49
49
  ' Next steps:',
@@ -54,15 +54,23 @@ export function getNextSteps(invocation) {
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 \`${invocation} theme build <file>\` to generate the built artifacts.`,
58
- ` 4. ${invocation} --help for all commands`,
57
+ ` For custom themes, run \`${runPrefix} astryx theme build <file>\` to generate the built artifacts.`,
58
+ ` 4. ${runPrefix} astryx --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
+
63
71
  // ─── Feature: agents ─────────────────────────────────────────────────────────
64
72
 
65
- function runAgents(targetDir, {agent, agentDocsPath} = {}) {
73
+ function runAgents(targetDir, {interactive = true, agent, agentDocsPath} = {}) {
66
74
  try {
67
75
  const paths = agentDocsPath
68
76
  ? Array.isArray(agentDocsPath)
@@ -71,55 +79,114 @@ function runAgents(targetDir, {agent, agentDocsPath} = {}) {
71
79
  : undefined;
72
80
  const written = installAgentDocs(targetDir, {agent, paths});
73
81
  const summary = written.join(', ');
74
- humanLog(`✓ AI agent docs installed → ${summary}`);
82
+ if (interactive) {
83
+ p.log.success(`AI agent docs installed → ${summary}`);
84
+ } else {
85
+ humanLog(`✓ AI agent docs installed → ${summary}`);
86
+ }
75
87
  } catch (err) {
76
88
  // PathSafetyError carries a precise, user-actionable message —
77
89
  // surface it instead of the generic "could not install" warning so
78
90
  // misconfigured --agent-docs-path values aren't silently swallowed.
79
91
  if (err instanceof PathSafetyError) {
80
- console.error(`Error: ${err.message}`);
92
+ const msg = `Error: ${err.message}`;
93
+ if (interactive) {
94
+ p.log.error(msg);
95
+ } else {
96
+ console.error(msg);
97
+ }
81
98
  process.exitCode = 1;
82
99
  return;
83
100
  }
84
- console.error(`Could not install agent docs. Try again with \`${run} init --features agents\`.`);
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
+ }
85
107
  }
86
108
  }
87
109
 
88
110
  // ─── Feature: theme ──────────────────────────────────────────────────────────
89
111
 
90
- function runTheme() {
91
- humanLog(`✓ For a custom theme, run \`${run} theme\` (browse) or \`${run} theme add <slug>\` (scaffold).`);
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
+ );
92
124
  }
93
125
 
94
126
  // ─── Feature: template ───────────────────────────────────────────────────────
95
127
 
96
- function runTemplate(targetDir, {templateName} = {}) {
128
+ async function runTemplate(targetDir, {interactive = true, templateName} = {}) {
97
129
  const templates = listTemplates();
98
130
  if (templates.length === 0) return;
99
131
 
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('');
110
- return;
111
- }
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
+ }
112
150
 
113
- if (!templates.includes(templateName)) {
114
- cliError(`Unknown template "${templateName}". Available: ${templates.join(', ')}`, {code: ERROR_CODES.ERR_UNKNOWN_TEMPLATE});
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`);
115
156
  return;
116
157
  }
117
158
 
118
- const outputDir = path.resolve(targetDir, `./src/pages/${templateName}`);
119
- const srcPath = path.join(CLI_ROOT, 'templates', 'pages', templateName, 'page.tsx');
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');
185
+
120
186
  fs.mkdirSync(outputDir, {recursive: true});
121
187
  fs.copyFileSync(srcPath, path.join(outputDir, 'page.tsx'));
122
- humanLog(`✓ Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
188
+
189
+ p.log.success(`Template created at ${path.relative(targetDir, outputDir)}/page.tsx`);
123
190
  }
124
191
 
125
192
  // ─── Command ─────────────────────────────────────────────────────────────────
@@ -133,7 +200,7 @@ export function registerInit(program) {
133
200
  .option('--remove-agents', 'Remove AI agent docs from all agent doc files')
134
201
  .option('--agent <tool>', 'Target AI tool for agent docs: claude, cursor, codex, hermes, all')
135
202
  .option('--agent-docs-path <path...>', 'Explicit file path(s) for agent docs')
136
- .action((options) => {
203
+ .action(async (options) => {
137
204
  const targetDir = process.cwd();
138
205
 
139
206
  // Remove mode
@@ -157,25 +224,62 @@ export function registerInit(program) {
157
224
 
158
225
  for (const feature of features) {
159
226
  if (feature === 'agents') runAgents(targetDir, {
227
+ interactive: false,
160
228
  agent: options.agent,
161
229
  agentDocsPath: options.agentDocsPath,
162
230
  });
163
- if (feature === 'theme') runTheme();
164
- if (feature === 'template') runTemplate(targetDir, {});
231
+ if (feature === 'theme') await runTheme({interactive: false});
232
+ if (feature === 'template') await runTemplate(targetDir, {interactive: false});
165
233
  }
166
234
  return;
167
235
  }
168
236
 
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,
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\``,
176
245
  });
177
- humanLog('');
178
- humanLog(` Tip: \`${run} init --all\` also points you to the theme and page-building workflows.`);
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!');
179
283
 
180
284
  for (const line of getNextSteps(run)) {
181
285
  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 astryx').join('\n');
26
+ const text = getNextSteps('npx').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,13 +1,15 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file Subprocess no-hang tests for `astryx init`.
4
+ * @file Subprocess no-hang tests for the interactivity contract.
5
5
  *
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.
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.
11
13
  */
12
14
 
13
15
  import {describe, it, expect, beforeEach, afterEach} from 'vitest';
@@ -40,24 +42,25 @@ afterEach(() => {
40
42
  fs.rmSync(tmpDir, {recursive: true, force: true});
41
43
  });
42
44
 
43
- describe('init is non-interactive by default (no TTY needed)', () => {
44
- it('runs cleanly (exit 0, no hang) and writes agent docs', () => {
45
+ describe('init non-interactive safety', () => {
46
+ it('fails fast (exit 1, no hang) and writes no files', () => {
45
47
  const r = runCli(['init']);
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
48
+ expect(r.signal).toBeNull();
49
+ expect(r.status).toBe(1);
50
+ expect(fs.readdirSync(tmpDir)).toEqual([]);
49
51
  });
50
52
 
51
- it('writes an agent-doc file containing the ASTRYX cheat-sheet marker', () => {
52
- runCli(['init']);
53
- // init injects into AGENTS.md/CLAUDE.md if present, else creates .claude/CLAUDE.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/);
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/);
58
61
  });
59
62
 
60
- it('still runs --features agents non-interactively', () => {
63
+ it('still runs --features agents non-interactively (guard does not over-catch)', () => {
61
64
  const r = runCli(['init', '--features', 'agents']);
62
65
  expect(r.signal).toBeNull();
63
66
  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
- // (log output, console.log strings, etc.) JSON.parse will throw —
48
+ // (clack 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
  }
@@ -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 === '-') {
23
+ if (expr === '-' || (!expr && !process.stdin.isTTY)) {
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 {getCliInvocation, formatCliCommand} from '../utils/package-manager.mjs';
19
+ import {getRunPrefix} 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 = getCliInvocation();
66
+ const run = getRunPrefix();
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} component --list`);
74
+ humanLog(`Try a broader term, or browse: ${run} astryx 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(` → ${formatCliCommand(r.command)}`);
89
+ humanLog(` → ${run} ${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})`);