weasel-icons 0.10.0 → 0.11.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/README.md +33 -0
- package/bin/cli.mjs +264 -0
- package/dist/source.json +1 -0
- package/package.json +10 -1
- package/skill-data/customize/SKILL.md +55 -0
- package/skill-data/customize/references/motion.md +15 -0
- package/skill-data/customize/references/review.md +14 -0
- package/skills/weasel-customize/SKILL.md +18 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Icons that overreact beautifully. Stateful React SVG icons with smooth transitions and optional continuous behavior.
|
|
4
4
|
|
|
5
|
+
Version `0.11.0` adds an optional multi-agent customization skill and CLI with version-matched editable source.
|
|
6
|
+
|
|
5
7
|
Version `0.10.0` contains 874 icons, adding 200 across documents, collaboration, spatial tools, media, commerce, transport and characters. Existing released icons retain their APIs.
|
|
6
8
|
|
|
7
9
|
Version `0.7.0` contained 469 icons; `0.6.0` contained 265 icons.
|
|
@@ -16,6 +18,37 @@ pnpm add weasel-icons
|
|
|
16
18
|
|
|
17
19
|
Supports React / React DOM 18.3 or 19. Motion is installed as a dependency. No stylesheet, provider, Tailwind, or Next.js dependency is required. The package is ESM with TypeScript declarations and per-icon exports.
|
|
18
20
|
|
|
21
|
+
## Customize with your coding agent
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install weasel-icons
|
|
25
|
+
npx weasel-icons setup
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Choose all the agents you use: Codex, Claude Code, Antigravity IDE/App, Antigravity CLI or Cursor. Personal setup is the default and stays outside your repository. Then ask your agent: **“Use weasel-customize to add a state to my icon.”** Setup installs instructions; it does not launch an AI, share conversations or generate code by itself.
|
|
29
|
+
|
|
30
|
+
For non-interactive setup, or to add another agent later:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
npx weasel-icons setup --agent codex,claude-code --scope user
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Use `--scope project` for a team skill checked into this app. Existing or modified skill entries are preserved. Restart/reload the agent if the skill does not appear. The `weasel-customize` skill is separate from the internal `weasel-motion` authoring skill.
|
|
37
|
+
|
|
38
|
+
The agent reads the full guide from the installed package and copies only the selected icon and its required runtime:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm exec --no -- weasel-icons skills get customize
|
|
42
|
+
npm exec --no -- weasel-icons icons list refresh
|
|
43
|
+
npm exec --no -- weasel-icons icons add refresh --dir src/components/weasel
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Declare `motion` as a direct dependency when using copied source (use the dependency range from the installed `weasel-icons/package.json`). Copied TypeScript needs a compiler target of ES2015+ or downlevelIteration. Import the customized component from your local source. Other icons can keep their npm imports. Existing custom files are never overwritten; conflicting runtime versions require a separate destination. Keep the copied license notices with redistributed source.
|
|
47
|
+
|
|
48
|
+
The CLI uses bundled source and makes no network requests. It requires Node 22.13 or newer; the icon components themselves remain React 18.3/19 compatible. Personal setup paths are agent-specific: Codex `~/.agents/skills`, Claude Code `~/.claude/skills`, Antigravity IDE/App `~/.gemini/config/skills`, Antigravity CLI `~/.gemini/antigravity-cli/skills`, Cursor `~/.cursor/skills`. Project paths are `.agents/skills` for Codex/Antigravity, `.claude/skills` for Claude Code and `.cursor/skills` for Cursor.
|
|
49
|
+
|
|
50
|
+
To remove an unchanged discovery entry, run `npx weasel-icons setup --agent codex,claude-code --remove`. Modified entries and customized app source are preserved. Re-run setup to restore an entry. The detailed guide updates when the app's Weasel package is updated.
|
|
51
|
+
|
|
19
52
|
## Use
|
|
20
53
|
|
|
21
54
|
```tsx
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { lstat, mkdir, readFile, realpath, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
9
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
|
|
10
|
+
const skillName = 'weasel-customize';
|
|
11
|
+
const agents = {
|
|
12
|
+
codex: { label: 'Codex', user: '.agents/skills', project: '.agents/skills' },
|
|
13
|
+
'claude-code': { label: 'Claude Code', user: '.claude/skills', project: '.claude/skills' },
|
|
14
|
+
antigravity: {
|
|
15
|
+
label: 'Antigravity IDE / App',
|
|
16
|
+
user: '.gemini/config/skills',
|
|
17
|
+
project: '.agents/skills',
|
|
18
|
+
},
|
|
19
|
+
'antigravity-cli': {
|
|
20
|
+
label: 'Antigravity CLI',
|
|
21
|
+
user: '.gemini/antigravity-cli/skills',
|
|
22
|
+
project: '.agents/skills',
|
|
23
|
+
},
|
|
24
|
+
cursor: { label: 'Cursor', user: '.cursor/skills', project: '.cursor/skills' },
|
|
25
|
+
};
|
|
26
|
+
const help = `weasel-icons ${manifest.version}
|
|
27
|
+
|
|
28
|
+
setup Choose agents and install their discovery skill
|
|
29
|
+
setup --agent codex,claude-code --scope user
|
|
30
|
+
setup --agent codex --scope project
|
|
31
|
+
setup --agent codex --remove Remove only an unchanged Weasel discovery entry
|
|
32
|
+
skills get customize Read the installed version's customization guide
|
|
33
|
+
skills get customize <name> Read a guide reference (motion or review)
|
|
34
|
+
icons list [query] Find an icon's source name
|
|
35
|
+
icons add <name> [--dir path] Copy editable icon source and runtime into this app
|
|
36
|
+
|
|
37
|
+
Personal setup is the default. Project setup writes skill files into this repo.
|
|
38
|
+
Existing custom files are never overwritten. No network requests or AI calls.
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
function parse(argv) {
|
|
42
|
+
const positional = [],
|
|
43
|
+
options = {};
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
if (!arg.startsWith('--')) positional.push(arg);
|
|
47
|
+
else if (['--remove', '--help', '--version'].includes(arg)) options[arg.slice(2)] = true;
|
|
48
|
+
else if (['--agent', '--scope', '--dir'].includes(arg)) {
|
|
49
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--')) throw new Error(`Missing value for ${arg}`);
|
|
50
|
+
if (options[arg.slice(2)]) throw new Error(`Repeated option ${arg}`);
|
|
51
|
+
options[arg.slice(2)] = argv[++i];
|
|
52
|
+
} else throw new Error(`Unknown option ${arg}`);
|
|
53
|
+
}
|
|
54
|
+
return { positional, options };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function exists(path) {
|
|
58
|
+
try {
|
|
59
|
+
return await lstat(path);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error.code === 'ENOENT') return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolve ordinary parent symlinks (e.g. macOS /tmp), but refuse symlinks within
|
|
67
|
+
// a write target. An icon/skill destination cannot redirect writes elsewhere.
|
|
68
|
+
async function checkedTarget(root, path) {
|
|
69
|
+
const absolute = resolve(path);
|
|
70
|
+
const rel = relative(root, absolute);
|
|
71
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel))
|
|
72
|
+
throw new Error(`Destination must stay inside ${root}`);
|
|
73
|
+
let current = root;
|
|
74
|
+
for (const segment of rel.split(sep).filter(Boolean)) {
|
|
75
|
+
current = join(current, segment);
|
|
76
|
+
const info = await exists(current);
|
|
77
|
+
if (info?.isSymbolicLink()) throw new Error(`Refusing symlink destination: ${current}`);
|
|
78
|
+
}
|
|
79
|
+
return absolute;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function projectRoot() {
|
|
83
|
+
let directory = await realpath(process.cwd());
|
|
84
|
+
while (true) {
|
|
85
|
+
const path = join(directory, 'package.json');
|
|
86
|
+
if (await exists(path)) {
|
|
87
|
+
const project = JSON.parse(await readFile(path, 'utf8'));
|
|
88
|
+
if (project.name === manifest.name)
|
|
89
|
+
throw new Error('Run this inside your app, not the Weasel package.');
|
|
90
|
+
return directory;
|
|
91
|
+
}
|
|
92
|
+
if (dirname(directory) === directory)
|
|
93
|
+
throw new Error('Run this inside an app with a package.json.');
|
|
94
|
+
directory = dirname(directory);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function setup(options) {
|
|
99
|
+
let selected = options.agent?.split(',').filter(Boolean);
|
|
100
|
+
let scope = options.scope ?? 'user';
|
|
101
|
+
if (!selected?.length) {
|
|
102
|
+
if (!process.stdin.isTTY)
|
|
103
|
+
throw new Error('Choose agents with --agent codex,claude-code (or run setup interactively).');
|
|
104
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
105
|
+
try {
|
|
106
|
+
const names = Object.keys(agents);
|
|
107
|
+
console.log('Choose every agent you use:');
|
|
108
|
+
names.forEach((name, i) => console.log(` ${i + 1}. ${agents[name].label}`));
|
|
109
|
+
const answer = await prompt.question('Numbers separated by commas: ');
|
|
110
|
+
selected = answer.split(',').map((value) => names[Number(value.trim()) - 1]);
|
|
111
|
+
if (!options.scope) {
|
|
112
|
+
const answer = (await prompt.question('Scope: personal or project? [personal] ')).trim();
|
|
113
|
+
scope = answer === 'personal' || !answer ? 'user' : answer;
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
prompt.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!['user', 'project'].includes(scope)) throw new Error('Scope must be user or project.');
|
|
120
|
+
if (selected.some((name) => !Object.hasOwn(agents, name)))
|
|
121
|
+
throw new Error(`Supported agents: ${Object.keys(agents).join(', ')}`);
|
|
122
|
+
const root = scope === 'user' ? await realpath(homedir()) : await projectRoot();
|
|
123
|
+
const content = await readFile(join(packageRoot, 'skills', skillName, 'SKILL.md'), 'utf8');
|
|
124
|
+
const paths = [
|
|
125
|
+
...new Set(selected.map((name) => join(root, agents[name][scope], skillName, 'SKILL.md'))),
|
|
126
|
+
];
|
|
127
|
+
const plan = [];
|
|
128
|
+
// Preflight the entire selection before changing any agent's entry.
|
|
129
|
+
for (const path of paths) {
|
|
130
|
+
await checkedTarget(root, path);
|
|
131
|
+
const previous = await exists(path);
|
|
132
|
+
if (previous && (!previous.isFile() || (await readFile(path, 'utf8')) !== content))
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Preserving an existing or modified skill: ${path}. Move it deliberately before setup; nothing was changed.`,
|
|
135
|
+
);
|
|
136
|
+
plan.push({ path, previous });
|
|
137
|
+
}
|
|
138
|
+
if (options.remove) {
|
|
139
|
+
for (const { path, previous } of plan) {
|
|
140
|
+
if (previous) await unlink(path);
|
|
141
|
+
console.log(`${previous ? 'Removed' : 'Already absent'}: ${path}`);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const created = [];
|
|
146
|
+
try {
|
|
147
|
+
for (const { path, previous } of plan) {
|
|
148
|
+
if (!previous) {
|
|
149
|
+
await mkdir(dirname(path), { recursive: true });
|
|
150
|
+
await checkedTarget(root, path);
|
|
151
|
+
await writeFile(path, content, { flag: 'wx' });
|
|
152
|
+
created.push(path);
|
|
153
|
+
}
|
|
154
|
+
console.log(`${previous ? 'Already installed' : 'Installed'}: ${path}`);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
for (const path of created) await unlink(path);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
console.log('\nAsk your agent: Use weasel-customize to add a state to my icon.');
|
|
161
|
+
console.log('If it is not visible, start a new session or reload the agent.');
|
|
162
|
+
console.log('This installs instructions; it does not launch an AI or share chat history.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function sourceCommand(command, name, options) {
|
|
166
|
+
const source = JSON.parse(await readFile(join(packageRoot, 'dist', 'source.json'), 'utf8'));
|
|
167
|
+
if (source.version !== manifest.version)
|
|
168
|
+
throw new Error('Source and package versions do not match. Reinstall the package.');
|
|
169
|
+
if (command === 'list') {
|
|
170
|
+
for (const icon of Object.keys(source.icons)
|
|
171
|
+
.filter((icon) => icon.includes(name ?? ''))
|
|
172
|
+
.sort())
|
|
173
|
+
console.log(icon);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (!name || !Object.hasOwn(source.icons, name))
|
|
177
|
+
throw new Error('Unknown icon. Use: weasel-icons icons list <query>');
|
|
178
|
+
const root = await projectRoot();
|
|
179
|
+
const defaultDir = (await exists(join(root, 'src')))
|
|
180
|
+
? 'src/components/weasel'
|
|
181
|
+
: 'components/weasel';
|
|
182
|
+
const destination = await checkedTarget(root, resolve(root, options.dir ?? defaultDir));
|
|
183
|
+
if (
|
|
184
|
+
relative(root, destination)
|
|
185
|
+
.split(sep)
|
|
186
|
+
.some((part) => ['node_modules', '.git', '.agents', '.claude'].includes(part))
|
|
187
|
+
)
|
|
188
|
+
throw new Error(
|
|
189
|
+
'Choose an application source directory, outside dependencies and agent configuration.',
|
|
190
|
+
);
|
|
191
|
+
const files = new Set();
|
|
192
|
+
const visit = (path) => {
|
|
193
|
+
if (files.has(path)) return;
|
|
194
|
+
const file = source.files[path];
|
|
195
|
+
if (!file) throw new Error(`Missing bundled source: ${path}`);
|
|
196
|
+
files.add(path);
|
|
197
|
+
file.dependencies.forEach(visit);
|
|
198
|
+
};
|
|
199
|
+
source.icons[name].forEach(visit);
|
|
200
|
+
for (const file of ['LICENSE', 'THIRD_PARTY_NOTICES.md']) files.add(file);
|
|
201
|
+
const writes = [];
|
|
202
|
+
for (const file of files) {
|
|
203
|
+
const path = await checkedTarget(root, join(destination, file));
|
|
204
|
+
const content = source.files[file].content;
|
|
205
|
+
if (await exists(path)) {
|
|
206
|
+
if ((await readFile(path, 'utf8')) !== content)
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Preserving customized or different-version source: ${path}. Reuse it, or choose a new --dir. Nothing was copied.`,
|
|
209
|
+
);
|
|
210
|
+
} else writes.push({ path, content });
|
|
211
|
+
}
|
|
212
|
+
const created = [];
|
|
213
|
+
try {
|
|
214
|
+
for (const { path, content } of writes) {
|
|
215
|
+
await mkdir(dirname(path), { recursive: true });
|
|
216
|
+
await checkedTarget(root, path);
|
|
217
|
+
await writeFile(path, content, { flag: 'wx' });
|
|
218
|
+
created.push(path);
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
for (const path of created) await unlink(path);
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
console.log(`Weasel ${source.version}: ${writes.length} files copied for ${name}.`);
|
|
225
|
+
console.log(`Editable icon: ${join(destination, 'icons', name, `${name}-icon.tsx`)}`);
|
|
226
|
+
console.log(
|
|
227
|
+
'Import this local component when customizing. Existing package imports keep working.',
|
|
228
|
+
);
|
|
229
|
+
console.log(
|
|
230
|
+
'The copied source imports motion/react. Declare motion directly in your app before using it.',
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const { positional: args, options } = parse(process.argv.slice(2));
|
|
236
|
+
const [command, subcommand, name, ...extra] = args;
|
|
237
|
+
if (options.version) console.log(manifest.version);
|
|
238
|
+
else if (options.help || !command || command === 'help') console.log(help);
|
|
239
|
+
else if (command === 'setup' && args.length === 1) {
|
|
240
|
+
if (options.dir) throw new Error('--dir belongs to icons add.');
|
|
241
|
+
await setup(options);
|
|
242
|
+
} else if (
|
|
243
|
+
command === 'skills' &&
|
|
244
|
+
subcommand === 'get' &&
|
|
245
|
+
name === 'customize' &&
|
|
246
|
+
extra.length <= 1
|
|
247
|
+
) {
|
|
248
|
+
const reference = extra[0];
|
|
249
|
+
if (reference && !['motion', 'review'].includes(reference))
|
|
250
|
+
throw new Error('References: motion, review');
|
|
251
|
+
const path = reference
|
|
252
|
+
? `skill-data/customize/references/${reference}.md`
|
|
253
|
+
: 'skill-data/customize/SKILL.md';
|
|
254
|
+
console.log(`# Instructions for installed weasel-icons ${manifest.version}\n`);
|
|
255
|
+
console.log(await readFile(join(packageRoot, path), 'utf8'));
|
|
256
|
+
} else if (command === 'icons' && ['list', 'add'].includes(subcommand) && extra.length === 0) {
|
|
257
|
+
if (options.agent || options.scope || options.remove)
|
|
258
|
+
throw new Error('Agent options belong to setup.');
|
|
259
|
+
await sourceCommand(subcommand, name, options);
|
|
260
|
+
} else throw new Error('Unknown command. Run: weasel-icons --help');
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error(`Weasel: ${error.message}`);
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
}
|