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.
- package/bin/install.js +725 -724
- 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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
"
|
|
500
|
-
"
|
|
501
|
-
"
|
|
502
|
-
"
|
|
503
|
-
"
|
|
504
|
-
"
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
const
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
console.log(`
|
|
578
|
-
console.log(`
|
|
579
|
-
|
|
580
|
-
if (
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
fs.
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
console.log(`
|
|
624
|
-
console.log(`
|
|
625
|
-
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
console.log(
|
|
643
|
-
console.log("
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
case "
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
npx ${SKILL_NAME}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
--agent
|
|
677
|
-
--all
|
|
678
|
-
--
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
--
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
--
|
|
689
|
-
--
|
|
690
|
-
--
|
|
691
|
-
--
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
/agentic-web-developer
|
|
699
|
-
/
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
/awd
|
|
704
|
-
/awd-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
npx ${SKILL_NAME} install
|
|
721
|
-
npx ${SKILL_NAME} install --agent=
|
|
722
|
-
npx ${SKILL_NAME}
|
|
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
|
+
}
|