@langchain/macaw-cli 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 LangChain, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # Macaw CLI
2
+
3
+ Discover the components installed in your project and set up agent guidance.
4
+ This package has no React or component runtime dependencies.
5
+
6
+ ```sh
7
+ pnpm add -D @langchain/macaw-cli
8
+ pnpm exec macaw search "loading"
9
+ pnpm exec macaw inspect Button --json
10
+ pnpm exec macaw init
11
+ ```
12
+
13
+ Run commands in a project with `@langchain/macaw-components` installed. Discovery
14
+ reads that version's catalog, so import suggestions match the installed API.
15
+ `list`, `search`, and `inspect` support `--json`; `search` also accepts `--limit`.
16
+
17
+ `init` creates `.agents/skills/macaw/SKILL.md` with links to the installed usage
18
+ guidance. It leaves existing files unchanged. No configuration is written during
19
+ npm installation. Agent integration templates live in this package; canonical
20
+ component guidance lives in `@langchain/macaw-components`.
package/bin/macaw.mjs ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.mjs';
package/dist/cli.mjs ADDED
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { createRequire } from 'node:module';
4
+ import { dirname, relative, resolve } from 'node:path';
5
+ import { CliError, main } from "./commands.mjs";
6
+ const projectRoot = process.cwd();
7
+ const requireFromProject = createRequire(resolve(projectRoot, 'package.json'));
8
+ function installedFile(specifier) {
9
+ try {
10
+ return requireFromProject.resolve(specifier);
11
+ }
12
+ catch {
13
+ throw new CliError('ERR_PACKAGE_NOT_FOUND', `Cannot resolve ${specifier} from ${projectRoot}. Install @langchain/macaw-components in this project first.`);
14
+ }
15
+ }
16
+ function catalog() {
17
+ const file = installedFile('@langchain/macaw-components/catalog.json');
18
+ const data = JSON.parse(readFileSync(file, 'utf8'));
19
+ if (data.schemaVersion !== 1 ||
20
+ data.packageName !== '@langchain/macaw-components' ||
21
+ !Array.isArray(data.components)) {
22
+ throw new CliError('ERR_CATALOG_VERSION', 'This component catalog is unsupported. Update @langchain/macaw-cli to a compatible version.');
23
+ }
24
+ return data.components;
25
+ }
26
+ function initialize() {
27
+ installedFile('@langchain/macaw-components/package.json');
28
+ // Keep links on the stable node_modules alias, not a versioned pnpm store path.
29
+ const moduleDirectory = requireFromProject.resolve
30
+ .paths('@langchain/macaw-components')
31
+ ?.find((directory) => existsSync(resolve(directory, '@langchain/macaw-components/package.json')));
32
+ if (!moduleDirectory)
33
+ throw new CliError('ERR_PACKAGE_NOT_FOUND', 'Cannot locate the installed component documentation.');
34
+ const components = resolve(moduleDirectory, '@langchain/macaw-components');
35
+ const file = resolve(projectRoot, '.agents/skills/macaw/SKILL.md');
36
+ const location = dirname(file);
37
+ const portablePath = (path) => relative(location, path).split('\\').join('/');
38
+ const template = readFileSync(new URL('../templates/SKILL.md', import.meta.url), 'utf8').replaceAll('{{componentsDocs}}', portablePath(resolve(components, 'docs')));
39
+ mkdirSync(location, { recursive: true });
40
+ try {
41
+ writeFileSync(file, template, { flag: 'wx' });
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error && 'code' in error && error.code === 'EEXIST') {
45
+ throw new CliError('ERR_ALREADY_CONFIGURED', `${file} already exists. It has been left unchanged.`);
46
+ }
47
+ throw error;
48
+ }
49
+ return relative(projectRoot, file);
50
+ }
51
+ main(process.argv.slice(2), catalog, initialize);
@@ -0,0 +1,224 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { parseArgs, styleText } from 'node:util';
3
+ const HELP = `Discover design-system components.
4
+
5
+ Usage: macaw <command>
6
+ init Set up agent guidance in this project
7
+ list Browse components (↑/↓ scroll, q quit)
8
+ search <query> Find components by name or capability
9
+ inspect <name> Show import, source files, and stories
10
+
11
+ Options: --json, --no-pager, --limit <1–100> (search; default 10), --help (-h)
12
+ For pipeable JSON: macaw <command> --json`;
13
+ export class CliError extends Error {
14
+ code;
15
+ suggestions;
16
+ constructor(code, message, suggestions = []) {
17
+ super(message);
18
+ this.code = code;
19
+ this.suggestions = suggestions;
20
+ }
21
+ }
22
+ function words(value) {
23
+ return value
24
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
25
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
26
+ .toLowerCase()
27
+ .split(/[^a-z0-9]+/)
28
+ .filter(Boolean)
29
+ .map((word) => word.length > 3
30
+ ? word
31
+ .replace(/ies$/, 'y')
32
+ .replace(/(ch|sh|x|z)es$/, '$1')
33
+ .replace(/(?<!s|u|i)s$/, '')
34
+ : word);
35
+ }
36
+ function distance(left, right) {
37
+ const row = Array.from({ length: right.length + 1 }, (_, index) => index);
38
+ for (let i = 1; i <= left.length; i += 1) {
39
+ let diagonal = row[0];
40
+ row[0] = i;
41
+ for (let j = 1; j <= right.length; j += 1) {
42
+ const previous = row[j];
43
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, diagonal + (left[i - 1] === right[j - 1] ? 0 : 1));
44
+ diagonal = previous;
45
+ }
46
+ }
47
+ return row[right.length];
48
+ }
49
+ function search(components, query, limit) {
50
+ const queryWords = new Set(words(query).filter((word) => ![
51
+ 'a',
52
+ 'an',
53
+ 'and',
54
+ 'for',
55
+ 'in',
56
+ 'of',
57
+ 'or',
58
+ 'the',
59
+ 'to',
60
+ 'with',
61
+ ].includes(word)));
62
+ const normalized = [...queryWords].join(' ');
63
+ return components
64
+ .map((component) => {
65
+ const name = component.name.toLowerCase();
66
+ const terms = new Set(words(`${component.name} ${component.family} ${component.keywords}`));
67
+ let score = [...queryWords].filter((word) => terms.has(word)).length * 8;
68
+ if (score < queryWords.size * 8)
69
+ score = 0;
70
+ if (name === query.toLowerCase() ||
71
+ (normalized && words(component.name).join(' ') === normalized))
72
+ score += 100;
73
+ if (normalized && words(component.name).join(' ').includes(normalized))
74
+ score += 30;
75
+ if (normalized &&
76
+ words(component.keywords).join(' ').includes(normalized))
77
+ score += 22;
78
+ if (normalized.length >= 4 &&
79
+ distance(name, normalized.replaceAll(' ', '')) <= 2)
80
+ score += 12;
81
+ return { ...component, score };
82
+ })
83
+ .filter(({ score }) => score > 0)
84
+ .sort((left, right) => right.score - left.score ||
85
+ Number(right.name === right.family) -
86
+ Number(left.name === left.family) ||
87
+ left.name.localeCompare(right.name))
88
+ .slice(0, limit);
89
+ }
90
+ function summary(component) {
91
+ return `${styleText('bold', component.name)}\n ${component.keywords}\n ${component.importStatement}`;
92
+ }
93
+ function componentList(components) {
94
+ const nameWidth = Math.max(9, ...components.map(({ name }) => name.length));
95
+ const width = Math.max(20, (process.stdout.columns || 100) - nameWidth - 4);
96
+ const rows = components.map(({ name, keywords }) => {
97
+ const lines = [''];
98
+ for (const word of (keywords || '—').split(' ')) {
99
+ const last = lines.length - 1;
100
+ if (lines[last] && lines[last].length + word.length + 1 > width) {
101
+ lines.push(word);
102
+ }
103
+ else {
104
+ lines[last] += `${lines[last] ? ' ' : ''}${word}`;
105
+ }
106
+ }
107
+ return lines
108
+ .map((line, index) => `${(index === 0 ? name : '').padEnd(nameWidth)} ${line}`)
109
+ .join('\n');
110
+ });
111
+ return [
112
+ styleText('bold', `Design system · ${components.length} components`),
113
+ 'Run pnpm design-system inspect <name> for imports and examples.',
114
+ '',
115
+ styleText('bold', `${'COMPONENT'.padEnd(nameWidth)} CAPABILITIES`),
116
+ ...rows,
117
+ ].join('\n');
118
+ }
119
+ function page(text) {
120
+ const result = spawnSync('less', ['-FRX', '+g', '-P', '↑/↓ scroll · Space next page · / search · q quit'], { input: `${text}\n`, stdio: ['pipe', 'inherit', 'inherit'] });
121
+ // Plain output also works on systems without less.
122
+ if (result.error || (result.status !== 0 && !result.signal)) {
123
+ process.stdout.write(`${text}\n`);
124
+ }
125
+ }
126
+ export function run(argv, catalog, initialize) {
127
+ const { values, positionals } = parseArgs({
128
+ args: argv,
129
+ allowPositionals: true,
130
+ options: {
131
+ json: { type: 'boolean' },
132
+ help: { type: 'boolean', short: 'h' },
133
+ limit: { type: 'string' },
134
+ 'no-pager': { type: 'boolean' },
135
+ },
136
+ });
137
+ const [requestedCommand, ...args] = positionals;
138
+ const command = requestedCommand === 'component' ? 'inspect' : requestedCommand;
139
+ function print(data, text, browse = false) {
140
+ if (browse &&
141
+ !values.json &&
142
+ !values['no-pager'] &&
143
+ process.stdin.isTTY &&
144
+ process.stdout.isTTY &&
145
+ process.env.TERM !== 'dumb' &&
146
+ !process.env.CI) {
147
+ page(text);
148
+ }
149
+ else {
150
+ process.stdout.write(`${values.json ? JSON.stringify(data) : text}\n`);
151
+ }
152
+ }
153
+ if (values.help || command === undefined) {
154
+ print({ usage: HELP }, HELP);
155
+ return;
156
+ }
157
+ if (command === 'init' && initialize) {
158
+ if (args.length > 0 || values.limit !== undefined) {
159
+ throw new CliError('ERR_INVALID_ARGUMENT', 'init does not accept arguments or --limit.');
160
+ }
161
+ const file = initialize();
162
+ print({ file }, `Created ${file}`);
163
+ return;
164
+ }
165
+ if (!['list', 'search', 'inspect'].includes(command)) {
166
+ throw new CliError('ERR_UNKNOWN_COMMAND', `Unknown command "${command}".`, [
167
+ 'list',
168
+ 'search',
169
+ 'inspect',
170
+ ]);
171
+ }
172
+ if ((command === 'list' && args.length > 0) ||
173
+ (command === 'inspect' && args.length > 1)) {
174
+ throw new CliError('ERR_INVALID_ARGUMENT', `Unexpected arguments for ${command}.`);
175
+ }
176
+ const query = args.join(' ').trim();
177
+ if (command !== 'list' && !query) {
178
+ throw new CliError('ERR_MISSING_ARGUMENT', `${command} requires ${command === 'search' ? 'a query' : 'a name'}.`);
179
+ }
180
+ const limit = Number(values.limit ?? 10);
181
+ if (values.limit !== undefined &&
182
+ (command !== 'search' ||
183
+ !/^\d+$/.test(values.limit) ||
184
+ limit < 1 ||
185
+ limit > 100)) {
186
+ throw new CliError('ERR_INVALID_ARGUMENT', '--limit requires search and an integer from 1 to 100.');
187
+ }
188
+ const components = catalog();
189
+ if (command === 'list') {
190
+ print(components, componentList(components), true);
191
+ }
192
+ else if (command === 'search') {
193
+ const results = search(components, query, limit);
194
+ print(results, results.map(summary).join('\n\n') || `No components matched "${query}".`, true);
195
+ }
196
+ else {
197
+ const component = components.find(({ name }) => name.toLowerCase() === query.toLowerCase());
198
+ if (!component) {
199
+ throw new CliError('ERR_UNKNOWN_COMPONENT', `No design-system component named "${query}".`, search(components, query, 3).map(({ name }) => name));
200
+ }
201
+ print(component, `${summary(component)}\n\nFamily: ${component.family}\nSource: ${component.sourceFiles.join(', ')}\nStories: ${component.stories.join(', ') || 'none'}`);
202
+ }
203
+ }
204
+ export function main(argv, catalog, initialize) {
205
+ try {
206
+ run(argv, catalog, initialize);
207
+ }
208
+ catch (error) {
209
+ const payload = {
210
+ error: error instanceof Error
211
+ ? error.message
212
+ : 'The design-system CLI failed.',
213
+ code: error instanceof Error &&
214
+ 'code' in error &&
215
+ typeof error.code === 'string' &&
216
+ error.code.startsWith('ERR_')
217
+ ? error.code
218
+ : 'ERR_CLI',
219
+ suggestions: error instanceof CliError ? error.suggestions : [],
220
+ };
221
+ process.stderr.write(`${argv.includes('--json') ? JSON.stringify(payload) : `${payload.code}: ${payload.error}\n${payload.suggestions.length ? `Try: ${payload.suggestions.join(', ')}` : 'Run with --help for usage.'}`}\n`);
222
+ process.exitCode = 1;
223
+ }
224
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@langchain/macaw-cli",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "Component discovery and agent setup for the Macaw Design System.",
5
+ "homepage": "https://github.com/langchain-ai/macaw-design-system#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/langchain-ai/macaw-design-system/issues"
8
+ },
9
+ "license": "MIT",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/langchain-ai/macaw-design-system.git",
13
+ "directory": "packages/cli"
14
+ },
15
+ "bin": {
16
+ "macaw": "./bin/macaw.mjs"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "dist",
21
+ "templates",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "type": "module",
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">=22"
31
+ },
32
+ "nx": {
33
+ "targets": {
34
+ "build": {
35
+ "outputs": [
36
+ "{projectRoot}/dist"
37
+ ]
38
+ }
39
+ }
40
+ },
41
+ "scripts": {
42
+ "build": "node ../../scripts/build.mjs cli"
43
+ }
44
+ }
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: macaw
3
+ description: Use Macaw components and semantic tokens when building application UI.
4
+ ---
5
+
6
+ # Build with Macaw
7
+
8
+ Read [component guidance]({{componentsDocs}}/DESIGN.md) and the relevant
9
+ [styling guidance]({{componentsDocs}}/STYLES.md) before implementing UI.
10
+ The token package also exports `@langchain/macaw-tokens/docs/TOKENS.md`.
11
+ These files belong to the installed package versions.
12
+
13
+ Use `pnpm exec macaw search "<capability>"` and
14
+ `pnpm exec macaw inspect <name> --json` to find components and exact imports.
15
+ Compose existing components, preserve keyboard and screen-reader behavior,
16
+ and use semantic tokens. Keep product data, routing, permissions, and analytics
17
+ in the application.
18
+
19
+ Import `@langchain/macaw-components/styles.css` once in the application entry
20
+ point. Inspect meaningful loading, empty, error, disabled, responsive, and
21
+ light/dark states when changing UI.