agentic-web-developer 2.1.0 → 2.1.1

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 (2) hide show
  1. package/bin/install.js +725 -724
  2. package/package.json +2 -2
package/bin/install.js CHANGED
@@ -1,724 +1,725 @@
1
- #!/usr/bin/env node
2
- /**
3
- * agentic-web-developer installer
4
- *
5
- * npx agentic-web-developer install [--agent=<key>|all] [--source=<url|path>] [--token=<key>]
6
- * [--license-key=<key> | --admin-email=<e> --admin-secret=<s>]
7
- * [--force] [--dry-run] [--dir=<path>] [--no-commands]
8
- * npx agentic-web-developer detect
9
- *
10
- * The npm package ships only this installer (+ docs). The skill payload is
11
- * resolved from the first available source:
12
- *
13
- * 1. --source=<url|path> explicit flag (URL of a .tar.gz/.zip, or a local folder/archive)
14
- * 2. AWD_SOURCE=<url|path> environment variable
15
- * 3. skill/ next to this file local development checkout
16
- * 4. awd.source in package.json configured by the package author
17
- *
18
- * Private sources: --token=<key> or AWD_TOKEN=<key> (GITHUB_TOKEN is used
19
- * for github.com URLs). Installs each agent with its own file format.
20
- * No dependencies. Node 18+.
21
- */
22
-
23
- const fs = require("fs");
24
- const path = require("path");
25
- const os = require("os");
26
- const { execFileSync } = require("child_process");
27
-
28
- const argv = process.argv.slice(2);
29
- const flags = {};
30
- for (const arg of argv) {
31
- const m = arg.match(/^--([^=]+)(?:=(.*))?$/);
32
- if (m) flags[m[1]] = m[2] !== undefined ? m[2] : true;
33
- }
34
- const command = argv.find((a) => !a.startsWith("--")) || (flags.detect ? "detect" : "install");
35
-
36
- const HOME = flags.home ? path.resolve(flags.home) : os.homedir();
37
- const APPDATA = flags.home
38
- ? path.join(HOME, "AppData", "Roaming")
39
- : process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
40
- const LOCALAPPDATA = flags.home
41
- ? path.join(HOME, "AppData", "Local")
42
- : process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
43
- const PKG_ROOT = path.join(__dirname, "..");
44
- const BUNDLED_SKILL = path.join(PKG_ROOT, "skill");
45
- const SKILL_NAME = "agentic-web-developer";
46
- const MARKER = "<!-- agentic-web-developer | installed by npx agentic-web-developer -->";
47
-
48
- // format:
49
- // skill -> <dirs>/<name>/SKILL.md
50
- // skill-lower -> <dirs>/<name>/skill.md
51
- // rules-mdc -> <agentRoot>/rules/<name>.mdc (Cursor rules)
52
- // rules-md -> <agentRoot>/rules/<name>.md (Continue / Augment / Amazon Q)
53
- // conventions -> <dirs>/CONVENTIONS.md (Aider)
54
- // dirs = candidate skills directories (first existing one wins, else the first is created)
55
- // roots = used by `detect` to decide whether the agent is installed on this machine
56
- const AGENTS = {
57
- opencode: {
58
- name: "OpenCode",
59
- format: "skill",
60
- roots: [path.join(HOME, ".opencode"), path.join(HOME, ".config", "opencode")],
61
- dirs: [path.join(HOME, ".opencode", "skills"), path.join(HOME, ".config", "opencode", "skills")]
62
- },
63
- claude: {
64
- name: "Claude Code",
65
- format: "skill",
66
- roots: [path.join(HOME, ".claude"), path.join(APPDATA, "claude")],
67
- dirs: [path.join(HOME, ".claude", "skills"), path.join(APPDATA, "claude", "skills")]
68
- },
69
- copilot: {
70
- name: "GitHub Copilot",
71
- format: "skill",
72
- roots: [path.join(APPDATA, "github-copilot"), path.join(HOME, ".config", "github-copilot")],
73
- dirs: [path.join(APPDATA, "github-copilot", "skills"), path.join(HOME, ".config", "github-copilot", "skills")]
74
- },
75
- cursor: {
76
- name: "Cursor",
77
- format: "rules-mdc",
78
- roots: [path.join(HOME, ".cursor"), path.join(LOCALAPPDATA, "cursor")],
79
- dirs: [path.join(HOME, ".cursor", "skills"), path.join(LOCALAPPDATA, "cursor", "skills")]
80
- },
81
- windsurf: {
82
- name: "Windsurf (Codeium)",
83
- format: "skill-lower",
84
- roots: [path.join(HOME, ".codeium", "windsurf"), path.join(LOCALAPPDATA, "windsurf")],
85
- dirs: [path.join(HOME, ".codeium", "windsurf", "skills"), path.join(LOCALAPPDATA, "windsurf", "skills")]
86
- },
87
- cody: {
88
- name: "Cody (Sourcegraph)",
89
- format: "skill",
90
- roots: [path.join(HOME, ".cody"), path.join(APPDATA, "cody")],
91
- dirs: [path.join(HOME, ".cody", "skills"), path.join(APPDATA, "cody", "skills")]
92
- },
93
- codium: {
94
- name: "Codium",
95
- format: "skill-lower",
96
- roots: [path.join(HOME, ".codium"), path.join(LOCALAPPDATA, "codium")],
97
- dirs: [path.join(HOME, ".codium", "skills"), path.join(LOCALAPPDATA, "codium", "skills")]
98
- },
99
- aider: {
100
- name: "Aider",
101
- format: "conventions",
102
- roots: [path.join(HOME, ".aider")],
103
- dirs: [path.join(HOME, ".aider", "skills")]
104
- },
105
- continue: {
106
- name: "Continue",
107
- format: "rules-md",
108
- roots: [path.join(HOME, ".continue")],
109
- dirs: [path.join(HOME, ".continue", "skills")]
110
- },
111
- codex: {
112
- name: "Codex (OpenAI)",
113
- format: "skill",
114
- roots: [path.join(HOME, ".codex"), path.join(HOME, ".agents")],
115
- dirs: [path.join(HOME, ".codex", "skills"), path.join(HOME, ".agents", "skills")]
116
- },
117
- roo: {
118
- name: "Roo Code",
119
- format: "skill",
120
- roots: [path.join(HOME, ".roo"), path.join(HOME, ".agents")],
121
- dirs: [path.join(HOME, ".roo", "skills"), path.join(HOME, ".agents", "skills")]
122
- },
123
- cline: {
124
- name: "Cline",
125
- format: "skill",
126
- roots: [path.join(HOME, ".cline"), path.join(HOME, ".agents")],
127
- dirs: [path.join(HOME, ".cline", "skills"), path.join(HOME, ".agents", "skills")]
128
- },
129
- augment: {
130
- name: "Augment",
131
- format: "rules-md",
132
- roots: [path.join(HOME, ".augment")],
133
- dirs: [path.join(HOME, ".augment", "skills")]
134
- },
135
- amazonq: {
136
- name: "Amazon Q",
137
- format: "rules-md",
138
- roots: [path.join(HOME, ".amazonq"), path.join(HOME, ".aws", "amazonq")],
139
- dirs: [path.join(HOME, ".amazonq", "skills"), path.join(HOME, ".aws", "amazonq", "skills")]
140
- },
141
- jetbrains: {
142
- name: "JetBrains AI",
143
- format: "skill",
144
- roots: [path.join(HOME, ".junie")],
145
- dirs: [path.join(HOME, ".junie", "skills")]
146
- }
147
- };
148
-
149
- function readSource(skillDir) {
150
- const skillMd = path.join(skillDir, "SKILL.md");
151
- if (!fs.existsSync(skillMd)) {
152
- throw new Error(`no SKILL.md at ${skillMd}`);
153
- }
154
- const raw = fs.readFileSync(skillMd, "utf8");
155
- const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
156
- const body = fm ? raw.slice(fm[0].length) : raw;
157
- const descMatch = fm && fm[1].match(/^description:\s*(.+)$/m);
158
- const description = descMatch ? descMatch[1].trim().replace(/^['"]|['"]$/g, "") : SKILL_NAME;
159
- return { body, description };
160
- }
161
-
162
- function getFrontmatter(key, description) {
163
- const d = description.replace(/'/g, "''");
164
- const base = `name: ${SKILL_NAME}\ndescription: '${d}'\nlicense: Proprietary`;
165
- switch (key) {
166
- case "aider":
167
- return ""; // Aider uses plain markdown, no frontmatter
168
- case "opencode":
169
- return `---\n${base}\ncompatibility: opencode\n---\n\n`;
170
- case "cursor":
171
- return `---\n${base}\nglobs: ["**/*.{html,css,js,ts,jsx,tsx,vue,svelte,astro,md}"]\nalwaysApply: true\n---\n\n`;
172
- case "claude":
173
- return `---\n${base}\nversion: 1.0.0\n---\n\n`;
174
- case "copilot":
175
- return `---\n${base}\napplyTo: "**/*"\n---\n\n`;
176
- case "windsurf":
177
- case "codium":
178
- return `---\n${base}\ntrigger: always_on\n---\n\n`;
179
- case "cody":
180
- return `---\n${base}\n---\n\n`;
181
- case "continue":
182
- return `---\n${base}\nglobs: "**/*"\n---\n\n`;
183
- case "codex":
184
- return `---\n${base}\nmetadata:\n short-description: Build and sell high-converting websites\n---\n\n`;
185
- case "roo":
186
- case "cline":
187
- case "jetbrains":
188
- return `---\n${base}\nversion: 1.0.0\n---\n\n`;
189
- case "augment":
190
- case "amazonq":
191
- return `---\n${base}\n---\n\n`;
192
- default:
193
- return `---\n${base}\n---\n\n`;
194
- }
195
- }
196
-
197
- function pickDir(agent) {
198
- for (const d of agent.dirs) {
199
- if (fs.existsSync(d)) return d;
200
- }
201
- return agent.dirs[0];
202
- }
203
-
204
- // agentRoot: the agent's home (dir containing skills/ or rules/), derived from a skills dir
205
- function agentRootOf(skillsDir) {
206
- return path.basename(skillsDir) === "skills" ? path.dirname(skillsDir) : skillsDir;
207
- }
208
-
209
- function destFor(key, agent) {
210
- if (flags.dir) {
211
- const payloadDir = path.join(path.resolve(flags.dir), SKILL_NAME);
212
- return { payloadDir, mainFile: path.join(payloadDir, "SKILL.md") };
213
- }
214
- const base = pickDir(agent);
215
- const payloadDir = path.join(base, SKILL_NAME);
216
- const root = agentRootOf(base);
217
- switch (agent.format) {
218
- case "skill":
219
- return { payloadDir, mainFile: path.join(payloadDir, "SKILL.md") };
220
- case "skill-lower":
221
- return { payloadDir, mainFile: path.join(payloadDir, "skill.md") };
222
- case "rules-mdc":
223
- return { payloadDir, mainFile: path.join(root, "rules", `${SKILL_NAME}.mdc`) };
224
- case "rules-md":
225
- return { payloadDir, mainFile: path.join(root, "rules", `${SKILL_NAME}.md`) };
226
- case "conventions":
227
- return { payloadDir, mainFile: path.join(base, "CONVENTIONS.md") };
228
- default:
229
- return { payloadDir, mainFile: path.join(payloadDir, "SKILL.md") };
230
- }
231
- }
232
-
233
- function copyRecursive(src, dest) {
234
- const stat = fs.statSync(src);
235
- if (stat.isDirectory()) {
236
- fs.mkdirSync(dest, { recursive: true });
237
- for (const entry of fs.readdirSync(src)) {
238
- copyRecursive(path.join(src, entry), path.join(dest, entry));
239
- }
240
- } else {
241
- fs.copyFileSync(src, dest);
242
- }
243
- }
244
-
245
- function installPayload(payloadDir, skillDir) {
246
- copyRecursive(path.join(skillDir, "references"), path.join(payloadDir, "references"));
247
- copyRecursive(path.join(skillDir, "templates"), path.join(payloadDir, "templates"));
248
- if (fs.existsSync(path.join(skillDir, "departments"))) {
249
- copyRecursive(path.join(skillDir, "departments"), path.join(payloadDir, "departments"));
250
- }
251
- }
252
-
253
- /* ------------------------------ slash commands ------------------------------ */
254
- // Installed globally for opencode (~/.config/opencode/command/) and Claude Code
255
- // (~/.claude/commands/). Each full-name command file (agentic-web-developer-<slug>.md)
256
- // also gets a short alias (awd-<slug>.md) derived from the filename.
257
-
258
- const COMMANDS = {
259
- opencode: {
260
- name: "OpenCode",
261
- // fallback order: last entry wins when none exists — prefer ~/.config/opencode/command
262
- dirs: [
263
- path.join(HOME, ".opencode", "command"),
264
- path.join(HOME, ".config", "opencode", "command")
265
- ]
266
- },
267
- claude: {
268
- name: "Claude Code",
269
- // Claude Code reads user commands from ~/.claude/commands
270
- dirs: [path.join(HOME, ".claude", "commands")]
271
- }
272
- };
273
-
274
- // commands/ lives next to the skill folder (repo root) or inside it.
275
- function resolveCommandsSource(skillDir) {
276
- const nested = path.join(skillDir, "commands");
277
- if (fs.existsSync(nested)) return nested;
278
- const sibling = path.join(path.dirname(skillDir), "commands");
279
- if (fs.existsSync(sibling)) return sibling;
280
- return null;
281
- }
282
-
283
- function pickCommandDir(cmd) {
284
- for (const d of cmd.dirs) {
285
- if (fs.existsSync(d)) return d;
286
- }
287
- return cmd.dirs[cmd.dirs.length - 1];
288
- }
289
-
290
- function commandDestPaths(key) {
291
- if (flags.dir) {
292
- return { dir: path.join(path.resolve(flags.dir), "commands") };
293
- }
294
- const cmd = COMMANDS[key];
295
- if (!cmd) return null;
296
- return { dir: pickCommandDir(cmd) };
297
- }
298
-
299
- // Writes every command file + derived awd-* alias. Returns written/skipped counts.
300
- function installCommands(skillDir, key) {
301
- const spec = commandDestPaths(key);
302
- if (!spec) return null;
303
- const src = resolveCommandsSource(skillDir);
304
- if (!src) return null;
305
-
306
- let written = 0;
307
- let skipped = 0;
308
- if (flags["dry-run"]) {
309
- const names = fs.readdirSync(src).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
310
- const all = names.flatMap((n) =>
311
- n.startsWith(`${SKILL_NAME}-`) ? [n, `awd-${n.slice(SKILL_NAME.length + 1)}`] : [n]
312
- );
313
- console.log(` cmds: ${spec.dir}{/${all.join(", /")}}`);
314
- return { dir: spec.dir, written: 0, skipped: all.length, dryRun: true };
315
- }
316
-
317
- fs.mkdirSync(spec.dir, { recursive: true });
318
- for (const entry of fs.readdirSync(src)) {
319
- if (!entry.endsWith(".md")) continue;
320
- const base = entry.replace(/\.md$/, "");
321
- const targets = [entry];
322
- if (base.startsWith(`${SKILL_NAME}-`)) {
323
- targets.push(`awd-${base.slice(SKILL_NAME.length + 1)}.md`);
324
- }
325
- const body = fs.readFileSync(path.join(src, entry), "utf8");
326
- for (const t of targets) {
327
- const dest = path.join(spec.dir, t);
328
- if (fs.existsSync(dest) && !flags.force) {
329
- skipped++;
330
- continue;
331
- }
332
- fs.writeFileSync(dest, body);
333
- written++;
334
- }
335
- }
336
- return { dir: spec.dir, written, skipped, dryRun: false };
337
- }
338
-
339
- /* ------------------------- license config (the gate) ----------------------- */
340
-
341
- function licenseFlagsPresent() {
342
- return Boolean(flags["license-key"] || flags["admin-email"] || flags["admin-secret"]);
343
- }
344
-
345
- function validateLicenseFlags() {
346
- const key = flags["license-key"];
347
- const adminEmail = flags["admin-email"];
348
- const adminSecret = flags["admin-secret"];
349
- if (key && (adminEmail || adminSecret)) {
350
- throw new Error("--license-key cannot be combined with --admin-email/--admin-secret");
351
- }
352
- if ((adminEmail && !adminSecret) || (!adminEmail && adminSecret)) {
353
- throw new Error("--admin-email and --admin-secret must be provided together");
354
- }
355
- }
356
-
357
- // Writes animotion-config.json into the installed skill folder — the file the
358
- // agentic-web-developer license gate reads (skill dir first, then its parent,
359
- // ~/.animotion-skills/, AGENTIC_LICENSE_CONFIG).
360
- function writeLicenseConfig(payloadDir) {
361
- if (!licenseFlagsPresent()) return null;
362
- const cfg = flags["admin-email"]
363
- ? { tier: "pro", requiredPlan: "pro", authMethod: "admin", adminEmail: flags["admin-email"], adminSecret: flags["admin-secret"] }
364
- : { tier: "pro", requiredPlan: "pro", authMethod: "license", licenseKey: flags["license-key"] };
365
- const file = path.join(payloadDir, "animotion-config.json");
366
- fs.mkdirSync(payloadDir, { recursive: true });
367
- fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\n`);
368
- try { fs.chmodSync(file, 0o600); } catch { /* best-effort; Windows ignores */ }
369
- return file;
370
- }
371
-
372
- /* ---------------- skill source resolution (private fetch) ---------------- */
373
-
374
- function expandHome(p) {
375
- if (p === "~") return HOME;
376
- if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(HOME, p.slice(2));
377
- return p;
378
- }
379
-
380
- // Find the folder holding SKILL.md inside an extracted/downloaded tree.
381
- // Accepts: the skill folder itself, a folder containing skill/, or an
382
- // archive whose repo root is the skill (depth <= 3).
383
- function findSkillRoot(root, depth) {
384
- if (fs.existsSync(path.join(root, "SKILL.md"))) return root;
385
- const nested = path.join(root, "skill");
386
- if (fs.existsSync(path.join(nested, "SKILL.md"))) return nested;
387
- if (depth >= 3) return null;
388
- let entries;
389
- try {
390
- entries = fs.readdirSync(root, { withFileTypes: true });
391
- } catch {
392
- return null;
393
- }
394
- for (const e of entries) {
395
- if (!e.isDirectory()) continue;
396
- const found = findSkillRoot(path.join(root, e.name), depth + 1);
397
- if (found) return found;
398
- }
399
- return null;
400
- }
401
-
402
- function extractArchive(file) {
403
- const work = fs.mkdtempSync(path.join(os.tmpdir(), "awd-src-"));
404
- const args = /\.zip$/i.test(file) ? ["-xf", file, "-C", work] : ["-xzf", file, "-C", work];
405
- try {
406
- execFileSync("tar", args, { stdio: "pipe" });
407
- } catch (e) {
408
- throw new Error(
409
- `could not extract ${path.basename(file)} with tar (tar must be on PATH; use a .tar.gz archive — raw: ${e.message})`
410
- );
411
- }
412
- return work;
413
- }
414
-
415
- async function downloadTo(url, label) {
416
- const headers = {
417
- "User-Agent": "agentic-web-developer-installer",
418
- Accept: "application/octet-stream, */*"
419
- };
420
- const isGithub = /github\.com|githubusercontent\.com/i.test(url);
421
- const token = flags.token || process.env.AWD_TOKEN || (isGithub ? process.env.GITHUB_TOKEN : undefined);
422
- if (token) headers.Authorization = `Bearer ${token}`;
423
-
424
- let res;
425
- try {
426
- res = await fetch(url, { headers, redirect: "follow" });
427
- } catch (e) {
428
- throw new Error(`${label}: network error fetching skill source — ${e.message}`);
429
- }
430
- if (!res.ok) {
431
- const hint =
432
- res.status === 404 || res.status === 403 || res.status === 401
433
- ? " (private source? add --token=<key> or AWD_TOKEN=<key>)"
434
- : "";
435
- throw new Error(`${label}: download failed — HTTP ${res.status} ${res.statusText}${hint}`);
436
- }
437
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "awd-dl-"));
438
- const name = (url.split("?")[0].split("/").pop() || "source").replace(/[^A-Za-z0-9._-]/g, "_");
439
- const file = path.join(dir, name.includes(".") ? name : `${name}.tar.gz`);
440
- fs.writeFileSync(file, Buffer.from(await res.arrayBuffer()));
441
- return file;
442
- }
443
-
444
- async function resolveLocation(loc, label) {
445
- if (/^https?:\/\//i.test(loc)) {
446
- console.log(`skill source (${label}): ${loc}`);
447
- const file = await downloadTo(loc, label);
448
- const root = findSkillRoot(extractArchive(file), 0);
449
- if (!root) throw new Error(`${label}: the downloaded archive contains no SKILL.md — does the source contain the skill/ folder?`);
450
- return root;
451
- }
452
-
453
- const p = path.resolve(expandHome(loc));
454
- if (!fs.existsSync(p)) return null;
455
- if (fs.statSync(p).isDirectory()) {
456
- const root = findSkillRoot(p, 0);
457
- if (!root) throw new Error(`${label}: no SKILL.md found under ${p}`);
458
- return root;
459
- }
460
- const root = findSkillRoot(extractArchive(p), 0);
461
- if (!root) throw new Error(`${label}: the archive contains no SKILL.md`);
462
- return root;
463
- }
464
-
465
- function readPkg() {
466
- try {
467
- return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8"));
468
- } catch {
469
- return null;
470
- }
471
- }
472
-
473
- async function resolveSource() {
474
- // 1. explicit flag
475
- if (flags.source) {
476
- const r = await resolveLocation(String(flags.source), "--source");
477
- if (!r) throw new Error(`--source: no such file or directory: ${flags.source}`);
478
- return r;
479
- }
480
- // 2. environment
481
- if (process.env.AWD_SOURCE) {
482
- const r = await resolveLocation(process.env.AWD_SOURCE, "AWD_SOURCE");
483
- if (!r) throw new Error(`AWD_SOURCE: no such file or directory: ${process.env.AWD_SOURCE}`);
484
- return r;
485
- }
486
- // 3. local development checkout (skill/ next to bin/)
487
- if (fs.existsSync(path.join(BUNDLED_SKILL, "SKILL.md"))) return BUNDLED_SKILL;
488
- // 4. source configured by the package author
489
- const pkg = readPkg();
490
- const pkgSource = pkg && pkg.awd && pkg.awd.source;
491
- if (pkgSource) {
492
- const r = await resolveLocation(String(pkgSource), "package.json awd.source");
493
- if (r) return r;
494
- throw new Error(`package.json awd.source could not be resolved: ${pkgSource}`);
495
- }
496
- throw new Error(
497
- [
498
- "no skill source found — this npm package ships only the installer (+ docs).",
499
- "Provide the skill payload with one of:",
500
- " --source=<url|path> a .tar.gz/.zip URL, or a local skill folder/archive",
501
- " AWD_SOURCE=<url|path> same, via environment variable",
502
- " awd.source in package.json set by the package author before publishing",
503
- "Private sources also need a token: --token=<key> or AWD_TOKEN=<key>",
504
- "Local development: keep the skill/ folder next to bin/ — it is used automatically."
505
- ].join("\n")
506
- );
507
- }
508
-
509
- /* -------------------------------- commands -------------------------------- */
510
-
511
- function detect() {
512
- console.log(`\nAgents on this machine (and ${SKILL_NAME} install status):\n`);
513
- let found = 0;
514
- for (const [key, agent] of Object.entries(AGENTS)) {
515
- const present = agent.roots.some((r) => fs.existsSync(r));
516
- const { mainFile, payloadDir } = destFor(key, agent);
517
- let installed = fs.existsSync(mainFile);
518
- // Aider's CONVENTIONS.md may belong to another skill — require our marker
519
- if (installed && agent.format === "conventions") {
520
- installed = fs.readFileSync(mainFile, "utf8").includes(MARKER);
521
- }
522
- if (installed && !fs.existsSync(payloadDir)) installed = false;
523
- if (present) found++;
524
- let status = installed ? "INSTALLED" : present ? "not installed" : "agent not found";
525
- // slash commands (opencode + claude only)
526
- let cmdNote = "";
527
- if (COMMANDS[key]) {
528
- const dir = pickCommandDir(COMMANDS[key]);
529
- const mainCmd = path.join(dir, `${SKILL_NAME}.md`);
530
- const aliasCmd = path.join(dir, "awd-strategy.md");
531
- if (fs.existsSync(mainCmd) || fs.existsSync(aliasCmd)) {
532
- cmdNote = ` cmds: ${dir}`;
533
- } else if (installed) {
534
- cmdNote = " cmds: not installed";
535
- }
536
- }
537
- console.log(
538
- ` ${present || installed ? "*" : " "} ${key.padEnd(10)} ${agent.name.padEnd(20)} ${status}` +
539
- (installed ? ` -> ${mainFile}` : "") +
540
- cmdNote
541
- );
542
- }
543
- console.log(`\n Install: npx ${SKILL_NAME} install --agent=<key> (or --all, or omit --agent for auto)\n`);
544
- return found;
545
- }
546
-
547
- async function install() {
548
- validateLicenseFlags();
549
- const skillDir = await resolveSource();
550
- const { body, description } = readSource(skillDir);
551
-
552
- let keys;
553
- if (flags.agent === "all") {
554
- keys = Object.keys(AGENTS);
555
- } else if (flags.agent) {
556
- if (!AGENTS[flags.agent]) {
557
- throw new Error(`unknown agent "${flags.agent}". Valid keys: ${Object.keys(AGENTS).join(", ")}, all`);
558
- }
559
- keys = [flags.agent];
560
- } else if (flags.all) {
561
- keys = Object.keys(AGENTS);
562
- } else {
563
- // auto: opencode first, then claude, then any detected agent
564
- const detected = Object.entries(AGENTS).filter(([, a]) => a.roots.some((r) => fs.existsSync(r)));
565
- const preferred = ["opencode", "claude"].filter((k) => detected.some(([dk]) => dk === k));
566
- keys = [preferred[0] || (detected[0] && detected[0][0]) || "opencode"];
567
- }
568
-
569
- let done = 0;
570
- for (const key of keys) {
571
- const agent = AGENTS[key];
572
- const { payloadDir, mainFile } = destFor(key, agent);
573
- const isRuleFile = ["rules-mdc", "rules-md", "conventions"].includes(agent.format);
574
-
575
- if (flags["dry-run"]) {
576
- console.log(`[dry-run] ${agent.name} (${key})`);
577
- console.log(` main: ${mainFile}`);
578
- console.log(` refs: ${payloadDir}{/references, /templates, /departments}`);
579
- if (licenseFlagsPresent()) console.log(` lic: ${path.join(payloadDir, "animotion-config.json")}`);
580
- if (!flags["no-commands"]) installCommands(skillDir, key);
581
- done++;
582
- continue;
583
- }
584
-
585
- if (fs.existsSync(mainFile) && !flags.force) {
586
- if (agent.format === "conventions") {
587
- const existing = fs.readFileSync(mainFile, "utf8");
588
- if (!existing.includes(MARKER)) {
589
- console.log(`skip ${agent.name}: ${mainFile} already exists and was not written by this installer (use --force to overwrite)`);
590
- continue;
591
- }
592
- }
593
- console.log(`skip ${agent.name}: already installed at ${mainFile} (use --force to overwrite)`);
594
- const existingCfg = writeLicenseConfig(payloadDir);
595
- if (existingCfg) console.log(` license config: ${existingCfg}`);
596
- if (!flags["no-commands"]) {
597
- const cmds = installCommands(skillDir, key);
598
- if (cmds && !cmds.dryRun) {
599
- console.log(` commands: ${cmds.written} written, ${cmds.skipped} kept -> ${cmds.dir}`);
600
- }
601
- }
602
- continue;
603
- }
604
-
605
- fs.mkdirSync(payloadDir, { recursive: true });
606
- installPayload(payloadDir, skillDir);
607
-
608
- // Frontmatter per agent; rule files get a pointer to the reference payload
609
- let content = getFrontmatter(key, description) + body;
610
- if (isRuleFile) {
611
- const pointer = `> Reference playbooks: ${path.join(payloadDir, "references")} — templates: ${path.join(payloadDir, "templates")}\n\n`;
612
- const fm = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n\n/);
613
- if (fm) {
614
- content = fm[0] + pointer + content.slice(fm[0].length);
615
- } else {
616
- content = `${MARKER}\n${pointer}${content}`;
617
- }
618
- }
619
- fs.mkdirSync(path.dirname(mainFile), { recursive: true });
620
- fs.writeFileSync(mainFile, content);
621
- const cfgFile = writeLicenseConfig(payloadDir);
622
- console.log(`installed ${agent.name} (${key})`);
623
- console.log(` main: ${mainFile}`);
624
- console.log(` refs: ${payloadDir}`);
625
- if (cfgFile) console.log(` license config: ${cfgFile}`);
626
- if (!flags["no-commands"]) {
627
- const cmds = installCommands(skillDir, key);
628
- if (cmds && !cmds.dryRun) {
629
- console.log(` commands: ${cmds.written} written, ${cmds.skipped} kept -> ${cmds.dir}`);
630
- }
631
- }
632
- done++;
633
- }
634
-
635
- if (!done) {
636
- console.log("nothing installed — everything already present (use --force to overwrite)");
637
- } else if (!flags["dry-run"]) {
638
- if (licenseFlagsPresent()) {
639
- console.log("\nLicense saved — the skill's gate reads animotion-config.json automatically.");
640
- }
641
- console.log(`\nDone. Restart your AI agent so it loads ${SKILL_NAME}.`);
642
- console.log("Animations for any site built with this skill are handled by the AnimotionJS agentic skill:");
643
- console.log(" npx animotion-skills install --tier=agentic --agent=<agent> --license-key=YOUR_KEY");
644
- }
645
- }
646
-
647
- switch (command) {
648
- case "detect":
649
- detect();
650
- break;
651
- case "install":
652
- install().catch((err) => {
653
- console.error(`error: ${err.message}`);
654
- process.exit(1);
655
- });
656
- break;
657
- case "help":
658
- case "--help":
659
- usage();
660
- break;
661
- default:
662
- usage();
663
- process.exit(1);
664
- }
665
-
666
- function usage() {
667
- console.log(`
668
- ${SKILL_NAME} — install the agentic-web-developer skill into your AI agent
669
-
670
- Usage:
671
- npx ${SKILL_NAME} install [options]
672
- npx ${SKILL_NAME} detect
673
-
674
- Options:
675
- --agent=<key> Target one agent: ${Object.keys(AGENTS).join(", ")}
676
- --agent=all Target every supported agent
677
- --all Same as --agent=all
678
- --source=<s> Skill source: a .tar.gz/.zip URL or a local folder/archive
679
- (falls back to AWD_SOURCE, then skill/, then awd.source)
680
- --token=<key> Auth token for a private source (or AWD_TOKEN=<key>)
681
- --license-key=<key>
682
- Save your Pro license key to the installed skill
683
- (writes animotion-config.json the license gate reads)
684
- --admin-email=<e> --admin-secret=<s>
685
- Admin credentials instead of a key (Animotion staff /
686
- whitelisted emails only — both flags required together)
687
- --force Overwrite an existing install
688
- --dry-run Print target paths without copying
689
- --dir=<path> Install into <path>/${SKILL_NAME} (flat, SKILL.md layout)
690
- --home=<path> Use <path> as the home directory (testing)
691
- --no-commands Skip installing the slash commands (/agentic-web-developer-* + /awd-*)
692
-
693
- Skill source order: --source > AWD_SOURCE > ./skill > package.json awd.source
694
- The npm package ships only this installer (+ docs) — never the skill source.
695
-
696
- Slash commands (opencode + claude, unless --no-commands):
697
- /agentic-web-developer router (starts at Strategy)
698
- /agentic-web-developer-<dept> full name per department
699
- /awd-<dept> short alias (strategy, research, content,
700
- design, frontend, backend, motion, qa,
701
- deploy, launch, growth)
702
- /awd-<dept> <stage> stage jump (e.g. /awd-design 4.3)
703
- /awd-list print every department + stage; start anywhere
704
- /awd-taste taste training (not a department —
705
- builds/edits your taste profile any time)
706
- -> opencode: ~/.config/opencode/command/ claude: ~/.claude/commands/
707
-
708
- Each agent gets its own file format:
709
- skill agents (opencode, claude, copilot, cody, codex, roo, cline, jetbrains)
710
- -> <skills>/${SKILL_NAME}/SKILL.md
711
- windsurf, codium -> <skills>/${SKILL_NAME}/skill.md
712
- cursor -> ~/.cursor/rules/${SKILL_NAME}.mdc
713
- continue -> ~/.continue/rules/${SKILL_NAME}.md
714
- augment -> ~/.augment/rules/${SKILL_NAME}.md
715
- amazonq -> ~/.amazonq/rules/${SKILL_NAME}.md
716
- aider -> ~/.aider/skills/CONVENTIONS.md
717
-
718
- Examples:
719
- npx ${SKILL_NAME} install # auto-detect agent
720
- npx ${SKILL_NAME} install --agent=opencode --license-key=sec_live_xxxxxxxx
721
- npx ${SKILL_NAME} install --agent=all --source=https://you/server/skill.tar.gz --token=KEY
722
- npx ${SKILL_NAME} detect
723
- `);
724
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * agentic-web-developer installer
4
+ *
5
+ * npx agentic-web-developer install [--agent=<key>|all] [--source=<url|path>] [--token=<key>]
6
+ * [--license-key=<key> | --admin-email=<e> --admin-secret=<s>]
7
+ * [--force] [--dry-run] [--dir=<path>] [--no-commands]
8
+ * npx agentic-web-developer detect
9
+ *
10
+ * The npm package ships only this installer (+ docs). The skill payload is
11
+ * resolved from the first available source:
12
+ *
13
+ * 1. --source=<url|path> explicit flag (URL of a .tar.gz/.zip, or a local folder/archive)
14
+ * 2. AWD_SOURCE=<url|path> environment variable
15
+ * 3. skill/ next to this file local development checkout
16
+ * 4. awd.source in package.json configured by the package author
17
+ *
18
+ * Private sources: --token=<key> or AWD_TOKEN=<key> (GITHUB_TOKEN is used
19
+ * for github.com URLs). Installs each agent with its own file format.
20
+ * No dependencies. Node 18+.
21
+ */
22
+
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+ const os = require("os");
26
+ const { execFileSync } = require("child_process");
27
+
28
+ const argv = process.argv.slice(2);
29
+ const flags = {};
30
+ for (const arg of argv) {
31
+ const m = arg.match(/^--([^=]+)(?:=(.*))?$/);
32
+ if (m) flags[m[1]] = m[2] !== undefined ? m[2] : true;
33
+ }
34
+ const command = argv.find((a) => !a.startsWith("--")) || (flags.detect ? "detect" : "install");
35
+
36
+ const HOME = flags.home ? path.resolve(flags.home) : os.homedir();
37
+ const APPDATA = flags.home
38
+ ? path.join(HOME, "AppData", "Roaming")
39
+ : process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
40
+ const LOCALAPPDATA = flags.home
41
+ ? path.join(HOME, "AppData", "Local")
42
+ : process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
43
+ const PKG_ROOT = path.join(__dirname, "..");
44
+ const BUNDLED_SKILL = path.join(PKG_ROOT, "skill");
45
+ const SKILL_NAME = "agentic-web-developer";
46
+ const MARKER = "<!-- agentic-web-developer | installed by npx agentic-web-developer -->";
47
+
48
+ // format:
49
+ // skill -> <dirs>/<name>/SKILL.md
50
+ // skill-lower -> <dirs>/<name>/skill.md
51
+ // rules-mdc -> <agentRoot>/rules/<name>.mdc (Cursor rules)
52
+ // rules-md -> <agentRoot>/rules/<name>.md (Continue / Augment / Amazon Q)
53
+ // conventions -> <dirs>/CONVENTIONS.md (Aider)
54
+ // dirs = candidate skills directories (first existing one wins, else the first is created)
55
+ // roots = used by `detect` to decide whether the agent is installed on this machine
56
+ const AGENTS = {
57
+ opencode: {
58
+ name: "OpenCode",
59
+ format: "skill",
60
+ roots: [path.join(HOME, ".opencode"), path.join(HOME, ".config", "opencode")],
61
+ dirs: [path.join(HOME, ".opencode", "skills"), path.join(HOME, ".config", "opencode", "skills")]
62
+ },
63
+ claude: {
64
+ name: "Claude Code",
65
+ format: "skill",
66
+ roots: [path.join(HOME, ".claude"), path.join(APPDATA, "claude")],
67
+ dirs: [path.join(HOME, ".claude", "skills"), path.join(APPDATA, "claude", "skills")]
68
+ },
69
+ copilot: {
70
+ name: "GitHub Copilot",
71
+ format: "skill",
72
+ roots: [path.join(APPDATA, "github-copilot"), path.join(HOME, ".config", "github-copilot")],
73
+ dirs: [path.join(APPDATA, "github-copilot", "skills"), path.join(HOME, ".config", "github-copilot", "skills")]
74
+ },
75
+ cursor: {
76
+ name: "Cursor",
77
+ format: "rules-mdc",
78
+ roots: [path.join(HOME, ".cursor"), path.join(LOCALAPPDATA, "cursor")],
79
+ dirs: [path.join(HOME, ".cursor", "skills"), path.join(LOCALAPPDATA, "cursor", "skills")]
80
+ },
81
+ windsurf: {
82
+ name: "Windsurf (Codeium)",
83
+ format: "skill-lower",
84
+ roots: [path.join(HOME, ".codeium", "windsurf"), path.join(LOCALAPPDATA, "windsurf")],
85
+ dirs: [path.join(HOME, ".codeium", "windsurf", "skills"), path.join(LOCALAPPDATA, "windsurf", "skills")]
86
+ },
87
+ cody: {
88
+ name: "Cody (Sourcegraph)",
89
+ format: "skill",
90
+ roots: [path.join(HOME, ".cody"), path.join(APPDATA, "cody")],
91
+ dirs: [path.join(HOME, ".cody", "skills"), path.join(APPDATA, "cody", "skills")]
92
+ },
93
+ codium: {
94
+ name: "Codium",
95
+ format: "skill-lower",
96
+ roots: [path.join(HOME, ".codium"), path.join(LOCALAPPDATA, "codium")],
97
+ dirs: [path.join(HOME, ".codium", "skills"), path.join(LOCALAPPDATA, "codium", "skills")]
98
+ },
99
+ aider: {
100
+ name: "Aider",
101
+ format: "conventions",
102
+ roots: [path.join(HOME, ".aider")],
103
+ dirs: [path.join(HOME, ".aider", "skills")]
104
+ },
105
+ continue: {
106
+ name: "Continue",
107
+ format: "rules-md",
108
+ roots: [path.join(HOME, ".continue")],
109
+ dirs: [path.join(HOME, ".continue", "skills")]
110
+ },
111
+ codex: {
112
+ name: "Codex (OpenAI)",
113
+ format: "skill",
114
+ roots: [path.join(HOME, ".codex"), path.join(HOME, ".agents")],
115
+ dirs: [path.join(HOME, ".codex", "skills"), path.join(HOME, ".agents", "skills")]
116
+ },
117
+ roo: {
118
+ name: "Roo Code",
119
+ format: "skill",
120
+ roots: [path.join(HOME, ".roo"), path.join(HOME, ".agents")],
121
+ dirs: [path.join(HOME, ".roo", "skills"), path.join(HOME, ".agents", "skills")]
122
+ },
123
+ cline: {
124
+ name: "Cline",
125
+ format: "skill",
126
+ roots: [path.join(HOME, ".cline"), path.join(HOME, ".agents")],
127
+ dirs: [path.join(HOME, ".cline", "skills"), path.join(HOME, ".agents", "skills")]
128
+ },
129
+ augment: {
130
+ name: "Augment",
131
+ format: "rules-md",
132
+ roots: [path.join(HOME, ".augment")],
133
+ dirs: [path.join(HOME, ".augment", "skills")]
134
+ },
135
+ amazonq: {
136
+ name: "Amazon Q",
137
+ format: "rules-md",
138
+ roots: [path.join(HOME, ".amazonq"), path.join(HOME, ".aws", "amazonq")],
139
+ dirs: [path.join(HOME, ".amazonq", "skills"), path.join(HOME, ".aws", "amazonq", "skills")]
140
+ },
141
+ jetbrains: {
142
+ name: "JetBrains AI",
143
+ format: "skill",
144
+ roots: [path.join(HOME, ".junie")],
145
+ dirs: [path.join(HOME, ".junie", "skills")]
146
+ }
147
+ };
148
+
149
+ function readSource(skillDir) {
150
+ const skillMd = path.join(skillDir, "SKILL.md");
151
+ if (!fs.existsSync(skillMd)) {
152
+ throw new Error(`no SKILL.md at ${skillMd}`);
153
+ }
154
+ const raw = fs.readFileSync(skillMd, "utf8");
155
+ const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
156
+ const body = fm ? raw.slice(fm[0].length) : raw;
157
+ const descMatch = fm && fm[1].match(/^description:\s*(.+)$/m);
158
+ const description = descMatch ? descMatch[1].trim().replace(/^['"]|['"]$/g, "") : SKILL_NAME;
159
+ return { body, description };
160
+ }
161
+
162
+ function getFrontmatter(key, description) {
163
+ const d = description.replace(/'/g, "''");
164
+ const base = `name: ${SKILL_NAME}\ndescription: '${d}'\nlicense: Proprietary`;
165
+ switch (key) {
166
+ case "aider":
167
+ return ""; // Aider uses plain markdown, no frontmatter
168
+ case "opencode":
169
+ return `---\n${base}\ncompatibility: opencode\n---\n\n`;
170
+ case "cursor":
171
+ return `---\n${base}\nglobs: ["**/*.{html,css,js,ts,jsx,tsx,vue,svelte,astro,md}"]\nalwaysApply: true\n---\n\n`;
172
+ case "claude":
173
+ return `---\n${base}\nversion: 1.0.0\n---\n\n`;
174
+ case "copilot":
175
+ return `---\n${base}\napplyTo: "**/*"\n---\n\n`;
176
+ case "windsurf":
177
+ case "codium":
178
+ return `---\n${base}\ntrigger: always_on\n---\n\n`;
179
+ case "cody":
180
+ return `---\n${base}\n---\n\n`;
181
+ case "continue":
182
+ return `---\n${base}\nglobs: "**/*"\n---\n\n`;
183
+ case "codex":
184
+ return `---\n${base}\nmetadata:\n short-description: Build and sell high-converting websites\n---\n\n`;
185
+ case "roo":
186
+ case "cline":
187
+ case "jetbrains":
188
+ return `---\n${base}\nversion: 1.0.0\n---\n\n`;
189
+ case "augment":
190
+ case "amazonq":
191
+ return `---\n${base}\n---\n\n`;
192
+ default:
193
+ return `---\n${base}\n---\n\n`;
194
+ }
195
+ }
196
+
197
+ function pickDir(agent) {
198
+ for (const d of agent.dirs) {
199
+ if (fs.existsSync(d)) return d;
200
+ }
201
+ return agent.dirs[0];
202
+ }
203
+
204
+ // agentRoot: the agent's home (dir containing skills/ or rules/), derived from a skills dir
205
+ function agentRootOf(skillsDir) {
206
+ return path.basename(skillsDir) === "skills" ? path.dirname(skillsDir) : skillsDir;
207
+ }
208
+
209
+ function destFor(key, agent) {
210
+ if (flags.dir) {
211
+ const payloadDir = path.join(path.resolve(flags.dir), SKILL_NAME);
212
+ return { payloadDir, mainFile: path.join(payloadDir, "SKILL.md") };
213
+ }
214
+ const base = pickDir(agent);
215
+ const payloadDir = path.join(base, SKILL_NAME);
216
+ const root = agentRootOf(base);
217
+ switch (agent.format) {
218
+ case "skill":
219
+ return { payloadDir, mainFile: path.join(payloadDir, "SKILL.md") };
220
+ case "skill-lower":
221
+ return { payloadDir, mainFile: path.join(payloadDir, "skill.md") };
222
+ case "rules-mdc":
223
+ return { payloadDir, mainFile: path.join(root, "rules", `${SKILL_NAME}.mdc`) };
224
+ case "rules-md":
225
+ return { payloadDir, mainFile: path.join(root, "rules", `${SKILL_NAME}.md`) };
226
+ case "conventions":
227
+ return { payloadDir, mainFile: path.join(base, "CONVENTIONS.md") };
228
+ default:
229
+ return { payloadDir, mainFile: path.join(payloadDir, "SKILL.md") };
230
+ }
231
+ }
232
+
233
+ function copyRecursive(src, dest) {
234
+ const stat = fs.statSync(src);
235
+ if (stat.isDirectory()) {
236
+ fs.mkdirSync(dest, { recursive: true });
237
+ for (const entry of fs.readdirSync(src)) {
238
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
239
+ }
240
+ } else {
241
+ fs.copyFileSync(src, dest);
242
+ }
243
+ }
244
+
245
+ function installPayload(payloadDir, skillDir) {
246
+ copyRecursive(path.join(skillDir, "references"), path.join(payloadDir, "references"));
247
+ copyRecursive(path.join(skillDir, "templates"), path.join(payloadDir, "templates"));
248
+ if (fs.existsSync(path.join(skillDir, "departments"))) {
249
+ copyRecursive(path.join(skillDir, "departments"), path.join(payloadDir, "departments"));
250
+ }
251
+ }
252
+
253
+ /* ------------------------------ slash commands ------------------------------ */
254
+ // Installed globally for opencode (~/.config/opencode/command/) and Claude Code
255
+ // (~/.claude/commands/). Each full-name command file (agentic-web-developer-<slug>.md)
256
+ // also gets a short alias (awd-<slug>.md) derived from the filename.
257
+
258
+ const COMMANDS = {
259
+ opencode: {
260
+ name: "OpenCode",
261
+ // fallback order: last entry wins when none exists — prefer ~/.config/opencode/command
262
+ dirs: [
263
+ path.join(HOME, ".opencode", "command"),
264
+ path.join(HOME, ".config", "opencode", "command")
265
+ ]
266
+ },
267
+ claude: {
268
+ name: "Claude Code",
269
+ // Claude Code reads user commands from ~/.claude/commands
270
+ dirs: [path.join(HOME, ".claude", "commands")]
271
+ }
272
+ };
273
+
274
+ // commands/ lives next to the skill folder (repo root) or inside it.
275
+ function resolveCommandsSource(skillDir) {
276
+ const nested = path.join(skillDir, "commands");
277
+ if (fs.existsSync(nested)) return nested;
278
+ const sibling = path.join(path.dirname(skillDir), "commands");
279
+ if (fs.existsSync(sibling)) return sibling;
280
+ return null;
281
+ }
282
+
283
+ function pickCommandDir(cmd) {
284
+ for (const d of cmd.dirs) {
285
+ if (fs.existsSync(d)) return d;
286
+ }
287
+ return cmd.dirs[cmd.dirs.length - 1];
288
+ }
289
+
290
+ function commandDestPaths(key) {
291
+ if (flags.dir) {
292
+ return { dir: path.join(path.resolve(flags.dir), "commands") };
293
+ }
294
+ const cmd = COMMANDS[key];
295
+ if (!cmd) return null;
296
+ return { dir: pickCommandDir(cmd) };
297
+ }
298
+
299
+ // Writes every command file + derived awd-* alias. Returns written/skipped counts.
300
+ function installCommands(skillDir, key) {
301
+ const spec = commandDestPaths(key);
302
+ if (!spec) return null;
303
+ const src = resolveCommandsSource(skillDir);
304
+ if (!src) return null;
305
+
306
+ let written = 0;
307
+ let skipped = 0;
308
+ if (flags["dry-run"]) {
309
+ const names = fs.readdirSync(src).filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, ""));
310
+ const all = names.flatMap((n) =>
311
+ n.startsWith(`${SKILL_NAME}-`) ? [n, `awd-${n.slice(SKILL_NAME.length + 1)}`] : [n]
312
+ );
313
+ console.log(` cmds: ${spec.dir}{/${all.join(", /")}}`);
314
+ return { dir: spec.dir, written: 0, skipped: all.length, dryRun: true };
315
+ }
316
+
317
+ fs.mkdirSync(spec.dir, { recursive: true });
318
+ for (const entry of fs.readdirSync(src)) {
319
+ if (!entry.endsWith(".md")) continue;
320
+ const base = entry.replace(/\.md$/, "");
321
+ const targets = [entry];
322
+ if (base.startsWith(`${SKILL_NAME}-`)) {
323
+ targets.push(`awd-${base.slice(SKILL_NAME.length + 1)}.md`);
324
+ }
325
+ const body = fs.readFileSync(path.join(src, entry), "utf8");
326
+ for (const t of targets) {
327
+ const dest = path.join(spec.dir, t);
328
+ if (fs.existsSync(dest) && !flags.force) {
329
+ skipped++;
330
+ continue;
331
+ }
332
+ fs.writeFileSync(dest, body);
333
+ written++;
334
+ }
335
+ }
336
+ return { dir: spec.dir, written, skipped, dryRun: false };
337
+ }
338
+
339
+ /* ------------------------- license config (the gate) ----------------------- */
340
+
341
+ function licenseFlagsPresent() {
342
+ return Boolean(flags["license-key"] || flags["admin-email"] || flags["admin-secret"]);
343
+ }
344
+
345
+ function validateLicenseFlags() {
346
+ const key = flags["license-key"];
347
+ const adminEmail = flags["admin-email"];
348
+ const adminSecret = flags["admin-secret"];
349
+ if (key && (adminEmail || adminSecret)) {
350
+ throw new Error("--license-key cannot be combined with --admin-email/--admin-secret");
351
+ }
352
+ if ((adminEmail && !adminSecret) || (!adminEmail && adminSecret)) {
353
+ throw new Error("--admin-email and --admin-secret must be provided together");
354
+ }
355
+ }
356
+
357
+ // Writes animotion-config.json into the installed skill folder — the file the
358
+ // agentic-web-developer license gate reads (skill dir first, then its parent,
359
+ // ~/.animotion-skills/, AGENTIC_LICENSE_CONFIG).
360
+ function writeLicenseConfig(payloadDir) {
361
+ if (!licenseFlagsPresent()) return null;
362
+ const cfg = flags["admin-email"]
363
+ ? { tier: "pro", requiredPlan: "pro", authMethod: "admin", adminEmail: flags["admin-email"], adminSecret: flags["admin-secret"] }
364
+ : { tier: "pro", requiredPlan: "pro", authMethod: "license", licenseKey: flags["license-key"] };
365
+ const file = path.join(payloadDir, "animotion-config.json");
366
+ fs.mkdirSync(payloadDir, { recursive: true });
367
+ fs.writeFileSync(file, `${JSON.stringify(cfg, null, 2)}\n`);
368
+ try { fs.chmodSync(file, 0o600); } catch { /* best-effort; Windows ignores */ }
369
+ return file;
370
+ }
371
+
372
+ /* ---------------- skill source resolution (private fetch) ---------------- */
373
+
374
+ function expandHome(p) {
375
+ if (p === "~") return HOME;
376
+ if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(HOME, p.slice(2));
377
+ return p;
378
+ }
379
+
380
+ // Find the folder holding SKILL.md inside an extracted/downloaded tree.
381
+ // Accepts: the skill folder itself, a folder containing skill/, or an
382
+ // archive whose repo root is the skill (depth <= 3).
383
+ function findSkillRoot(root, depth) {
384
+ if (fs.existsSync(path.join(root, "SKILL.md"))) return root;
385
+ const nested = path.join(root, "skill");
386
+ if (fs.existsSync(path.join(nested, "SKILL.md"))) return nested;
387
+ if (depth >= 3) return null;
388
+ let entries;
389
+ try {
390
+ entries = fs.readdirSync(root, { withFileTypes: true });
391
+ } catch {
392
+ return null;
393
+ }
394
+ for (const e of entries) {
395
+ if (!e.isDirectory()) continue;
396
+ const found = findSkillRoot(path.join(root, e.name), depth + 1);
397
+ if (found) return found;
398
+ }
399
+ return null;
400
+ }
401
+
402
+ function extractArchive(file) {
403
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), "awd-src-"));
404
+ const args = /\.zip$/i.test(file) ? ["-xf", file, "-C", work] : ["-xzf", file, "-C", work];
405
+ try {
406
+ execFileSync("tar", args, { stdio: "pipe" });
407
+ } catch (e) {
408
+ throw new Error(
409
+ `could not extract ${path.basename(file)} with tar (tar must be on PATH; use a .tar.gz archive — raw: ${e.message})`
410
+ );
411
+ }
412
+ return work;
413
+ }
414
+
415
+ async function downloadTo(url, label) {
416
+ const headers = {
417
+ "User-Agent": "agentic-web-developer-installer",
418
+ Accept: "application/octet-stream, */*"
419
+ };
420
+ const isGithub = /github\.com|githubusercontent\.com/i.test(url);
421
+ const token = flags.token || process.env.AWD_TOKEN || (isGithub ? process.env.GITHUB_TOKEN : undefined);
422
+ if (token) headers.Authorization = `Bearer ${token}`;
423
+
424
+ let res;
425
+ try {
426
+ res = await fetch(url, { headers, redirect: "follow" });
427
+ } catch (e) {
428
+ throw new Error(`${label}: network error fetching skill source — ${e.message}`);
429
+ }
430
+ if (!res.ok) {
431
+ const hint =
432
+ res.status === 404 || res.status === 403 || res.status === 401
433
+ ? " (private source? add --token=<key> or AWD_TOKEN=<key>)"
434
+ : "";
435
+ throw new Error(`${label}: download failed — HTTP ${res.status} ${res.statusText}${hint}`);
436
+ }
437
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "awd-dl-"));
438
+ const name = (url.split("?")[0].split("/").pop() || "source").replace(/[^A-Za-z0-9._-]/g, "_");
439
+ const file = path.join(dir, name.includes(".") ? name : `${name}.tar.gz`);
440
+ fs.writeFileSync(file, Buffer.from(await res.arrayBuffer()));
441
+ return file;
442
+ }
443
+
444
+ async function resolveLocation(loc, label) {
445
+ if (/^https?:\/\//i.test(loc)) {
446
+ console.log(`skill source (${label}): ${loc}`);
447
+ const file = await downloadTo(loc, label);
448
+ const root = findSkillRoot(extractArchive(file), 0);
449
+ if (!root) throw new Error(`${label}: the downloaded archive contains no SKILL.md — does the source contain the skill/ folder?`);
450
+ return root;
451
+ }
452
+
453
+ const p = path.resolve(expandHome(loc));
454
+ if (!fs.existsSync(p)) return null;
455
+ if (fs.statSync(p).isDirectory()) {
456
+ const root = findSkillRoot(p, 0);
457
+ if (!root) throw new Error(`${label}: no SKILL.md found under ${p}`);
458
+ return root;
459
+ }
460
+ const root = findSkillRoot(extractArchive(p), 0);
461
+ if (!root) throw new Error(`${label}: the archive contains no SKILL.md`);
462
+ return root;
463
+ }
464
+
465
+ function readPkg() {
466
+ try {
467
+ // strip BOM — some editors/PowerShell writes prepend it; JSON.parse would throw
468
+ return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8").replace(/^\uFEFF/, ""));
469
+ } catch {
470
+ return null;
471
+ }
472
+ }
473
+
474
+ async function resolveSource() {
475
+ // 1. explicit flag
476
+ if (flags.source) {
477
+ const r = await resolveLocation(String(flags.source), "--source");
478
+ if (!r) throw new Error(`--source: no such file or directory: ${flags.source}`);
479
+ return r;
480
+ }
481
+ // 2. environment
482
+ if (process.env.AWD_SOURCE) {
483
+ const r = await resolveLocation(process.env.AWD_SOURCE, "AWD_SOURCE");
484
+ if (!r) throw new Error(`AWD_SOURCE: no such file or directory: ${process.env.AWD_SOURCE}`);
485
+ return r;
486
+ }
487
+ // 3. local development checkout (skill/ next to bin/)
488
+ if (fs.existsSync(path.join(BUNDLED_SKILL, "SKILL.md"))) return BUNDLED_SKILL;
489
+ // 4. source configured by the package author
490
+ const pkg = readPkg();
491
+ const pkgSource = pkg && pkg.awd && pkg.awd.source;
492
+ if (pkgSource) {
493
+ const r = await resolveLocation(String(pkgSource), "package.json awd.source");
494
+ if (r) return r;
495
+ throw new Error(`package.json awd.source could not be resolved: ${pkgSource}`);
496
+ }
497
+ throw new Error(
498
+ [
499
+ "no skill source found — this npm package ships only the installer (+ docs).",
500
+ "Provide the skill payload with one of:",
501
+ " --source=<url|path> a .tar.gz/.zip URL, or a local skill folder/archive",
502
+ " AWD_SOURCE=<url|path> same, via environment variable",
503
+ " awd.source in package.json set by the package author before publishing",
504
+ "Private sources also need a token: --token=<key> or AWD_TOKEN=<key>",
505
+ "Local development: keep the skill/ folder next to bin/ — it is used automatically."
506
+ ].join("\n")
507
+ );
508
+ }
509
+
510
+ /* -------------------------------- commands -------------------------------- */
511
+
512
+ function detect() {
513
+ console.log(`\nAgents on this machine (and ${SKILL_NAME} install status):\n`);
514
+ let found = 0;
515
+ for (const [key, agent] of Object.entries(AGENTS)) {
516
+ const present = agent.roots.some((r) => fs.existsSync(r));
517
+ const { mainFile, payloadDir } = destFor(key, agent);
518
+ let installed = fs.existsSync(mainFile);
519
+ // Aider's CONVENTIONS.md may belong to another skill — require our marker
520
+ if (installed && agent.format === "conventions") {
521
+ installed = fs.readFileSync(mainFile, "utf8").includes(MARKER);
522
+ }
523
+ if (installed && !fs.existsSync(payloadDir)) installed = false;
524
+ if (present) found++;
525
+ let status = installed ? "INSTALLED" : present ? "not installed" : "agent not found";
526
+ // slash commands (opencode + claude only)
527
+ let cmdNote = "";
528
+ if (COMMANDS[key]) {
529
+ const dir = pickCommandDir(COMMANDS[key]);
530
+ const mainCmd = path.join(dir, `${SKILL_NAME}.md`);
531
+ const aliasCmd = path.join(dir, "awd-strategy.md");
532
+ if (fs.existsSync(mainCmd) || fs.existsSync(aliasCmd)) {
533
+ cmdNote = ` cmds: ${dir}`;
534
+ } else if (installed) {
535
+ cmdNote = " cmds: not installed";
536
+ }
537
+ }
538
+ console.log(
539
+ ` ${present || installed ? "*" : " "} ${key.padEnd(10)} ${agent.name.padEnd(20)} ${status}` +
540
+ (installed ? ` -> ${mainFile}` : "") +
541
+ cmdNote
542
+ );
543
+ }
544
+ console.log(`\n Install: npx ${SKILL_NAME} install --agent=<key> (or --all, or omit --agent for auto)\n`);
545
+ return found;
546
+ }
547
+
548
+ async function install() {
549
+ validateLicenseFlags();
550
+ const skillDir = await resolveSource();
551
+ const { body, description } = readSource(skillDir);
552
+
553
+ let keys;
554
+ if (flags.agent === "all") {
555
+ keys = Object.keys(AGENTS);
556
+ } else if (flags.agent) {
557
+ if (!AGENTS[flags.agent]) {
558
+ throw new Error(`unknown agent "${flags.agent}". Valid keys: ${Object.keys(AGENTS).join(", ")}, all`);
559
+ }
560
+ keys = [flags.agent];
561
+ } else if (flags.all) {
562
+ keys = Object.keys(AGENTS);
563
+ } else {
564
+ // auto: opencode first, then claude, then any detected agent
565
+ const detected = Object.entries(AGENTS).filter(([, a]) => a.roots.some((r) => fs.existsSync(r)));
566
+ const preferred = ["opencode", "claude"].filter((k) => detected.some(([dk]) => dk === k));
567
+ keys = [preferred[0] || (detected[0] && detected[0][0]) || "opencode"];
568
+ }
569
+
570
+ let done = 0;
571
+ for (const key of keys) {
572
+ const agent = AGENTS[key];
573
+ const { payloadDir, mainFile } = destFor(key, agent);
574
+ const isRuleFile = ["rules-mdc", "rules-md", "conventions"].includes(agent.format);
575
+
576
+ if (flags["dry-run"]) {
577
+ console.log(`[dry-run] ${agent.name} (${key})`);
578
+ console.log(` main: ${mainFile}`);
579
+ console.log(` refs: ${payloadDir}{/references, /templates, /departments}`);
580
+ if (licenseFlagsPresent()) console.log(` lic: ${path.join(payloadDir, "animotion-config.json")}`);
581
+ if (!flags["no-commands"]) installCommands(skillDir, key);
582
+ done++;
583
+ continue;
584
+ }
585
+
586
+ if (fs.existsSync(mainFile) && !flags.force) {
587
+ if (agent.format === "conventions") {
588
+ const existing = fs.readFileSync(mainFile, "utf8");
589
+ if (!existing.includes(MARKER)) {
590
+ console.log(`skip ${agent.name}: ${mainFile} already exists and was not written by this installer (use --force to overwrite)`);
591
+ continue;
592
+ }
593
+ }
594
+ console.log(`skip ${agent.name}: already installed at ${mainFile} (use --force to overwrite)`);
595
+ const existingCfg = writeLicenseConfig(payloadDir);
596
+ if (existingCfg) console.log(` license config: ${existingCfg}`);
597
+ if (!flags["no-commands"]) {
598
+ const cmds = installCommands(skillDir, key);
599
+ if (cmds && !cmds.dryRun) {
600
+ console.log(` commands: ${cmds.written} written, ${cmds.skipped} kept -> ${cmds.dir}`);
601
+ }
602
+ }
603
+ continue;
604
+ }
605
+
606
+ fs.mkdirSync(payloadDir, { recursive: true });
607
+ installPayload(payloadDir, skillDir);
608
+
609
+ // Frontmatter per agent; rule files get a pointer to the reference payload
610
+ let content = getFrontmatter(key, description) + body;
611
+ if (isRuleFile) {
612
+ const pointer = `> Reference playbooks: ${path.join(payloadDir, "references")} — templates: ${path.join(payloadDir, "templates")}\n\n`;
613
+ const fm = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n\n/);
614
+ if (fm) {
615
+ content = fm[0] + pointer + content.slice(fm[0].length);
616
+ } else {
617
+ content = `${MARKER}\n${pointer}${content}`;
618
+ }
619
+ }
620
+ fs.mkdirSync(path.dirname(mainFile), { recursive: true });
621
+ fs.writeFileSync(mainFile, content);
622
+ const cfgFile = writeLicenseConfig(payloadDir);
623
+ console.log(`installed ${agent.name} (${key})`);
624
+ console.log(` main: ${mainFile}`);
625
+ console.log(` refs: ${payloadDir}`);
626
+ if (cfgFile) console.log(` license config: ${cfgFile}`);
627
+ if (!flags["no-commands"]) {
628
+ const cmds = installCommands(skillDir, key);
629
+ if (cmds && !cmds.dryRun) {
630
+ console.log(` commands: ${cmds.written} written, ${cmds.skipped} kept -> ${cmds.dir}`);
631
+ }
632
+ }
633
+ done++;
634
+ }
635
+
636
+ if (!done) {
637
+ console.log("nothing installed — everything already present (use --force to overwrite)");
638
+ } else if (!flags["dry-run"]) {
639
+ if (licenseFlagsPresent()) {
640
+ console.log("\nLicense saved — the skill's gate reads animotion-config.json automatically.");
641
+ }
642
+ console.log(`\nDone. Restart your AI agent so it loads ${SKILL_NAME}.`);
643
+ console.log("Animations for any site built with this skill are handled by the AnimotionJS agentic skill:");
644
+ console.log(" npx animotion-skills install --tier=agentic --agent=<agent> --license-key=YOUR_KEY");
645
+ }
646
+ }
647
+
648
+ switch (command) {
649
+ case "detect":
650
+ detect();
651
+ break;
652
+ case "install":
653
+ install().catch((err) => {
654
+ console.error(`error: ${err.message}`);
655
+ process.exit(1);
656
+ });
657
+ break;
658
+ case "help":
659
+ case "--help":
660
+ usage();
661
+ break;
662
+ default:
663
+ usage();
664
+ process.exit(1);
665
+ }
666
+
667
+ function usage() {
668
+ console.log(`
669
+ ${SKILL_NAME} — install the agentic-web-developer skill into your AI agent
670
+
671
+ Usage:
672
+ npx ${SKILL_NAME} install [options]
673
+ npx ${SKILL_NAME} detect
674
+
675
+ Options:
676
+ --agent=<key> Target one agent: ${Object.keys(AGENTS).join(", ")}
677
+ --agent=all Target every supported agent
678
+ --all Same as --agent=all
679
+ --source=<s> Skill source: a .tar.gz/.zip URL or a local folder/archive
680
+ (falls back to AWD_SOURCE, then skill/, then awd.source)
681
+ --token=<key> Auth token for a private source (or AWD_TOKEN=<key>)
682
+ --license-key=<key>
683
+ Save your Pro license key to the installed skill
684
+ (writes animotion-config.json the license gate reads)
685
+ --admin-email=<e> --admin-secret=<s>
686
+ Admin credentials instead of a key (Animotion staff /
687
+ whitelisted emails only — both flags required together)
688
+ --force Overwrite an existing install
689
+ --dry-run Print target paths without copying
690
+ --dir=<path> Install into <path>/${SKILL_NAME} (flat, SKILL.md layout)
691
+ --home=<path> Use <path> as the home directory (testing)
692
+ --no-commands Skip installing the slash commands (/agentic-web-developer-* + /awd-*)
693
+
694
+ Skill source order: --source > AWD_SOURCE > ./skill > package.json awd.source
695
+ The npm package ships only this installer (+ docs) — never the skill source.
696
+
697
+ Slash commands (opencode + claude, unless --no-commands):
698
+ /agentic-web-developer router (starts at Strategy)
699
+ /agentic-web-developer-<dept> full name per department
700
+ /awd-<dept> short alias (strategy, research, content,
701
+ design, frontend, backend, motion, qa,
702
+ deploy, launch, growth)
703
+ /awd-<dept> <stage> stage jump (e.g. /awd-design 4.3)
704
+ /awd-list print every department + stage; start anywhere
705
+ /awd-taste taste training (not a department —
706
+ builds/edits your taste profile any time)
707
+ -> opencode: ~/.config/opencode/command/ claude: ~/.claude/commands/
708
+
709
+ Each agent gets its own file format:
710
+ skill agents (opencode, claude, copilot, cody, codex, roo, cline, jetbrains)
711
+ -> <skills>/${SKILL_NAME}/SKILL.md
712
+ windsurf, codium -> <skills>/${SKILL_NAME}/skill.md
713
+ cursor -> ~/.cursor/rules/${SKILL_NAME}.mdc
714
+ continue -> ~/.continue/rules/${SKILL_NAME}.md
715
+ augment -> ~/.augment/rules/${SKILL_NAME}.md
716
+ amazonq -> ~/.amazonq/rules/${SKILL_NAME}.md
717
+ aider -> ~/.aider/skills/CONVENTIONS.md
718
+
719
+ Examples:
720
+ npx ${SKILL_NAME} install # auto-detect agent
721
+ npx ${SKILL_NAME} install --agent=opencode --license-key=sec_live_xxxxxxxx
722
+ npx ${SKILL_NAME} install --agent=all --source=https://you/server/skill.tar.gz --token=KEY
723
+ npx ${SKILL_NAME} detect
724
+ `);
725
+ }