@aiforui/install 0.1.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 +44 -0
- package/bin/cli.js +371 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @aiforui/install
|
|
2
|
+
|
|
3
|
+
Installs [aiforui.dev](https://aiforui.dev) agent skills into every supported
|
|
4
|
+
coding agent on your machine: Claude Code, Cursor, Codex, Amp, Gemini CLI,
|
|
5
|
+
OpenCode, Windsurf, and Antigravity.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
Grab your personal install command from [aiforui.dev](https://aiforui.dev)
|
|
10
|
+
(it includes your token), then run it:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx @aiforui/install --token=<token>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Preselect skills by passing their ids:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx @aiforui/install --token=<token> typography color
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Non-interactive (installs everything you own, keeps default names):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @aiforui/install --token=<token> -y
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
- The CLI detects which coding agents are installed by looking for their
|
|
31
|
+
config directories under your home directory.
|
|
32
|
+
- It fetches the skill files from aiforui.dev, authenticated by your
|
|
33
|
+
per-user token. Entitlement is checked live against your purchase, so the
|
|
34
|
+
same command keeps working as new skills are added.
|
|
35
|
+
- Skills are written into each agent's skills directory (for Windsurf, the
|
|
36
|
+
skill is appended to `global_rules.md`).
|
|
37
|
+
|
|
38
|
+
## Development
|
|
39
|
+
|
|
40
|
+
Point the CLI at a local or preview deployment:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
node bin/cli.js --token=<token> --api=http://localhost:3000
|
|
44
|
+
```
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @aiforui/install
|
|
5
|
+
*
|
|
6
|
+
* Installs aiforui.dev agent skills into every supported coding agent found
|
|
7
|
+
* on the machine. Skill content is fetched from aiforui.dev at runtime and
|
|
8
|
+
* gated by a per-user token, so this CLI rarely needs to be republished.
|
|
9
|
+
*
|
|
10
|
+
* npx @aiforui/install --token=<token> # interactive
|
|
11
|
+
* npx @aiforui/install --token=<token> typography # preselect a skill
|
|
12
|
+
* npx @aiforui/install --token=<token> -y # non-interactive
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { createRequire } from "node:module";
|
|
19
|
+
import * as p from "@clack/prompts";
|
|
20
|
+
import color from "picocolors";
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const { version } = require("../package.json");
|
|
24
|
+
|
|
25
|
+
const DEFAULT_API = "https://aiforui.dev";
|
|
26
|
+
|
|
27
|
+
const HOME = os.homedir();
|
|
28
|
+
const underHome = (...seg) => path.join(HOME, ...seg);
|
|
29
|
+
const existsUnderHome = (...seg) => fs.existsSync(underHome(...seg));
|
|
30
|
+
|
|
31
|
+
function writeSkillFiles(dir, files) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
fs.writeFileSync(path.join(dir, file.name), file.content);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every supported agent: how to detect it, where its skills live, and the
|
|
40
|
+
* prefix used to invoke a skill inside that tool (purely cosmetic, for the
|
|
41
|
+
* summary line).
|
|
42
|
+
*/
|
|
43
|
+
const AGENTS = [
|
|
44
|
+
{
|
|
45
|
+
id: "claude",
|
|
46
|
+
label: "Claude Code",
|
|
47
|
+
prefix: "/",
|
|
48
|
+
detect: () => existsUnderHome(".claude"),
|
|
49
|
+
install: (skill, name) =>
|
|
50
|
+
writeSkillFiles(underHome(".claude", "skills", name), skill.files),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "cursor",
|
|
54
|
+
label: "Cursor",
|
|
55
|
+
prefix: "/",
|
|
56
|
+
detect: () => existsUnderHome(".cursor"),
|
|
57
|
+
install: (skill, name) =>
|
|
58
|
+
writeSkillFiles(underHome(".cursor", "skills", name), skill.files),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "codex",
|
|
62
|
+
label: "Codex",
|
|
63
|
+
prefix: "$",
|
|
64
|
+
detect: () => existsUnderHome(".codex"),
|
|
65
|
+
install: (skill, name) => {
|
|
66
|
+
const dir = underHome(".codex", "skills", name);
|
|
67
|
+
writeSkillFiles(dir, skill.files);
|
|
68
|
+
fs.mkdirSync(path.join(dir, "agents"), { recursive: true });
|
|
69
|
+
fs.writeFileSync(
|
|
70
|
+
path.join(dir, "agents", "openai.yaml"),
|
|
71
|
+
`interface:\n display_name: "${skill.displayName}"\n short_description: "${skill.shortDescription}"\n`,
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: "opencode",
|
|
77
|
+
label: "OpenCode",
|
|
78
|
+
prefix: "@",
|
|
79
|
+
detect: () => existsUnderHome(".config", "opencode"),
|
|
80
|
+
install: (skill, name) =>
|
|
81
|
+
writeSkillFiles(
|
|
82
|
+
underHome(".config", "opencode", "skills", name),
|
|
83
|
+
skill.files,
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: "amp",
|
|
88
|
+
label: "Amp",
|
|
89
|
+
prefix: "",
|
|
90
|
+
detect: () => existsUnderHome(".amp"),
|
|
91
|
+
install: (skill, name) =>
|
|
92
|
+
writeSkillFiles(underHome(".config", "amp", "skills", name), skill.files),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "gemini",
|
|
96
|
+
label: "Gemini CLI",
|
|
97
|
+
prefix: "/",
|
|
98
|
+
detect: () => existsUnderHome(".gemini"),
|
|
99
|
+
install: (skill, name) =>
|
|
100
|
+
writeSkillFiles(underHome(".gemini", "skills", name), skill.files),
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: "antigravity",
|
|
104
|
+
label: "Antigravity",
|
|
105
|
+
prefix: "/",
|
|
106
|
+
detect: () => existsUnderHome(".gemini", "antigravity"),
|
|
107
|
+
install: (skill, name) =>
|
|
108
|
+
writeSkillFiles(
|
|
109
|
+
underHome(".gemini", "antigravity", "skills", name),
|
|
110
|
+
skill.files,
|
|
111
|
+
),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "windsurf",
|
|
115
|
+
label: "Windsurf",
|
|
116
|
+
prefix: "",
|
|
117
|
+
detect: () =>
|
|
118
|
+
existsUnderHome(".codeium") ||
|
|
119
|
+
existsUnderHome("Library", "Application Support", "Windsurf"),
|
|
120
|
+
install: (skill) => {
|
|
121
|
+
const skillFile = skill.files.find((f) => f.name === "SKILL.md");
|
|
122
|
+
if (!skillFile) return;
|
|
123
|
+
const dir = underHome(".codeium", "windsurf", "memories");
|
|
124
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
125
|
+
const rulesFile = path.join(dir, "global_rules.md");
|
|
126
|
+
const existing = fs.existsSync(rulesFile)
|
|
127
|
+
? fs.readFileSync(rulesFile, "utf-8")
|
|
128
|
+
: "";
|
|
129
|
+
if (existing.includes(skill.windsurfMarker)) return;
|
|
130
|
+
fs.appendFileSync(
|
|
131
|
+
rulesFile,
|
|
132
|
+
`${existing.length ? "\n" : ""}${skill.windsurfMarker}\n\n${skillFile.content}`,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
function parseArgs(argv) {
|
|
139
|
+
const args = { token: null, skills: [], yes: false, help: false };
|
|
140
|
+
for (let i = 0; i < argv.length; i++) {
|
|
141
|
+
const arg = argv[i];
|
|
142
|
+
if (arg === "--help" || arg === "-h") args.help = true;
|
|
143
|
+
else if (arg === "--yes" || arg === "-y") args.yes = true;
|
|
144
|
+
else if (arg === "--token") args.token = argv[++i];
|
|
145
|
+
else if (arg.startsWith("--token=")) args.token = arg.slice(8);
|
|
146
|
+
else if (arg.startsWith("--api=")) process.env.AIFORUI_API = arg.slice(6);
|
|
147
|
+
else if (!arg.startsWith("-")) args.skills.push(arg.toLowerCase());
|
|
148
|
+
}
|
|
149
|
+
return args;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function printHelp() {
|
|
153
|
+
console.log(`
|
|
154
|
+
${color.bold("@aiforui/install")}
|
|
155
|
+
|
|
156
|
+
Install aiforui.dev skills into your coding agents.
|
|
157
|
+
|
|
158
|
+
${color.bold("Usage")}
|
|
159
|
+
npx @aiforui/install --token=<token> [skills...]
|
|
160
|
+
|
|
161
|
+
${color.bold("Options")}
|
|
162
|
+
--token=<token> Your install token (from aiforui.dev)
|
|
163
|
+
-y, --yes Non-interactive: install everything you own, keep names
|
|
164
|
+
-h, --help Show this help
|
|
165
|
+
|
|
166
|
+
${color.bold("Examples")}
|
|
167
|
+
npx @aiforui/install --token=abc.def
|
|
168
|
+
npx @aiforui/install --token=abc.def typography
|
|
169
|
+
npx @aiforui/install --token=abc.def color ui-polish -y
|
|
170
|
+
|
|
171
|
+
Get your command at ${color.bold("https://aiforui.dev")}
|
|
172
|
+
`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sanitizeName(name) {
|
|
176
|
+
return String(name || "")
|
|
177
|
+
.trim()
|
|
178
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
179
|
+
.replace(/^-+|-+$/g, "");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function joinWithAnd(items) {
|
|
183
|
+
if (items.length <= 1) return items.join("");
|
|
184
|
+
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
|
185
|
+
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function fetchManifest(token, skills) {
|
|
189
|
+
const base = process.env.AIFORUI_API || DEFAULT_API;
|
|
190
|
+
const url = new URL("/api/install", base);
|
|
191
|
+
url.searchParams.set("token", token);
|
|
192
|
+
if (skills.length > 0) url.searchParams.set("skills", skills.join(","));
|
|
193
|
+
|
|
194
|
+
let res;
|
|
195
|
+
try {
|
|
196
|
+
res = await fetch(url.toString());
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Could not reach ${base}. Check your connection and try again.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let body = {};
|
|
204
|
+
try {
|
|
205
|
+
body = await res.json();
|
|
206
|
+
} catch {
|
|
207
|
+
/* keep {} */
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!res.ok) throw new Error(body.error || `Request failed (${res.status}).`);
|
|
211
|
+
return body;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function bail(message) {
|
|
215
|
+
p.cancel(message);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function prompt(value) {
|
|
220
|
+
if (p.isCancel(value)) bail("Installation cancelled.");
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function main() {
|
|
225
|
+
const args = parseArgs(process.argv.slice(2));
|
|
226
|
+
|
|
227
|
+
if (args.help) {
|
|
228
|
+
printHelp();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!args.token) {
|
|
233
|
+
console.error(color.red("\nA token is required."));
|
|
234
|
+
console.error("Get your install command at https://aiforui.dev\n");
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const interactive =
|
|
239
|
+
Boolean(process.stdin.isTTY && process.stdout.isTTY) && !args.yes;
|
|
240
|
+
|
|
241
|
+
p.intro(`${color.cyan("aiforui.dev")} installer ${color.dim(`v${version}`)}`);
|
|
242
|
+
|
|
243
|
+
// 1. Detect agents.
|
|
244
|
+
const detected = AGENTS.filter((a) => a.detect());
|
|
245
|
+
if (detected.length === 0) {
|
|
246
|
+
p.cancel(
|
|
247
|
+
"No supported agents detected. Install Claude Code, Cursor, Codex, Amp, Gemini CLI, OpenCode, Windsurf, or Antigravity first.",
|
|
248
|
+
);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// 2. Which agents?
|
|
253
|
+
let chosenAgents = detected;
|
|
254
|
+
if (interactive && detected.length > 1) {
|
|
255
|
+
const selected = await prompt(
|
|
256
|
+
await p.multiselect({
|
|
257
|
+
message: "Which tools do you want to set up?",
|
|
258
|
+
options: detected.map((a) => ({ value: a.id, label: a.label })),
|
|
259
|
+
initialValues: detected.map((a) => a.id),
|
|
260
|
+
required: true,
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
chosenAgents = detected.filter((a) => selected.includes(a.id));
|
|
264
|
+
} else if (!interactive) {
|
|
265
|
+
p.log.step(`Detected ${detected.map((a) => a.label).join(", ")}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 3. Fetch entitled skills.
|
|
269
|
+
const loader = p.spinner();
|
|
270
|
+
loader.start("Checking your access");
|
|
271
|
+
let manifest;
|
|
272
|
+
try {
|
|
273
|
+
manifest = await fetchManifest(args.token, args.skills);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
loader.stop("Couldn't reach aiforui.dev");
|
|
276
|
+
p.cancel(err.message);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const available = manifest.skills || [];
|
|
281
|
+
const unavailable = manifest.unavailable || [];
|
|
282
|
+
|
|
283
|
+
if (available.length === 0) {
|
|
284
|
+
loader.stop("No skills available");
|
|
285
|
+
p.cancel("None of the requested skills are available for your account.");
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
loader.stop(
|
|
289
|
+
`Found ${available.length} skill${available.length === 1 ? "" : "s"}`,
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
// 4. Which skills?
|
|
293
|
+
let chosenSkills = available;
|
|
294
|
+
if (interactive && args.skills.length === 0 && available.length > 1) {
|
|
295
|
+
const selected = await prompt(
|
|
296
|
+
await p.multiselect({
|
|
297
|
+
message: "Which skills do you want to install?",
|
|
298
|
+
options: available.map((sk) => ({
|
|
299
|
+
value: sk.id,
|
|
300
|
+
label: sk.name,
|
|
301
|
+
hint: sk.shortDescription,
|
|
302
|
+
})),
|
|
303
|
+
initialValues: available.map((sk) => sk.id),
|
|
304
|
+
required: true,
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
chosenSkills = available.filter((sk) => selected.includes(sk.id));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 5. Rename?
|
|
311
|
+
const names = new Map(chosenSkills.map((sk) => [sk.id, sk.name]));
|
|
312
|
+
if (interactive) {
|
|
313
|
+
const wantRename = await prompt(
|
|
314
|
+
await p.confirm({
|
|
315
|
+
message: "Do you want to rename any of these skills?",
|
|
316
|
+
initialValue: false,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
if (wantRename) {
|
|
320
|
+
for (const sk of chosenSkills) {
|
|
321
|
+
const answer = await prompt(
|
|
322
|
+
await p.text({
|
|
323
|
+
message: `Install ${color.bold(sk.displayName)} as`,
|
|
324
|
+
placeholder: sk.name,
|
|
325
|
+
defaultValue: sk.name,
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
328
|
+
names.set(sk.id, sanitizeName(answer) || sk.name);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 6. Install.
|
|
334
|
+
const installer = p.spinner();
|
|
335
|
+
installer.start(
|
|
336
|
+
`Installing ${chosenSkills.length} skill${chosenSkills.length === 1 ? "" : "s"}`,
|
|
337
|
+
);
|
|
338
|
+
for (const agent of chosenAgents) {
|
|
339
|
+
for (const sk of chosenSkills) {
|
|
340
|
+
try {
|
|
341
|
+
agent.install(sk, names.get(sk.id));
|
|
342
|
+
} catch {
|
|
343
|
+
/* keep going; summary reflects best effort */
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
installer.stop(
|
|
348
|
+
`Downloaded ${chosenSkills.length} skill${chosenSkills.length === 1 ? "" : "s"}`,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// 7. Per-agent summary.
|
|
352
|
+
for (const agent of chosenAgents) {
|
|
353
|
+
const list = chosenSkills.map((sk) =>
|
|
354
|
+
color.cyan(agent.prefix + names.get(sk.id)),
|
|
355
|
+
);
|
|
356
|
+
p.log.success(
|
|
357
|
+
`${color.bold(agent.label)} ${joinWithAnd(list)} ${color.dim("installed")}`,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
for (const u of unavailable) {
|
|
362
|
+
p.log.warn(`Skipped ${color.bold(u.id)} ${color.dim(`— ${u.reason}`)}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
p.outro("You're all set!");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
main().catch((err) => {
|
|
369
|
+
console.error(color.red(err && err.message ? err.message : String(err)));
|
|
370
|
+
process.exit(1);
|
|
371
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aiforui/install",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install aiforui.dev agent skills into Claude Code, Cursor, Codex, Amp, Windsurf, Gemini CLI, OpenCode, and Antigravity.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"aiforui-installer": "bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/cli.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@clack/prompts": "^0.11.0",
|
|
18
|
+
"picocolors": "^1.1.1"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ui",
|
|
22
|
+
"design",
|
|
23
|
+
"agent-skills",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"cursor",
|
|
26
|
+
"skills"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://aiforui.dev",
|
|
29
|
+
"license": "UNLICENSED",
|
|
30
|
+
"author": "Emil Kowalski"
|
|
31
|
+
}
|