@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 +21 -0
- package/README.md +20 -0
- package/bin/macaw.mjs +2 -0
- package/dist/cli.mjs +51 -0
- package/dist/commands.mjs +224 -0
- package/package.json +44 -0
- package/templates/SKILL.md +21 -0
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
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.
|