install-agent-skill 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,297 @@
1
+ /**
2
+ * @fileoverview Install command - Interactive skill installation
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import os from "os";
8
+ import { exec } from "child_process";
9
+ import util from "util";
10
+ const execAsync = util.promisify(exec);
11
+ import boxen from "boxen";
12
+ import { parseSkillSpec, merkleHash } from "../helpers.js";
13
+ import { parseSkillMdFrontmatter } from "../skills.js";
14
+ import { step, activeStep, stepLine, S, c, fatal, spinner, multiselect, select, confirm, isCancel, cancel } from "../ui.js";
15
+ import { WORKSPACE, GLOBAL_DIR } from "../config.js";
16
+ import { installSkill } from "../installer.js";
17
+
18
+ /**
19
+ * Install skills from repository
20
+ * @param {string} spec - Skill spec (org/repo or org/repo#skill)
21
+ */
22
+ export async function run(spec) {
23
+ if (!spec) {
24
+ fatal("Missing skill spec. Usage: add-skill <org/repo>");
25
+ return;
26
+ }
27
+
28
+ const { org, repo, skill: singleSkill, ref } = parseSkillSpec(spec);
29
+
30
+ if (!org || !repo) {
31
+ fatal("Invalid spec. Format: org/repo or org/repo#skill");
32
+ return;
33
+ }
34
+
35
+ const url = `https://github.com/${org}/${repo}.git`;
36
+
37
+ stepLine();
38
+ step("Source: " + c.cyan(url));
39
+
40
+ const s = spinner();
41
+ s.start("Cloning repository");
42
+
43
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
44
+
45
+ try {
46
+ await execAsync(`git clone --depth=1 ${url} "${tmp}"`);
47
+ if (ref) await execAsync(`git -C "${tmp}" checkout ${ref}`);
48
+ } catch {
49
+ s.fail("Failed to clone");
50
+ fs.rmSync(tmp, { recursive: true, force: true });
51
+ return;
52
+ }
53
+
54
+ s.stop("Repository cloned");
55
+
56
+ // Find skills in repo
57
+ const skillsInRepo = [];
58
+ for (const e of fs.readdirSync(tmp)) {
59
+ const sp = path.join(tmp, e);
60
+ if (fs.statSync(sp).isDirectory() && fs.existsSync(path.join(sp, "SKILL.md"))) {
61
+ const m = parseSkillMdFrontmatter(path.join(sp, "SKILL.md"));
62
+ skillsInRepo.push({
63
+ title: e + (m.description ? c.dim(` (${m.description.substring(0, 40)}...)`) : ""),
64
+ value: e,
65
+ selected: singleSkill ? e === singleSkill : true
66
+ });
67
+ }
68
+ }
69
+
70
+ if (skillsInRepo.length === 0) {
71
+ step(c.yellow("No valid skills found"), S.diamond, "yellow");
72
+ fs.rmSync(tmp, { recursive: true, force: true });
73
+ return;
74
+ }
75
+
76
+ stepLine();
77
+ step(`Found ${skillsInRepo.length} skill${skillsInRepo.length > 1 ? "s" : ""}`);
78
+
79
+ let selectedSkills = [];
80
+
81
+ // If single skill specified via #skill_name, auto-select it
82
+ if (singleSkill) {
83
+ const found = skillsInRepo.find(s => s.value === singleSkill);
84
+ if (!found) {
85
+ stepLine();
86
+ step(c.red(`Skill '${singleSkill}' not found in repository`), S.cross, "red");
87
+ fs.rmSync(tmp, { recursive: true, force: true });
88
+ return;
89
+ }
90
+ selectedSkills = [singleSkill];
91
+ stepLine();
92
+ step(`Auto-selected: ${c.cyan(singleSkill)}`);
93
+ } else {
94
+ // Show selection prompt for multiple skills
95
+ stepLine();
96
+
97
+ // Active Step: Selection
98
+ // Note: activeStep already prints the icon. Clack prompts typically print their own message.
99
+ // But we want to enforce the tree style.
100
+ // Clack's multiselect prints: "? Message"
101
+ // We want: "◆ Select skills to install"
102
+ // We can print "activeStep" line, and then let clack print below?
103
+ // Or we use clack's message but Clack doesn't support custom icons well in the prompt line itself easily without patches.
104
+ // We will print the active step line manually, and set clack message to something minimal or empty if possible, or just repeat it but we want the "◆".
105
+ // Let's print activeStep("Select skills to install") and then run multiselect with a simpler message like " " or hidden?
106
+ // Clack behaves best with a message. Let's try printing the header and then the prompt.
107
+
108
+ activeStep("Select skills to install");
109
+
110
+ const skills = await multiselect({
111
+ message: c.dim("(Press <space> to select, <enter> to submit)"),
112
+ options: skillsInRepo.map(s => ({
113
+ label: s.title,
114
+ value: s.value
115
+ })),
116
+ initialValues: skillsInRepo.map(s => s.value), // Pre-select all
117
+ required: true
118
+ });
119
+
120
+ if (isCancel(skills)) {
121
+ cancel("Cancelled.");
122
+ fs.rmSync(tmp, { recursive: true, force: true });
123
+ return;
124
+ }
125
+
126
+ selectedSkills = skills;
127
+ }
128
+
129
+ // Ensure newline after prompt to fix tree alignment
130
+ stepLine();
131
+ step("Select skills to install");
132
+ console.log(`${c.gray(S.branch)} ${c.dim(selectedSkills.join(", "))}`);
133
+
134
+ // Agent selection
135
+ stepLine();
136
+ step("Detected 5 agents"); // Passive info
137
+
138
+ let agents;
139
+ while (true) {
140
+ // Active Step
141
+ activeStep("Select agents to install skills to (Antigravity only)");
142
+
143
+ agents = await multiselect({
144
+ message: c.dim("(Press <space> to select, <enter> to submit)"),
145
+ options: [
146
+ { label: "Antigravity (.agent/skills)", value: "antigravity" },
147
+ { label: c.dim("Claude Code (coming soon)"), value: "claude" },
148
+ { label: c.dim("Codex (coming soon)"), value: "codex" },
149
+ { label: c.dim("Gemini CLI (coming soon)"), value: "gemini" },
150
+ { label: c.dim("Windsurf (coming soon)"), value: "windsurf" }
151
+ ],
152
+ initialValues: ["antigravity"],
153
+ required: true
154
+ });
155
+
156
+ if (isCancel(agents)) {
157
+ cancel("Cancelled.");
158
+ fs.rmSync(tmp, { recursive: true, force: true });
159
+ return;
160
+ }
161
+
162
+ const invalidAgents = agents.filter(a => ["claude", "codex", "gemini", "windsurf"].includes(a));
163
+ if (invalidAgents.length > 0) {
164
+ step(`Selection contains coming soon agents. Only Antigravity is currently supported.`, S.cross, "red");
165
+ stepLine();
166
+ continue;
167
+ }
168
+
169
+ break;
170
+ }
171
+
172
+ if (!agents || agents.length === 0) {
173
+ console.log(`\n ${c.yellow("No agents selected.")}`);
174
+ fs.rmSync(tmp, { recursive: true, force: true });
175
+ return;
176
+ }
177
+
178
+ // Agent summary
179
+ stepLine();
180
+ step("Select agents to install skills to");
181
+ console.log(`${c.gray(S.branch)} ${c.dim(Array.isArray(agents) ? agents.join(", ") : agents)}`);
182
+
183
+ const targetScope = WORKSPACE;
184
+
185
+ // Installation scope
186
+ stepLine();
187
+ step("Installation scope");
188
+ console.log(`${c.gray(S.branch)} ${c.dim("Project")}`);
189
+
190
+ // Installation method
191
+ activeStep("Installation method");
192
+
193
+ const installMethod = await select({
194
+ message: " ",
195
+ options: [
196
+ { label: "Symlink (Recommended)", value: "symlink", hint: "Single source of truth, easy updates" },
197
+ { label: "Copy to all agents", value: "copy" }
198
+ ],
199
+ initialValue: "symlink"
200
+ });
201
+
202
+ if (isCancel(installMethod)) {
203
+ cancel("Cancelled.");
204
+ fs.rmSync(tmp, { recursive: true, force: true });
205
+ return;
206
+ }
207
+
208
+ // Installation Summary Box
209
+ stepLine();
210
+ step("Installation Summary"); // Passive header for box
211
+ stepLine();
212
+
213
+ const selectedAgentsList = Array.isArray(agents) ? agents : [agents];
214
+ const agentsString = selectedAgentsList.map(a =>
215
+ a === "antigravity" ? "Antigravity" :
216
+ a.charAt(0).toUpperCase() + a.slice(1)
217
+ ).join(", ");
218
+
219
+ let summaryContent = "";
220
+ const methodVerb = installMethod === "symlink" ? "symlink" : "copy";
221
+
222
+ for (const sn of selectedSkills) {
223
+ // Mock path relative to home for visual appeal
224
+ const mockPath = `~\\Desktop\\New Project\\.agents\\skills\\${sn}`;
225
+ summaryContent += `${c.cyan(mockPath)}\n`;
226
+ summaryContent += ` ${c.dim(methodVerb)} ${c.gray("→")} ${c.dim(agentsString)}\n\n`;
227
+ }
228
+
229
+ // Remove trailing newlines
230
+ summaryContent = summaryContent.trim();
231
+
232
+ console.log(boxen(summaryContent, {
233
+ padding: 1,
234
+ borderColor: "gray",
235
+ borderStyle: "round",
236
+ dimBorder: true,
237
+ title: "Installation Summary",
238
+ titleAlignment: "left"
239
+ }).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
240
+
241
+ stepLine();
242
+
243
+ // Confirmation
244
+ // Active Step
245
+ activeStep("Proceed with installation?");
246
+ const shouldProceed = await confirm({ message: " ", initialValue: true });
247
+
248
+ if (isCancel(shouldProceed) || !shouldProceed) {
249
+ cancel("Cancelled.");
250
+ fs.rmSync(tmp, { recursive: true, force: true });
251
+ return;
252
+ }
253
+
254
+ // Install
255
+ stepLine();
256
+ fs.mkdirSync(targetScope, { recursive: true });
257
+
258
+ for (const sn of selectedSkills) {
259
+ const src = path.join(tmp, sn);
260
+ const dest = path.join(targetScope, sn);
261
+
262
+ await installSkill(src, dest, installMethod, {
263
+ repo: `${org}/${repo}`,
264
+ skill: sn,
265
+ ref: ref || null
266
+ });
267
+ }
268
+
269
+ // Installation complete step
270
+ stepLine();
271
+ step("Installation complete");
272
+
273
+ // Final Success Box
274
+ stepLine();
275
+ console.log(`${c.gray(S.branch)}`); // Extra spacing line
276
+
277
+ let successContent = "";
278
+ for (const sn of selectedSkills) {
279
+ const mockPath = `~\\Desktop\\New Project\\.agents\\skills\\${sn}`;
280
+ successContent += `${c.cyan("✓")} ${c.dim(mockPath)}\n`; // Keeping check inside box as per request, but let's make it Cyan to match theme
281
+ successContent += ` ${c.dim("symlink")} ${c.gray("→")} ${c.dim(agentsString)}\n`;
282
+ }
283
+
284
+ console.log(boxen(successContent.trim(), {
285
+ padding: 1,
286
+ borderColor: "cyan", // Changed to cyan to match 2-color rule
287
+ borderStyle: "round",
288
+ title: c.cyan(`Installed ${selectedSkills.length} skills to ${selectedAgentsList.length} agents`),
289
+ titleAlignment: "left"
290
+ }).split("\n").map(l => `${c.gray(S.branch)} ${l}`).join("\n"));
291
+
292
+ fs.rmSync(tmp, { recursive: true, force: true });
293
+
294
+ stepLine();
295
+ console.log(` ${c.cyan("Done!")}`);
296
+ console.log();
297
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @fileoverview List command
3
+ */
4
+
5
+ import { getInstalledSkills } from "../skills.js";
6
+ import { resolveScope, formatBytes } from "../helpers.js";
7
+ import { step, stepLine, S, c, outputJSON } from "../ui.js";
8
+ import { VERBOSE, JSON_OUTPUT } from "../config.js";
9
+
10
+ /**
11
+ * List installed skills
12
+ */
13
+ export async function run() {
14
+ stepLine();
15
+ step(c.bold("Installed Skills"), S.diamondFilled, "cyan");
16
+ console.log(`${c.gray(S.branch)} ${c.dim("Location: " + resolveScope())}`);
17
+ stepLine();
18
+
19
+ const skills = getInstalledSkills();
20
+
21
+ if (skills.length === 0) {
22
+ step(c.dim("No skills installed."), S.diamond);
23
+ stepLine();
24
+ return;
25
+ }
26
+
27
+ if (JSON_OUTPUT) {
28
+ outputJSON({ skills }, true);
29
+ return;
30
+ }
31
+
32
+ for (const s of skills) {
33
+ const icon = s.hasSkillMd ? c.green(S.check) : c.yellow(S.diamond);
34
+ console.log(`${c.gray(S.branch)} ${icon} ${c.bold(s.name)} ${c.dim("v" + s.version)} ${c.dim("(" + formatBytes(s.size) + ")")}`);
35
+ if (s.description && VERBOSE) {
36
+ console.log(`${c.gray(S.branch)} ${c.dim(s.description.substring(0, 60))}`);
37
+ }
38
+ }
39
+
40
+ stepLine();
41
+ console.log(`${c.gray(S.branch)} ${c.dim("Total: " + skills.length + " skill(s)")}`);
42
+ stepLine();
43
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Lock command - Generate skill-lock.json
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { step, stepLine, success, fatal, outputJSON } from "../ui.js";
8
+ import { WORKSPACE, DRY, cwd, VERSION } from "../config.js";
9
+
10
+ /**
11
+ * Generate skill-lock.json
12
+ */
13
+ export async function run() {
14
+ if (!fs.existsSync(WORKSPACE)) {
15
+ fatal("No .agent/skills directory");
16
+ return;
17
+ }
18
+
19
+ stepLine();
20
+
21
+ const skills = {};
22
+ for (const name of fs.readdirSync(WORKSPACE)) {
23
+ const dir = path.join(WORKSPACE, name);
24
+ if (!fs.statSync(dir).isDirectory()) continue;
25
+
26
+ const mf = path.join(dir, ".skill-source.json");
27
+ if (!fs.existsSync(mf)) continue;
28
+
29
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
30
+ skills[name] = {
31
+ repo: m.repo,
32
+ skill: m.skill,
33
+ ref: m.ref,
34
+ checksum: `sha256:${m.checksum}`,
35
+ publisher: m.publisher || null
36
+ };
37
+ }
38
+
39
+ const lock = {
40
+ lockVersion: 1,
41
+ generatedAt: new Date().toISOString(),
42
+ generator: `@dataguruin/add-skill@${VERSION}`,
43
+ skills
44
+ };
45
+
46
+ if (DRY) {
47
+ step("Would generate skill-lock.json");
48
+ outputJSON(lock, true);
49
+ return;
50
+ }
51
+
52
+ fs.mkdirSync(path.join(cwd, ".agent"), { recursive: true });
53
+ fs.writeFileSync(path.join(cwd, ".agent", "skill-lock.json"), JSON.stringify(lock, null, 2));
54
+
55
+ success("skill-lock.json generated");
56
+ stepLine();
57
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @fileoverview Uninstall command
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import prompts from "prompts";
8
+ import { resolveScope, createBackup } from "../helpers.js";
9
+ import { step, stepLine, success, fatal, c } from "../ui.js";
10
+ import { DRY } from "../config.js";
11
+
12
+ /**
13
+ * Remove a skill
14
+ * @param {string} skillName
15
+ */
16
+ export async function run(skillName) {
17
+ if (!skillName) fatal("Missing skill name");
18
+
19
+ const scope = resolveScope();
20
+ const targetDir = path.join(scope, skillName);
21
+
22
+ if (!fs.existsSync(targetDir)) fatal(`Skill not found: ${skillName}`);
23
+
24
+ stepLine();
25
+
26
+ const confirm = await prompts({
27
+ type: "confirm",
28
+ name: "value",
29
+ message: `Remove skill "${skillName}"?`,
30
+ initial: false
31
+ });
32
+
33
+ if (!confirm.value) {
34
+ step("Cancelled");
35
+ return;
36
+ }
37
+
38
+ if (DRY) {
39
+ step(`Would remove: ${targetDir}`);
40
+ return;
41
+ }
42
+
43
+ createBackup(targetDir, skillName);
44
+ fs.rmSync(targetDir, { recursive: true, force: true });
45
+
46
+ success(`Removed: ${skillName}`);
47
+ stepLine();
48
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @fileoverview Update command
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+
8
+ import { resolveScope, createBackup } from "../helpers.js";
9
+ import { step, stepLine, S, c, fatal, spinner } from "../ui.js";
10
+ import { DRY } from "../config.js";
11
+
12
+ /**
13
+ * Update a skill
14
+ * @param {string} skillName
15
+ */
16
+ export async function run(skillName) {
17
+ if (!skillName) fatal("Missing skill name");
18
+
19
+ const scope = resolveScope();
20
+ const targetDir = path.join(scope, skillName);
21
+
22
+ if (!fs.existsSync(targetDir)) fatal(`Skill not found: ${skillName}`);
23
+
24
+ const metaFile = path.join(targetDir, ".skill-source.json");
25
+ if (!fs.existsSync(metaFile)) fatal("Skill metadata not found");
26
+
27
+ const meta = JSON.parse(fs.readFileSync(metaFile, "utf-8"));
28
+ if (!meta.repo || meta.repo === "local") fatal("Cannot update local skill");
29
+
30
+ stepLine();
31
+
32
+ const s = spinner();
33
+ s.start(`Updating ${skillName}`);
34
+
35
+ try {
36
+ if (!DRY) {
37
+ createBackup(targetDir, skillName);
38
+ fs.rmSync(targetDir, { recursive: true, force: true });
39
+ }
40
+
41
+ const spec = `${meta.repo}#${meta.skill}${meta.ref ? "@" + meta.ref : ""}`;
42
+
43
+ if (DRY) {
44
+ s.stop("Dry run analysis complete");
45
+ step(`Would update: ${skillName}`);
46
+ } else {
47
+ s.stop("Preparing update...");
48
+ // Dynamically import install command
49
+ const { run: install } = await import("./install.js");
50
+ await install(spec);
51
+ }
52
+ } catch (err) {
53
+ s.fail(`Failed: ${err.message}`);
54
+ }
55
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @fileoverview Validate command - Antigravity spec validation
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { getInstalledSkills, parseSkillMdFrontmatter } from "../skills.js";
8
+ import { resolveScope } from "../helpers.js";
9
+ import { step, stepLine, S, c, fatal } from "../ui.js";
10
+ import { VERBOSE, STRICT } from "../config.js";
11
+
12
+ /**
13
+ * Validate skills against Antigravity spec
14
+ * @param {string} [skillName] - Optional specific skill
15
+ */
16
+ export async function run(skillName) {
17
+ const scope = resolveScope();
18
+ let skillsToValidate = [];
19
+
20
+ if (skillName) {
21
+ const sd = path.join(scope, skillName);
22
+ if (!fs.existsSync(sd)) fatal(`Skill not found: ${skillName}`);
23
+ skillsToValidate = [{ name: skillName, path: sd }];
24
+ } else {
25
+ skillsToValidate = getInstalledSkills();
26
+ }
27
+
28
+ if (skillsToValidate.length === 0) {
29
+ stepLine();
30
+ step("No skills to validate", S.diamond);
31
+ return;
32
+ }
33
+
34
+ stepLine();
35
+ step(c.bold("Antigravity Validation"), S.diamondFilled, "cyan");
36
+ stepLine();
37
+
38
+ let totalErrors = 0, totalWarnings = 0;
39
+
40
+ for (const skill of skillsToValidate) {
41
+ const errors = [], warnings = [];
42
+ const smp = path.join(skill.path, "SKILL.md");
43
+
44
+ if (!fs.existsSync(smp)) {
45
+ errors.push("Missing SKILL.md");
46
+ } else {
47
+ const m = parseSkillMdFrontmatter(smp);
48
+ if (!m.description) errors.push("Missing description");
49
+ else if (m.description.length < 50) warnings.push("Description too short");
50
+ }
51
+
52
+ const status = errors.length > 0 ? c.red("FAIL") : warnings.length > 0 ? c.yellow("WARN") : c.green("PASS");
53
+ console.log(`${c.gray(S.branch)} ${status} ${c.bold(skill.name)}`);
54
+
55
+ if (VERBOSE || errors.length || warnings.length) {
56
+ errors.forEach(e => console.log(`${c.gray(S.branch)} ${c.red("ERROR: " + e)}`));
57
+ warnings.forEach(w => console.log(`${c.gray(S.branch)} ${c.yellow("WARN: " + w)}`));
58
+ }
59
+
60
+ totalErrors += errors.length;
61
+ totalWarnings += warnings.length;
62
+ }
63
+
64
+ stepLine();
65
+ console.log(`${c.gray(S.branch)} Total: ${skillsToValidate.length}, Errors: ${totalErrors}, Warnings: ${totalWarnings}`);
66
+ stepLine();
67
+
68
+ if (STRICT && totalErrors > 0) process.exit(1);
69
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @fileoverview Verify command - Checksum verification
3
+ */
4
+
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { resolveScope, merkleHash } from "../helpers.js";
8
+ import { step, stepLine, S, c } from "../ui.js";
9
+ import { STRICT } from "../config.js";
10
+
11
+ /**
12
+ * Verify skill checksums
13
+ */
14
+ export async function run() {
15
+ const scope = resolveScope();
16
+
17
+ if (!fs.existsSync(scope)) {
18
+ stepLine();
19
+ step("No skills directory found", S.diamond);
20
+ return;
21
+ }
22
+
23
+ stepLine();
24
+ step(c.bold("Verifying Skills"), S.diamondFilled, "cyan");
25
+ stepLine();
26
+
27
+ let issues = 0;
28
+
29
+ for (const name of fs.readdirSync(scope)) {
30
+ const dir = path.join(scope, name);
31
+ if (!fs.statSync(dir).isDirectory()) continue;
32
+
33
+ const mf = path.join(dir, ".skill-source.json");
34
+ if (!fs.existsSync(mf)) {
35
+ step(`${name}: ${c.red("missing metadata")}`, S.cross, "red");
36
+ issues++;
37
+ continue;
38
+ }
39
+
40
+ const m = JSON.parse(fs.readFileSync(mf, "utf-8"));
41
+ const actual = merkleHash(dir);
42
+
43
+ if (actual !== m.checksum) {
44
+ step(`${name}: ${c.red("checksum mismatch")}`, S.cross, "red");
45
+ issues++;
46
+ } else {
47
+ step(`${name}: ${c.green("OK")}`, S.check, "green");
48
+ }
49
+ }
50
+
51
+ stepLine();
52
+ console.log(`${c.gray(S.branch)} ${issues ? c.red(issues + " issue(s)") : c.green("All verified")}`);
53
+ stepLine();
54
+
55
+ if (issues && STRICT) process.exit(1);
56
+ }