skai 0.0.4 → 0.0.6
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/dist/index.js +460 -69
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -446,7 +446,7 @@ function skillTreeToTreeNodes(node, parentPath = "") {
|
|
|
446
446
|
result.push({
|
|
447
447
|
id,
|
|
448
448
|
label: s.name,
|
|
449
|
-
hint: s.skill.description,
|
|
449
|
+
hint: extractShortSummary(s.skill.description),
|
|
450
450
|
skill: s.skill,
|
|
451
451
|
selected: false
|
|
452
452
|
});
|
|
@@ -475,6 +475,28 @@ function matchesSkillFilter(skill, filter) {
|
|
|
475
475
|
}
|
|
476
476
|
return skill.name.toLowerCase() === filterLower;
|
|
477
477
|
}
|
|
478
|
+
function extractShortSummary(description, maxLength = 50) {
|
|
479
|
+
if (!description) return "";
|
|
480
|
+
const triggers = [
|
|
481
|
+
"This skill should be used",
|
|
482
|
+
"Triggers on tasks",
|
|
483
|
+
"Use this skill when",
|
|
484
|
+
"Apply when"
|
|
485
|
+
];
|
|
486
|
+
let summary = description;
|
|
487
|
+
for (const trigger of triggers) {
|
|
488
|
+
const index = summary.indexOf(trigger);
|
|
489
|
+
if (index > 0) {
|
|
490
|
+
summary = summary.slice(0, index).trim();
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
summary = summary.replace(/[.,;:]+$/, "").trim();
|
|
495
|
+
if (summary.length > maxLength) {
|
|
496
|
+
summary = summary.slice(0, maxLength - 1).trim() + "\u2026";
|
|
497
|
+
}
|
|
498
|
+
return summary;
|
|
499
|
+
}
|
|
478
500
|
|
|
479
501
|
// src/agents.ts
|
|
480
502
|
import * as fs4 from "fs";
|
|
@@ -651,6 +673,73 @@ function installSkillForAgent(skill, agent, options) {
|
|
|
651
673
|
};
|
|
652
674
|
}
|
|
653
675
|
}
|
|
676
|
+
function uninstallSkill(skillName, agent, options) {
|
|
677
|
+
const basePath = options.global ? agent.globalPath : path5.join(process.cwd(), agent.projectPath);
|
|
678
|
+
const sanitizedName = sanitizeName(skillName);
|
|
679
|
+
const targetPath = path5.join(basePath, sanitizedName);
|
|
680
|
+
if (!isPathSafe(targetPath, basePath)) {
|
|
681
|
+
return {
|
|
682
|
+
skillName,
|
|
683
|
+
agent,
|
|
684
|
+
success: false,
|
|
685
|
+
targetPath,
|
|
686
|
+
error: `Invalid skill name: ${skillName}`
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
if (fs5.existsSync(targetPath)) {
|
|
690
|
+
try {
|
|
691
|
+
fs5.rmSync(targetPath, { recursive: true, force: true });
|
|
692
|
+
return { skillName, agent, success: true, targetPath };
|
|
693
|
+
} catch (error) {
|
|
694
|
+
return {
|
|
695
|
+
skillName,
|
|
696
|
+
agent,
|
|
697
|
+
success: false,
|
|
698
|
+
targetPath,
|
|
699
|
+
error: error instanceof Error ? error.message : String(error)
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
skillName,
|
|
705
|
+
agent,
|
|
706
|
+
success: false,
|
|
707
|
+
targetPath,
|
|
708
|
+
error: "Skill not found"
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
function listInstalledSkills(agent, options = {}) {
|
|
712
|
+
const skills = [];
|
|
713
|
+
const checkPath = (basePath, scope) => {
|
|
714
|
+
if (!fs5.existsSync(basePath)) return;
|
|
715
|
+
try {
|
|
716
|
+
const entries = fs5.readdirSync(basePath, { withFileTypes: true });
|
|
717
|
+
for (const entry of entries) {
|
|
718
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
719
|
+
skills.push({
|
|
720
|
+
name: entry.name,
|
|
721
|
+
path: path5.join(basePath, entry.name),
|
|
722
|
+
agent,
|
|
723
|
+
scope
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} catch {
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
if (!options.global) {
|
|
731
|
+
const projectPath = path5.join(process.cwd(), agent.projectPath);
|
|
732
|
+
checkPath(projectPath, "project");
|
|
733
|
+
}
|
|
734
|
+
if (!options.projectOnly) {
|
|
735
|
+
checkPath(agent.globalPath, "global");
|
|
736
|
+
}
|
|
737
|
+
return skills;
|
|
738
|
+
}
|
|
739
|
+
function getSkillInstallPath(skillName, agent, options) {
|
|
740
|
+
const basePath = options.global ? agent.globalPath : path5.join(process.cwd(), agent.projectPath);
|
|
741
|
+
return path5.join(basePath, sanitizeName(skillName));
|
|
742
|
+
}
|
|
654
743
|
|
|
655
744
|
// src/tree-select.ts
|
|
656
745
|
import * as p from "@clack/prompts";
|
|
@@ -976,34 +1065,7 @@ var S_BAR_END = import_picocolors.default.gray("\u2514");
|
|
|
976
1065
|
var S_CHECKBOX_ACTIVE = import_picocolors.default.cyan("\u25FB");
|
|
977
1066
|
var S_CHECKBOX_SELECTED = import_picocolors.default.green("\u25FC");
|
|
978
1067
|
var S_CHECKBOX_INACTIVE = import_picocolors.default.dim("\u25FB");
|
|
979
|
-
var
|
|
980
|
-
function truncateHint(hint, maxLen) {
|
|
981
|
-
if (!hint) return "";
|
|
982
|
-
if (hint.length <= maxLen) return hint;
|
|
983
|
-
return hint.slice(0, maxLen - 1) + "\u2026";
|
|
984
|
-
}
|
|
985
|
-
function wrapText(text, maxWidth) {
|
|
986
|
-
if (!text) return [];
|
|
987
|
-
const words = text.split(/\s+/);
|
|
988
|
-
const lines = [];
|
|
989
|
-
let currentLine = "";
|
|
990
|
-
for (const word of words) {
|
|
991
|
-
if (currentLine.length === 0) {
|
|
992
|
-
currentLine = word;
|
|
993
|
-
} else if (currentLine.length + 1 + word.length <= maxWidth) {
|
|
994
|
-
currentLine += " " + word;
|
|
995
|
-
} else {
|
|
996
|
-
lines.push(currentLine);
|
|
997
|
-
currentLine = word;
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
if (currentLine) {
|
|
1001
|
-
lines.push(currentLine);
|
|
1002
|
-
}
|
|
1003
|
-
return lines;
|
|
1004
|
-
}
|
|
1005
|
-
var DESCRIPTION_INDENT = " ";
|
|
1006
|
-
var DESCRIPTION_MAX_WIDTH = 60;
|
|
1068
|
+
var MAX_LABEL_WIDTH = 30;
|
|
1007
1069
|
function buildSearchableOptions(options) {
|
|
1008
1070
|
return options.map((opt) => ({
|
|
1009
1071
|
option: opt,
|
|
@@ -1191,21 +1253,11 @@ var SearchableMultiSelectPrompt = class extends x {
|
|
|
1191
1253
|
} else {
|
|
1192
1254
|
checkbox = S_CHECKBOX_INACTIVE;
|
|
1193
1255
|
}
|
|
1194
|
-
const
|
|
1195
|
-
const
|
|
1196
|
-
const
|
|
1197
|
-
const
|
|
1198
|
-
const hint = displayHint ? import_picocolors.default.dim(` (${displayHint})`) : "";
|
|
1199
|
-
const line = isActive ? `${checkbox} ${label}${hint}` : `${checkbox} ${import_picocolors.default.dim(opt.option.label)}${import_picocolors.default.dim(hint)}`;
|
|
1256
|
+
const hint = opt.option.hint || "";
|
|
1257
|
+
const paddedLabel = opt.option.label.padEnd(MAX_LABEL_WIDTH);
|
|
1258
|
+
const highlightedPaddedLabel = this.searchTerm ? highlightMatch(paddedLabel, this.searchTerm) : paddedLabel;
|
|
1259
|
+
const line = isActive ? `${checkbox} ${highlightedPaddedLabel} ${import_picocolors.default.dim(hint)}` : `${checkbox} ${import_picocolors.default.dim(paddedLabel)} ${import_picocolors.default.dim(hint)}`;
|
|
1200
1260
|
lines.push(`${import_picocolors.default.cyan(S_BAR)} ${line}`);
|
|
1201
|
-
if (isActive && needsTruncation) {
|
|
1202
|
-
const wrappedLines = wrapText(fullHint, DESCRIPTION_MAX_WIDTH);
|
|
1203
|
-
for (const descLine of wrappedLines) {
|
|
1204
|
-
lines.push(
|
|
1205
|
-
`${import_picocolors.default.cyan(S_BAR)} ${DESCRIPTION_INDENT}${import_picocolors.default.dim(descLine)}`
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
1261
|
}
|
|
1210
1262
|
if (belowCount > 0) {
|
|
1211
1263
|
lines.push(
|
|
@@ -1454,24 +1506,13 @@ var SearchableGroupMultiSelectPrompt = class extends x {
|
|
|
1454
1506
|
} else {
|
|
1455
1507
|
checkbox = S_CHECKBOX_INACTIVE;
|
|
1456
1508
|
}
|
|
1457
|
-
const
|
|
1458
|
-
const fullHint = item.option.option.hint || "";
|
|
1459
|
-
const needsTruncation = fullHint.length > MAX_HINT_LENGTH;
|
|
1460
|
-
const displayHint = needsTruncation ? truncateHint(fullHint, MAX_HINT_LENGTH) : fullHint;
|
|
1461
|
-
const hint = displayHint ? import_picocolors.default.dim(` (${displayHint})`) : "";
|
|
1509
|
+
const hint = item.option.option.hint || "";
|
|
1462
1510
|
const isLastInGroup = i + 1 >= visibleItems.length || visibleItems[i + 1].type === "group";
|
|
1463
1511
|
const indent = isLastInGroup ? `${import_picocolors.default.gray("\u2514")} ` : `${import_picocolors.default.gray("\u2502")} `;
|
|
1464
|
-
const
|
|
1512
|
+
const paddedLabel = item.option.option.label.padEnd(MAX_LABEL_WIDTH);
|
|
1513
|
+
const highlightedPaddedLabel = this.searchTerm ? highlightMatch(paddedLabel, this.searchTerm) : paddedLabel;
|
|
1514
|
+
const line = isActive ? `${indent}${checkbox} ${highlightedPaddedLabel} ${import_picocolors.default.dim(hint)}` : `${indent}${checkbox} ${import_picocolors.default.dim(paddedLabel)} ${import_picocolors.default.dim(hint)}`;
|
|
1465
1515
|
lines.push(`${import_picocolors.default.cyan(S_BAR)} ${line}`);
|
|
1466
|
-
if (isActive && needsTruncation) {
|
|
1467
|
-
const descIndent = isLastInGroup ? " " : `${import_picocolors.default.gray("\u2502")} `;
|
|
1468
|
-
const wrappedLines = wrapText(fullHint, DESCRIPTION_MAX_WIDTH);
|
|
1469
|
-
for (const descLine of wrappedLines) {
|
|
1470
|
-
lines.push(
|
|
1471
|
-
`${import_picocolors.default.cyan(S_BAR)} ${descIndent}${DESCRIPTION_INDENT}${import_picocolors.default.dim(descLine)}`
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
1516
|
}
|
|
1476
1517
|
}
|
|
1477
1518
|
if (belowCount > 0) {
|
|
@@ -1856,14 +1897,70 @@ function printSkillTree(node, indent = 0) {
|
|
|
1856
1897
|
console.log(chalk.green(`\u2502 ${prefix}\u2022 ${s.name}`) + desc);
|
|
1857
1898
|
}
|
|
1858
1899
|
}
|
|
1859
|
-
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
}
|
|
1864
|
-
|
|
1900
|
+
function formatGitError(error, url) {
|
|
1901
|
+
const msg = error.message.toLowerCase();
|
|
1902
|
+
if (msg.includes("authentication") || msg.includes("401") || msg.includes("403")) {
|
|
1903
|
+
return `Authentication failed for ${url}. Check your credentials or ensure the repository is public.`;
|
|
1904
|
+
}
|
|
1905
|
+
if (msg.includes("not found") || msg.includes("404") || msg.includes("does not exist")) {
|
|
1906
|
+
return `Repository not found: ${url}. Check the URL or owner/repo name.`;
|
|
1907
|
+
}
|
|
1908
|
+
if (msg.includes("timeout") || msg.includes("timed out")) {
|
|
1909
|
+
return `Connection timed out while cloning ${url}. Check your network connection.`;
|
|
1910
|
+
}
|
|
1911
|
+
if (msg.includes("could not resolve host") || msg.includes("network")) {
|
|
1912
|
+
return `Network error while cloning ${url}. Check your internet connection.`;
|
|
1913
|
+
}
|
|
1914
|
+
if (msg.includes("permission denied")) {
|
|
1915
|
+
return `Permission denied when accessing ${url}. The repository may be private.`;
|
|
1916
|
+
}
|
|
1917
|
+
return `Failed to clone repository: ${error.message}`;
|
|
1918
|
+
}
|
|
1919
|
+
function formatInstallStatus(statuses, isDryRun) {
|
|
1920
|
+
if (statuses.length === 0) return;
|
|
1921
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1922
|
+
for (const status of statuses) {
|
|
1923
|
+
const key = status.agentName;
|
|
1924
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
1925
|
+
grouped.get(key).push(status);
|
|
1926
|
+
}
|
|
1927
|
+
for (const [agent, skills] of grouped) {
|
|
1928
|
+
console.log(chalk.bold(`
|
|
1929
|
+
${agent}:`));
|
|
1930
|
+
for (const skill of skills) {
|
|
1931
|
+
let icon;
|
|
1932
|
+
let color2;
|
|
1933
|
+
let suffix = "";
|
|
1934
|
+
switch (skill.status) {
|
|
1935
|
+
case "installed":
|
|
1936
|
+
icon = "\u2713";
|
|
1937
|
+
color2 = chalk.green;
|
|
1938
|
+
suffix = skill.path ? chalk.dim(` \u2192 ${skill.path}`) : "";
|
|
1939
|
+
break;
|
|
1940
|
+
case "would-install":
|
|
1941
|
+
icon = "\u25CB";
|
|
1942
|
+
color2 = chalk.cyan;
|
|
1943
|
+
suffix = skill.path ? chalk.dim(` \u2192 ${skill.path}`) : "";
|
|
1944
|
+
break;
|
|
1945
|
+
case "skipped":
|
|
1946
|
+
icon = "\u2013";
|
|
1947
|
+
color2 = chalk.yellow;
|
|
1948
|
+
suffix = skill.reason ? chalk.dim(` (${skill.reason})`) : "";
|
|
1949
|
+
break;
|
|
1950
|
+
case "failed":
|
|
1951
|
+
icon = "\u2717";
|
|
1952
|
+
color2 = chalk.red;
|
|
1953
|
+
suffix = skill.reason ? chalk.dim(` (${skill.reason})`) : "";
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
console.log(` ${color2(icon)} ${skill.skillName}${suffix}`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
if (isDryRun) {
|
|
1960
|
+
console.log(chalk.cyan("\n(dry-run mode - no changes made)"));
|
|
1961
|
+
}
|
|
1865
1962
|
}
|
|
1866
|
-
async function
|
|
1963
|
+
async function runInstall(source, options) {
|
|
1867
1964
|
let tempDirToClean = null;
|
|
1868
1965
|
const handleSignal = () => {
|
|
1869
1966
|
if (tempDirToClean) {
|
|
@@ -1881,6 +1978,7 @@ async function run(source, options) {
|
|
|
1881
1978
|
if (!source) {
|
|
1882
1979
|
clack.log.error("Please provide a source (GitHub repo, URL, or local path)");
|
|
1883
1980
|
clack.log.info("Usage: skai <source> [options]");
|
|
1981
|
+
clack.log.info("");
|
|
1884
1982
|
clack.log.info("Examples:");
|
|
1885
1983
|
clack.log.info(" skai pproenca/dot-skills");
|
|
1886
1984
|
clack.log.info(" skai https://github.com/org/repo");
|
|
@@ -1898,6 +1996,9 @@ async function run(source, options) {
|
|
|
1898
1996
|
if (!parsed.localPath) {
|
|
1899
1997
|
throw new Error("Local path not found in parsed source");
|
|
1900
1998
|
}
|
|
1999
|
+
if (!fs7.existsSync(parsed.localPath)) {
|
|
2000
|
+
throw new Error(`Local path does not exist: ${parsed.localPath}`);
|
|
2001
|
+
}
|
|
1901
2002
|
skillsBasePath = parsed.localPath;
|
|
1902
2003
|
clack.log.info(`Using local path: ${skillsBasePath}`);
|
|
1903
2004
|
} else {
|
|
@@ -1914,7 +2015,8 @@ async function run(source, options) {
|
|
|
1914
2015
|
spinner2.stop("Repository cloned");
|
|
1915
2016
|
} catch (error) {
|
|
1916
2017
|
spinner2.stop("Failed to clone repository");
|
|
1917
|
-
|
|
2018
|
+
const formattedError = formatGitError(error, parsed.url || source);
|
|
2019
|
+
throw new Error(formattedError);
|
|
1918
2020
|
}
|
|
1919
2021
|
}
|
|
1920
2022
|
const discoverSpinner = clack.spinner();
|
|
@@ -1923,6 +2025,7 @@ async function run(source, options) {
|
|
|
1923
2025
|
discoverSpinner.stop(`Found ${skills.length} skill(s)`);
|
|
1924
2026
|
if (skills.length === 0) {
|
|
1925
2027
|
clack.log.warn("No skills found in the repository");
|
|
2028
|
+
clack.log.info("Skills must have a SKILL.md file with frontmatter metadata.");
|
|
1926
2029
|
clack.outro(chalk.yellow("No skills to install"));
|
|
1927
2030
|
return;
|
|
1928
2031
|
}
|
|
@@ -1956,12 +2059,24 @@ async function run(source, options) {
|
|
|
1956
2059
|
}
|
|
1957
2060
|
let targetAgents;
|
|
1958
2061
|
if (options.agent && options.agent.length > 0) {
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2062
|
+
const invalidAgents = [];
|
|
2063
|
+
targetAgents = [];
|
|
2064
|
+
for (const name of options.agent) {
|
|
2065
|
+
const agent = getAgentByName(name);
|
|
2066
|
+
if (agent) {
|
|
2067
|
+
targetAgents.push(agent);
|
|
2068
|
+
} else {
|
|
2069
|
+
invalidAgents.push(name);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (invalidAgents.length > 0) {
|
|
2073
|
+
clack.log.warn(`Unknown agent(s): ${invalidAgents.join(", ")}`);
|
|
1962
2074
|
clack.log.info(
|
|
1963
2075
|
`Available agents: ${getAllAgents().map((a) => a.name).join(", ")}`
|
|
1964
2076
|
);
|
|
2077
|
+
}
|
|
2078
|
+
if (targetAgents.length === 0) {
|
|
2079
|
+
clack.log.error("No valid agents specified");
|
|
1965
2080
|
clack.outro(chalk.red("No valid agents"));
|
|
1966
2081
|
return;
|
|
1967
2082
|
}
|
|
@@ -2053,6 +2168,44 @@ async function run(source, options) {
|
|
|
2053
2168
|
}
|
|
2054
2169
|
isGlobal = scope === "global";
|
|
2055
2170
|
}
|
|
2171
|
+
if (options.dryRun) {
|
|
2172
|
+
const statuses2 = [];
|
|
2173
|
+
const installOptions2 = { global: isGlobal, yes: options.yes };
|
|
2174
|
+
for (const skill of selectedSkills) {
|
|
2175
|
+
for (const agent of selectedAgents) {
|
|
2176
|
+
const targetPath = getSkillInstallPath(skill.name, agent, installOptions2);
|
|
2177
|
+
if (isSkillInstalled(skill, agent, installOptions2)) {
|
|
2178
|
+
statuses2.push({
|
|
2179
|
+
skillName: skill.name,
|
|
2180
|
+
agentName: agent.displayName,
|
|
2181
|
+
status: "skipped",
|
|
2182
|
+
path: targetPath,
|
|
2183
|
+
reason: "already installed"
|
|
2184
|
+
});
|
|
2185
|
+
} else {
|
|
2186
|
+
statuses2.push({
|
|
2187
|
+
skillName: skill.name,
|
|
2188
|
+
agentName: agent.displayName,
|
|
2189
|
+
status: "would-install",
|
|
2190
|
+
path: targetPath
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
if (options.json) {
|
|
2196
|
+
const jsonOutput = {
|
|
2197
|
+
dry_run: true,
|
|
2198
|
+
would_install: statuses2.filter((s) => s.status === "would-install").map((s) => ({ skill: s.skillName, agent: s.agentName, path: s.path })),
|
|
2199
|
+
would_skip: statuses2.filter((s) => s.status === "skipped").map((s) => ({ skill: s.skillName, agent: s.agentName, reason: s.reason }))
|
|
2200
|
+
};
|
|
2201
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
clack.log.info(chalk.bold("\nDry Run - Installation Preview:"));
|
|
2205
|
+
formatInstallStatus(statuses2, true);
|
|
2206
|
+
clack.outro(chalk.cyan("Dry run complete"));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2056
2209
|
if (!options.yes) {
|
|
2057
2210
|
const summary = [
|
|
2058
2211
|
`Skills: ${selectedSkills.map((s) => s.name).join(", ")}`,
|
|
@@ -2079,20 +2232,41 @@ Installation Summary:
|
|
|
2079
2232
|
};
|
|
2080
2233
|
const installOptions = { global: isGlobal, yes: options.yes };
|
|
2081
2234
|
const installedSkillNames = [];
|
|
2235
|
+
const statuses = [];
|
|
2082
2236
|
for (const skill of selectedSkills) {
|
|
2083
2237
|
for (const agent of selectedAgents) {
|
|
2238
|
+
const targetPath = getSkillInstallPath(skill.name, agent, installOptions);
|
|
2084
2239
|
if (isSkillInstalled(skill, agent, installOptions)) {
|
|
2085
2240
|
results.skipped++;
|
|
2241
|
+
statuses.push({
|
|
2242
|
+
skillName: skill.name,
|
|
2243
|
+
agentName: agent.displayName,
|
|
2244
|
+
status: "skipped",
|
|
2245
|
+
path: targetPath,
|
|
2246
|
+
reason: "already installed"
|
|
2247
|
+
});
|
|
2086
2248
|
continue;
|
|
2087
2249
|
}
|
|
2088
2250
|
const result = installSkillForAgent(skill, agent, installOptions);
|
|
2089
2251
|
if (result.success) {
|
|
2090
2252
|
results.success++;
|
|
2253
|
+
statuses.push({
|
|
2254
|
+
skillName: skill.name,
|
|
2255
|
+
agentName: agent.displayName,
|
|
2256
|
+
status: "installed",
|
|
2257
|
+
path: result.targetPath
|
|
2258
|
+
});
|
|
2091
2259
|
if (!installedSkillNames.includes(skill.name)) {
|
|
2092
2260
|
installedSkillNames.push(skill.name);
|
|
2093
2261
|
}
|
|
2094
2262
|
} else {
|
|
2095
2263
|
results.failed++;
|
|
2264
|
+
statuses.push({
|
|
2265
|
+
skillName: skill.name,
|
|
2266
|
+
agentName: agent.displayName,
|
|
2267
|
+
status: "failed",
|
|
2268
|
+
reason: result.error
|
|
2269
|
+
});
|
|
2096
2270
|
if (!options.json) {
|
|
2097
2271
|
clack.log.warn(`Failed to install ${skill.name} to ${agent.displayName}: ${result.error}`);
|
|
2098
2272
|
}
|
|
@@ -2100,6 +2274,9 @@ Installation Summary:
|
|
|
2100
2274
|
}
|
|
2101
2275
|
}
|
|
2102
2276
|
installSpinner.stop(`Installed ${results.success} skill(s)`);
|
|
2277
|
+
if (!options.json && statuses.length > 0) {
|
|
2278
|
+
formatInstallStatus(statuses, false);
|
|
2279
|
+
}
|
|
2103
2280
|
const depSpinner = clack.spinner();
|
|
2104
2281
|
depSpinner.start("Scanning for dependencies...");
|
|
2105
2282
|
const skillDeps = [];
|
|
@@ -2251,6 +2428,220 @@ Installation Summary:
|
|
|
2251
2428
|
}
|
|
2252
2429
|
}
|
|
2253
2430
|
}
|
|
2431
|
+
async function runUninstall(skillNames, options) {
|
|
2432
|
+
clack.intro(chalk.cyan("skai uninstall"));
|
|
2433
|
+
if (skillNames.length === 0) {
|
|
2434
|
+
clack.log.error("Please provide at least one skill name to uninstall");
|
|
2435
|
+
clack.log.info("Usage: skai uninstall <skill> [skill...] [-a <agent>]");
|
|
2436
|
+
clack.outro(chalk.red("No skills specified"));
|
|
2437
|
+
process.exit(EXIT_ERROR);
|
|
2438
|
+
}
|
|
2439
|
+
let targetAgents;
|
|
2440
|
+
if (options.agent && options.agent.length > 0) {
|
|
2441
|
+
targetAgents = options.agent.map((name) => getAgentByName(name)).filter((a) => a !== void 0);
|
|
2442
|
+
if (targetAgents.length === 0) {
|
|
2443
|
+
clack.log.error(`No valid agents found for: ${options.agent.join(", ")}`);
|
|
2444
|
+
clack.log.info(
|
|
2445
|
+
`Available agents: ${getAllAgents().map((a) => a.name).join(", ")}`
|
|
2446
|
+
);
|
|
2447
|
+
clack.outro(chalk.red("No valid agents"));
|
|
2448
|
+
process.exit(EXIT_ERROR);
|
|
2449
|
+
}
|
|
2450
|
+
} else {
|
|
2451
|
+
targetAgents = detectInstalledAgents();
|
|
2452
|
+
if (targetAgents.length === 0) {
|
|
2453
|
+
targetAgents = getAllAgents();
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
const toUninstall = [];
|
|
2457
|
+
for (const skillName of skillNames) {
|
|
2458
|
+
for (const agent of targetAgents) {
|
|
2459
|
+
const projectInstalled = listInstalledSkills(agent, { projectOnly: true }).some((s) => s.name === skillName);
|
|
2460
|
+
const globalInstalled = listInstalledSkills(agent, { global: true }).some((s) => s.name === skillName);
|
|
2461
|
+
if (options.global && globalInstalled) {
|
|
2462
|
+
toUninstall.push({ skill: skillName, agent, scope: "global" });
|
|
2463
|
+
} else if (!options.global && projectInstalled) {
|
|
2464
|
+
toUninstall.push({ skill: skillName, agent, scope: "project" });
|
|
2465
|
+
} else if (!options.global && globalInstalled) {
|
|
2466
|
+
toUninstall.push({ skill: skillName, agent, scope: "global" });
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (toUninstall.length === 0) {
|
|
2471
|
+
clack.log.warn("No installed skills found matching the specified names");
|
|
2472
|
+
clack.outro(chalk.yellow("Nothing to uninstall"));
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (!options.yes) {
|
|
2476
|
+
clack.log.info(chalk.bold("\nSkills to uninstall:"));
|
|
2477
|
+
for (const item of toUninstall) {
|
|
2478
|
+
clack.log.info(` \u2022 ${item.skill} from ${item.agent.displayName} (${item.scope})`);
|
|
2479
|
+
}
|
|
2480
|
+
const confirmed = await clack.confirm({
|
|
2481
|
+
message: `Uninstall ${toUninstall.length} skill(s)?`
|
|
2482
|
+
});
|
|
2483
|
+
if (clack.isCancel(confirmed) || !confirmed) {
|
|
2484
|
+
clack.outro(chalk.yellow("Cancelled"));
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
const results = { success: 0, failed: 0 };
|
|
2489
|
+
const uninstalledSkills = [];
|
|
2490
|
+
const errors = [];
|
|
2491
|
+
for (const item of toUninstall) {
|
|
2492
|
+
const installOptions = { global: item.scope === "global", yes: options.yes };
|
|
2493
|
+
const result = uninstallSkill(item.skill, item.agent, installOptions);
|
|
2494
|
+
if (result.success) {
|
|
2495
|
+
results.success++;
|
|
2496
|
+
if (!uninstalledSkills.includes(item.skill)) {
|
|
2497
|
+
uninstalledSkills.push(item.skill);
|
|
2498
|
+
}
|
|
2499
|
+
if (!options.json) {
|
|
2500
|
+
clack.log.info(chalk.green(`\u2713 Uninstalled ${item.skill} from ${item.agent.displayName}`));
|
|
2501
|
+
}
|
|
2502
|
+
} else {
|
|
2503
|
+
results.failed++;
|
|
2504
|
+
errors.push({ skill: item.skill, agent: item.agent.displayName, error: result.error || "Unknown error" });
|
|
2505
|
+
if (!options.json) {
|
|
2506
|
+
clack.log.warn(`\u2717 Failed to uninstall ${item.skill} from ${item.agent.displayName}: ${result.error}`);
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if (options.json) {
|
|
2511
|
+
const jsonOutput = {
|
|
2512
|
+
skills_uninstalled: uninstalledSkills,
|
|
2513
|
+
errors
|
|
2514
|
+
};
|
|
2515
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
const resultParts = [];
|
|
2519
|
+
if (results.success > 0) resultParts.push(chalk.green(`${results.success} uninstalled`));
|
|
2520
|
+
if (results.failed > 0) resultParts.push(chalk.red(`${results.failed} failed`));
|
|
2521
|
+
clack.outro(resultParts.join(", ") || chalk.green("Done"));
|
|
2522
|
+
}
|
|
2523
|
+
async function runList(options) {
|
|
2524
|
+
if (!options.json) {
|
|
2525
|
+
clack.intro(chalk.cyan("skai list"));
|
|
2526
|
+
}
|
|
2527
|
+
let targetAgents;
|
|
2528
|
+
if (options.agent && options.agent.length > 0) {
|
|
2529
|
+
targetAgents = options.agent.map((name) => getAgentByName(name)).filter((a) => a !== void 0);
|
|
2530
|
+
if (targetAgents.length === 0 && !options.json) {
|
|
2531
|
+
clack.log.error(`No valid agents found for: ${options.agent.join(", ")}`);
|
|
2532
|
+
clack.log.info(
|
|
2533
|
+
`Available agents: ${getAllAgents().map((a) => a.name).join(", ")}`
|
|
2534
|
+
);
|
|
2535
|
+
clack.outro(chalk.red("No valid agents"));
|
|
2536
|
+
process.exit(EXIT_ERROR);
|
|
2537
|
+
}
|
|
2538
|
+
} else {
|
|
2539
|
+
targetAgents = detectInstalledAgents();
|
|
2540
|
+
if (targetAgents.length === 0) {
|
|
2541
|
+
targetAgents = getAllAgents();
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
const allSkills = [];
|
|
2545
|
+
const agentSkillsMap = /* @__PURE__ */ new Map();
|
|
2546
|
+
for (const agent of targetAgents) {
|
|
2547
|
+
const skills = listInstalledSkills(agent, { global: options.global });
|
|
2548
|
+
const projectSkills = [];
|
|
2549
|
+
const globalSkills = [];
|
|
2550
|
+
for (const skill of skills) {
|
|
2551
|
+
allSkills.push({
|
|
2552
|
+
name: skill.name,
|
|
2553
|
+
path: skill.path,
|
|
2554
|
+
agent: agent.displayName,
|
|
2555
|
+
scope: skill.scope
|
|
2556
|
+
});
|
|
2557
|
+
if (skill.scope === "project") {
|
|
2558
|
+
projectSkills.push(skill.name);
|
|
2559
|
+
} else {
|
|
2560
|
+
globalSkills.push(skill.name);
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
if (projectSkills.length > 0 || globalSkills.length > 0) {
|
|
2564
|
+
agentSkillsMap.set(agent.displayName, { project: projectSkills, global: globalSkills });
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
if (options.json) {
|
|
2568
|
+
const jsonOutput = { skills: allSkills };
|
|
2569
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (allSkills.length === 0) {
|
|
2573
|
+
clack.log.info("No skills installed");
|
|
2574
|
+
clack.outro(chalk.dim('Use "skai <source>" to install skills'));
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
for (const [agentName, skills] of agentSkillsMap) {
|
|
2578
|
+
console.log(chalk.bold(`
|
|
2579
|
+
${agentName}:`));
|
|
2580
|
+
if (skills.project.length > 0) {
|
|
2581
|
+
console.log(chalk.dim(" Project:"));
|
|
2582
|
+
for (const skill of skills.project.sort()) {
|
|
2583
|
+
console.log(chalk.green(` \u2022 ${skill}`));
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
if (skills.global.length > 0) {
|
|
2587
|
+
console.log(chalk.dim(" Global:"));
|
|
2588
|
+
for (const skill of skills.global.sort()) {
|
|
2589
|
+
console.log(chalk.blue(` \u2022 ${skill}`));
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
clack.outro(chalk.cyan(`${allSkills.length} skill(s) installed`));
|
|
2594
|
+
}
|
|
2595
|
+
async function runUpdate(_skillNames, _options) {
|
|
2596
|
+
clack.intro(chalk.cyan("skai update"));
|
|
2597
|
+
clack.log.warn("The update command requires tracking skill sources.");
|
|
2598
|
+
clack.log.info("Currently, skai does not track where skills were installed from.");
|
|
2599
|
+
clack.log.info("");
|
|
2600
|
+
clack.log.info("To update a skill, you can:");
|
|
2601
|
+
clack.log.info(" 1. Uninstall the existing skill: skai uninstall <skill>");
|
|
2602
|
+
clack.log.info(" 2. Reinstall from the source: skai <source>");
|
|
2603
|
+
clack.outro(chalk.yellow("Update not yet implemented"));
|
|
2604
|
+
}
|
|
2605
|
+
async function main() {
|
|
2606
|
+
const program = new Command();
|
|
2607
|
+
program.name("skai").description("The package manager for AI agent skills").version(getVersion(), "-V, --version", "Display version");
|
|
2608
|
+
program.argument("[source]", "GitHub repo, URL, or local path to install skills from").option("-g, --global", "Install to user directory instead of project", false).option("-a, --agent <agents...>", "Target specific agents").option("-s, --skill <skills...>", "Install specific skills by name").option("-l, --list", "List available skills without installing", false).option("-y, --yes", "Skip confirmation prompts", false).option("--json", "Output results in JSON format", false).option("--dry-run", "Preview installation without making changes", false).action(async (source, options) => {
|
|
2609
|
+
await runInstall(source, options);
|
|
2610
|
+
});
|
|
2611
|
+
program.command("install <source>").description("Install skills from a source").option("-g, --global", "Install to user directory instead of project", false).option("-a, --agent <agents...>", "Target specific agents").option("-s, --skill <skills...>", "Install specific skills by name").option("-l, --list", "List available skills without installing", false).option("-y, --yes", "Skip confirmation prompts", false).option("--json", "Output results in JSON format", false).option("--dry-run", "Preview installation without making changes", false).action(async (source, options) => {
|
|
2612
|
+
await runInstall(source, options);
|
|
2613
|
+
});
|
|
2614
|
+
program.command("uninstall <skills...>").alias("rm").alias("remove").description("Uninstall skills from agents").option("-g, --global", "Uninstall from global directory", false).option("-a, --agent <agents...>", "Target specific agents").option("-y, --yes", "Skip confirmation prompts", false).option("--json", "Output results in JSON format", false).action(async (skills, options) => {
|
|
2615
|
+
await runUninstall(skills, options);
|
|
2616
|
+
});
|
|
2617
|
+
program.command("list").alias("ls").description("List installed skills").option("-g, --global", "List only global skills", false).option("-a, --agent <agents...>", "Target specific agents").option("--json", "Output results in JSON format", false).action(async (options) => {
|
|
2618
|
+
await runList(options);
|
|
2619
|
+
});
|
|
2620
|
+
program.command("update [skills...]").alias("up").description("Update installed skills (coming soon)").option("-g, --global", "Update global skills", false).option("-a, --agent <agents...>", "Target specific agents").option("-y, --yes", "Skip confirmation prompts", false).option("--json", "Output results in JSON format", false).action(async (skills, options) => {
|
|
2621
|
+
await runUpdate(skills, options);
|
|
2622
|
+
});
|
|
2623
|
+
program.addHelpText(
|
|
2624
|
+
"after",
|
|
2625
|
+
`
|
|
2626
|
+
Examples:
|
|
2627
|
+
$ skai pproenca/dot-skills Install skills from GitHub
|
|
2628
|
+
$ skai https://github.com/org/repo Install from full URL
|
|
2629
|
+
$ skai ./local/skills Install from local directory
|
|
2630
|
+
$ skai pproenca/dot-skills -s typescript Install specific skill
|
|
2631
|
+
$ skai pproenca/dot-skills -a claude-code Install to specific agent
|
|
2632
|
+
$ skai pproenca/dot-skills --dry-run Preview installation
|
|
2633
|
+
$ skai list List installed skills
|
|
2634
|
+
$ skai list -a cursor List skills for specific agent
|
|
2635
|
+
$ skai uninstall typescript Uninstall a skill
|
|
2636
|
+
$ skai uninstall typescript -a cursor Uninstall from specific agent
|
|
2637
|
+
|
|
2638
|
+
Supported Agents:
|
|
2639
|
+
claude-code, cursor, copilot, windsurf, codex, opencode, amp,
|
|
2640
|
+
kilo-code, roo-code, goose, gemini, antigravity, clawdbot, droid
|
|
2641
|
+
`
|
|
2642
|
+
);
|
|
2643
|
+
await program.parseAsync(process.argv);
|
|
2644
|
+
}
|
|
2254
2645
|
main().catch((error) => {
|
|
2255
2646
|
console.error(chalk.red("Fatal error:"), error);
|
|
2256
2647
|
process.exit(EXIT_ERROR);
|