@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.
Files changed (3) hide show
  1. package/README.md +44 -0
  2. package/bin/cli.js +371 -0
  3. 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
+ }