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.
- package/README.bigtech.md +191 -0
- package/README.md +164 -0
- package/bin/add-skill.js +5 -0
- package/bin/add-skill.modular.js +5 -0
- package/bin/add-skill.v1.backup.js +1904 -0
- package/bin/cli.js +100 -0
- package/bin/lib/commands/analyze.js +70 -0
- package/bin/lib/commands/cache.js +65 -0
- package/bin/lib/commands/doctor.js +75 -0
- package/bin/lib/commands/help.js +42 -0
- package/bin/lib/commands/info.js +38 -0
- package/bin/lib/commands/init.js +39 -0
- package/bin/lib/commands/install.js +297 -0
- package/bin/lib/commands/list.js +43 -0
- package/bin/lib/commands/lock.js +57 -0
- package/bin/lib/commands/uninstall.js +48 -0
- package/bin/lib/commands/update.js +55 -0
- package/bin/lib/commands/validate.js +69 -0
- package/bin/lib/commands/verify.js +56 -0
- package/bin/lib/config.js +80 -0
- package/bin/lib/helpers.js +155 -0
- package/bin/lib/installer.js +49 -0
- package/bin/lib/skills.js +114 -0
- package/bin/lib/types.js +82 -0
- package/bin/lib/ui.js +132 -0
- package/package.json +56 -0
- package/specs/ADD_SKILL_SPEC.md +333 -0
- package/specs/REGISTRY_V2_SPEC.md +334 -0
|
@@ -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
|
+
}
|