@yonpark/skillhub-cli 0.1.3 → 0.2.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.md +121 -185
- package/dist/commands/logout.js +44 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/sync.js +153 -291
- package/dist/core/syncCore.js +122 -0
- package/dist/index.js +80 -3
- package/dist/service/config.js +26 -0
- package/dist/service/gistService.js +25 -15
- package/dist/service/skillsService.js +197 -0
- package/dist/utils/output.js +10 -0
- package/dist/utils/retry.js +69 -0
- package/package.json +55 -48
package/dist/index.js
CHANGED
|
@@ -2,9 +2,23 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const commander_1 = require("commander");
|
|
4
4
|
const login_1 = require("./commands/login");
|
|
5
|
+
const logout_1 = require("./commands/logout");
|
|
6
|
+
const status_1 = require("./commands/status");
|
|
5
7
|
const sync_1 = require("./commands/sync");
|
|
8
|
+
function getPackageVersion() {
|
|
9
|
+
try {
|
|
10
|
+
const pkg = require("../package.json");
|
|
11
|
+
return pkg.version || "0.0.0";
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return "0.0.0";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function errorMessage(error) {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
6
20
|
const program = new commander_1.Command();
|
|
7
|
-
program.name("skillhub").description("SkillHub CLI").version(
|
|
21
|
+
program.name("skillhub").description("SkillHub CLI").version(getPackageVersion());
|
|
8
22
|
program
|
|
9
23
|
.command("login")
|
|
10
24
|
.description("Login: register your GitHub PAT with gist access")
|
|
@@ -15,7 +29,70 @@ program
|
|
|
15
29
|
.command("sync")
|
|
16
30
|
.description("Sync: reconcile local skills with remote Gist backup")
|
|
17
31
|
.option("-s, --strategy <strategy>", "merge strategy (union|latest)", "union")
|
|
32
|
+
.option("--dry-run", "show planned changes without applying them", false)
|
|
33
|
+
.option("--json", "print output as JSON", false)
|
|
18
34
|
.action(async (options) => {
|
|
19
|
-
|
|
35
|
+
try {
|
|
36
|
+
await (0, sync_1.runSync)({
|
|
37
|
+
strategyInput: options.strategy,
|
|
38
|
+
dryRun: options.dryRun,
|
|
39
|
+
json: options.json,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (options.json) {
|
|
44
|
+
console.log(JSON.stringify({
|
|
45
|
+
ok: false,
|
|
46
|
+
error: errorMessage(error),
|
|
47
|
+
}, null, 2));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
program
|
|
55
|
+
.command("status")
|
|
56
|
+
.description("Show local auth/sync status")
|
|
57
|
+
.option("--json", "print output as JSON", false)
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
try {
|
|
60
|
+
await (0, status_1.runStatus)({ json: options.json });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (options.json) {
|
|
64
|
+
console.log(JSON.stringify({
|
|
65
|
+
ok: false,
|
|
66
|
+
error: errorMessage(error),
|
|
67
|
+
}, null, 2));
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
program
|
|
75
|
+
.command("logout")
|
|
76
|
+
.description("Clear stored session data (token, gist id, last sync)")
|
|
77
|
+
.option("--yes", "skip confirmation prompt", false)
|
|
78
|
+
.option("--json", "print output as JSON", false)
|
|
79
|
+
.action(async (options) => {
|
|
80
|
+
try {
|
|
81
|
+
await (0, logout_1.runLogout)({ yes: options.yes, json: options.json });
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (options.json) {
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
ok: false,
|
|
87
|
+
error: errorMessage(error),
|
|
88
|
+
}, null, 2));
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
96
|
+
console.error(`Error: ${errorMessage(error)}`);
|
|
97
|
+
process.exitCode = 1;
|
|
20
98
|
});
|
|
21
|
-
program.parseAsync(process.argv);
|
package/dist/service/config.js
CHANGED
|
@@ -25,4 +25,30 @@ exports.configStore = {
|
|
|
25
25
|
const config = await getConfig();
|
|
26
26
|
config.set("gistId", gistId);
|
|
27
27
|
},
|
|
28
|
+
async getLastSyncAt() {
|
|
29
|
+
const config = await getConfig();
|
|
30
|
+
return config.get("lastSyncAt");
|
|
31
|
+
},
|
|
32
|
+
async setLastSyncAt(lastSyncAt) {
|
|
33
|
+
const config = await getConfig();
|
|
34
|
+
config.set("lastSyncAt", lastSyncAt);
|
|
35
|
+
},
|
|
36
|
+
async clearToken() {
|
|
37
|
+
const config = await getConfig();
|
|
38
|
+
config.delete("githubToken");
|
|
39
|
+
},
|
|
40
|
+
async clearGistId() {
|
|
41
|
+
const config = await getConfig();
|
|
42
|
+
config.delete("gistId");
|
|
43
|
+
},
|
|
44
|
+
async clearLastSyncAt() {
|
|
45
|
+
const config = await getConfig();
|
|
46
|
+
config.delete("lastSyncAt");
|
|
47
|
+
},
|
|
48
|
+
async clearSession() {
|
|
49
|
+
const config = await getConfig();
|
|
50
|
+
config.delete("githubToken");
|
|
51
|
+
config.delete("gistId");
|
|
52
|
+
config.delete("lastSyncAt");
|
|
53
|
+
},
|
|
28
54
|
};
|
|
@@ -2,33 +2,44 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createOctokit = createOctokit;
|
|
4
4
|
exports.verifyToken = verifyToken;
|
|
5
|
+
exports.checkGistAccess = checkGistAccess;
|
|
5
6
|
exports.findSkillhubGist = findSkillhubGist;
|
|
6
7
|
exports.getSkillhubPayload = getSkillhubPayload;
|
|
7
8
|
exports.createSkillhubGist = createSkillhubGist;
|
|
8
9
|
exports.updateSkillhubGist = updateSkillhubGist;
|
|
9
10
|
const rest_1 = require("@octokit/rest");
|
|
11
|
+
const retry_1 = require("../utils/retry");
|
|
10
12
|
const SKILLHUB_FILENAME = "skillhub.json";
|
|
13
|
+
const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
|
|
14
|
+
const GITHUB_TIMEOUT_MS = 10000;
|
|
15
|
+
function withGitHubRetry(label, fn) {
|
|
16
|
+
return (0, retry_1.retryAsync)(fn, { label, shouldRetry: retry_1.isTransientError });
|
|
17
|
+
}
|
|
11
18
|
function createOctokit(token) {
|
|
12
|
-
return new rest_1.Octokit({
|
|
19
|
+
return new rest_1.Octokit({
|
|
20
|
+
auth: token,
|
|
21
|
+
request: {
|
|
22
|
+
timeout: GITHUB_TIMEOUT_MS,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
13
25
|
}
|
|
14
26
|
async function verifyToken(token) {
|
|
15
27
|
const octokit = createOctokit(token);
|
|
16
|
-
await octokit.users.getAuthenticated();
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
await withGitHubRetry("users.getAuthenticated", () => octokit.users.getAuthenticated());
|
|
29
|
+
await withGitHubRetry("gists.list", () => octokit.rest.gists.list({ per_page: 1 }));
|
|
30
|
+
}
|
|
31
|
+
async function checkGistAccess(octokit) {
|
|
32
|
+
await withGitHubRetry("gists.list", () => octokit.rest.gists.list({ per_page: 1 }));
|
|
20
33
|
}
|
|
21
34
|
async function findSkillhubGist(octokit) {
|
|
22
|
-
const gists = await octokit.paginate(octokit.rest.gists.list, {
|
|
23
|
-
per_page: 100,
|
|
24
|
-
});
|
|
35
|
+
const gists = await withGitHubRetry("gists.paginate", () => octokit.paginate(octokit.rest.gists.list, { per_page: 100 }));
|
|
25
36
|
return gists.find((gist) => {
|
|
26
37
|
const files = Object.values(gist.files ?? {});
|
|
27
38
|
return files.some((file) => file?.filename === SKILLHUB_FILENAME);
|
|
28
39
|
});
|
|
29
40
|
}
|
|
30
41
|
async function getSkillhubPayload(octokit, gistId) {
|
|
31
|
-
const gist = await octokit.gists.get({ gist_id: gistId });
|
|
42
|
+
const gist = await withGitHubRetry("gists.get", () => octokit.gists.get({ gist_id: gistId }));
|
|
32
43
|
const file = Object.values(gist.data.files ?? {}).find((item) => item?.filename === SKILLHUB_FILENAME);
|
|
33
44
|
if (!file?.content) {
|
|
34
45
|
return null;
|
|
@@ -38,10 +49,9 @@ async function getSkillhubPayload(octokit, gistId) {
|
|
|
38
49
|
if (!Array.isArray(parsed.skills)) {
|
|
39
50
|
return null;
|
|
40
51
|
}
|
|
41
|
-
// 하위 호환성: string[] 형식을 SkillInfo[] 형식으로 변환
|
|
42
52
|
const normalizedSkills = parsed.skills.map((skill) => {
|
|
43
53
|
if (typeof skill === "string") {
|
|
44
|
-
return { name: skill, source:
|
|
54
|
+
return { name: skill, source: DEFAULT_SKILL_SOURCE_REPO };
|
|
45
55
|
}
|
|
46
56
|
return skill;
|
|
47
57
|
});
|
|
@@ -55,7 +65,7 @@ async function getSkillhubPayload(octokit, gistId) {
|
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
async function createSkillhubGist(octokit, payload) {
|
|
58
|
-
const response = await octokit.gists.create({
|
|
68
|
+
const response = await withGitHubRetry("gists.create", () => octokit.gists.create({
|
|
59
69
|
description: "SkillHub sync",
|
|
60
70
|
public: false,
|
|
61
71
|
files: {
|
|
@@ -63,16 +73,16 @@ async function createSkillhubGist(octokit, payload) {
|
|
|
63
73
|
content: JSON.stringify(payload, null, 2),
|
|
64
74
|
},
|
|
65
75
|
},
|
|
66
|
-
});
|
|
76
|
+
}));
|
|
67
77
|
return response.data;
|
|
68
78
|
}
|
|
69
79
|
async function updateSkillhubGist(octokit, gistId, payload) {
|
|
70
|
-
await octokit.gists.update({
|
|
80
|
+
await withGitHubRetry("gists.update", () => octokit.gists.update({
|
|
71
81
|
gist_id: gistId,
|
|
72
82
|
files: {
|
|
73
83
|
[SKILLHUB_FILENAME]: {
|
|
74
84
|
content: JSON.stringify(payload, null, 2),
|
|
75
85
|
},
|
|
76
86
|
},
|
|
77
|
-
});
|
|
87
|
+
}));
|
|
78
88
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isValidSource = isValidSource;
|
|
7
|
+
exports.getLocalSkills = getLocalSkills;
|
|
8
|
+
exports.installSkills = installSkills;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const node_util_1 = require("node:util");
|
|
14
|
+
const syncCore_1 = require("../core/syncCore");
|
|
15
|
+
const retry_1 = require("../utils/retry");
|
|
16
|
+
const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
|
|
17
|
+
const SKILLS_LOCK_FILENAME = "skills-lock.json";
|
|
18
|
+
const DEFAULT_SKILL_SOURCE_REPO = "vercel-labs/agent-skills";
|
|
19
|
+
const NPX_COMMAND = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
20
|
+
const SOURCE_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
21
|
+
const COMMAND_TIMEOUT_MS = 120000;
|
|
22
|
+
const COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
23
|
+
function stringifyCommandResult(result) {
|
|
24
|
+
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
|
25
|
+
}
|
|
26
|
+
async function runSkillsCommand(args, label) {
|
|
27
|
+
return (0, retry_1.retryAsync)(async () => {
|
|
28
|
+
return execFileAsync(NPX_COMMAND, ["skills", ...args], {
|
|
29
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
30
|
+
maxBuffer: COMMAND_MAX_BUFFER,
|
|
31
|
+
shell: process.platform === "win32",
|
|
32
|
+
});
|
|
33
|
+
}, { label, shouldRetry: retry_1.isTransientError });
|
|
34
|
+
}
|
|
35
|
+
function isValidSource(source) {
|
|
36
|
+
return SOURCE_PATTERN.test(source);
|
|
37
|
+
}
|
|
38
|
+
function getCandidateSkillsLockPaths() {
|
|
39
|
+
const cwdPath = node_path_1.default.resolve(process.cwd(), SKILLS_LOCK_FILENAME);
|
|
40
|
+
const homePath = node_path_1.default.resolve(node_os_1.default.homedir(), SKILLS_LOCK_FILENAME);
|
|
41
|
+
const homeConfigPaths = [
|
|
42
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills", SKILLS_LOCK_FILENAME),
|
|
43
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub", SKILLS_LOCK_FILENAME),
|
|
44
|
+
node_path_1.default.resolve(node_os_1.default.homedir(), ".skills", SKILLS_LOCK_FILENAME),
|
|
45
|
+
];
|
|
46
|
+
const winAppData = process.env.APPDATA;
|
|
47
|
+
const winLocalAppData = process.env.LOCALAPPDATA;
|
|
48
|
+
const windowsConfigPaths = [
|
|
49
|
+
...(winAppData
|
|
50
|
+
? [node_path_1.default.resolve(winAppData, "skills", SKILLS_LOCK_FILENAME)]
|
|
51
|
+
: []),
|
|
52
|
+
...(winLocalAppData
|
|
53
|
+
? [node_path_1.default.resolve(winLocalAppData, "skills", SKILLS_LOCK_FILENAME)]
|
|
54
|
+
: []),
|
|
55
|
+
];
|
|
56
|
+
return [cwdPath, homePath, ...homeConfigPaths, ...windowsConfigPaths];
|
|
57
|
+
}
|
|
58
|
+
function parseSkillsLock(raw) {
|
|
59
|
+
const parsed = JSON.parse(raw);
|
|
60
|
+
const extractSkills = (items) => {
|
|
61
|
+
return items.map((item) => {
|
|
62
|
+
if (typeof item === "string") {
|
|
63
|
+
return { name: item, source: DEFAULT_SKILL_SOURCE_REPO };
|
|
64
|
+
}
|
|
65
|
+
if (typeof item === "object" && item !== null) {
|
|
66
|
+
const objectItem = item;
|
|
67
|
+
const name = typeof objectItem.name === "string"
|
|
68
|
+
? objectItem.name
|
|
69
|
+
: typeof objectItem.skill === "string"
|
|
70
|
+
? objectItem.skill
|
|
71
|
+
: String(item);
|
|
72
|
+
const source = typeof objectItem.source === "string"
|
|
73
|
+
? objectItem.source
|
|
74
|
+
: typeof objectItem.repo === "string"
|
|
75
|
+
? objectItem.repo
|
|
76
|
+
: DEFAULT_SKILL_SOURCE_REPO;
|
|
77
|
+
return { name, source };
|
|
78
|
+
}
|
|
79
|
+
return { name: String(item), source: DEFAULT_SKILL_SOURCE_REPO };
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
if (Array.isArray(parsed?.skills)) {
|
|
83
|
+
return extractSkills(parsed.skills);
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(parsed)) {
|
|
86
|
+
return extractSkills(parsed);
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(parsed?.installedSkills)) {
|
|
89
|
+
return extractSkills(parsed.installedSkills);
|
|
90
|
+
}
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
function parseSkillsListOutput(output) {
|
|
94
|
+
const cleaned = output.replace(/\x1b\[[0-9;]*m/g, "");
|
|
95
|
+
const lines = cleaned.split(/\r?\n/).map((line) => line.trim());
|
|
96
|
+
const skills = [];
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (!line)
|
|
99
|
+
continue;
|
|
100
|
+
if (line === "Global Skills")
|
|
101
|
+
continue;
|
|
102
|
+
if (line.startsWith("Agents:"))
|
|
103
|
+
continue;
|
|
104
|
+
if (line.startsWith("No project skills found"))
|
|
105
|
+
continue;
|
|
106
|
+
if (line.startsWith("Try listing global skills"))
|
|
107
|
+
continue;
|
|
108
|
+
if (line.startsWith("No global skills found"))
|
|
109
|
+
continue;
|
|
110
|
+
if (line.startsWith("Try listing project skills without -g"))
|
|
111
|
+
continue;
|
|
112
|
+
const [name] = line.split(/\s+/);
|
|
113
|
+
if (!name)
|
|
114
|
+
continue;
|
|
115
|
+
if (name.includes("\\") || name.includes("/") || name.includes("~"))
|
|
116
|
+
continue;
|
|
117
|
+
skills.push({ name, source: DEFAULT_SKILL_SOURCE_REPO });
|
|
118
|
+
}
|
|
119
|
+
return (0, syncCore_1.normalizeSkills)(skills);
|
|
120
|
+
}
|
|
121
|
+
async function tryReadSkillsLock(lockPath) {
|
|
122
|
+
try {
|
|
123
|
+
const raw = await promises_1.default.readFile(lockPath, "utf-8");
|
|
124
|
+
return parseSkillsLock(raw);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function getLocalSkills() {
|
|
131
|
+
const listResult = await runSkillsCommand(["list", "-g"], "skills list -g");
|
|
132
|
+
const listOutput = stringifyCommandResult(listResult);
|
|
133
|
+
if (listOutput.includes("No global skills found") ||
|
|
134
|
+
listOutput.includes("Try listing project skills without -g")) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
const fromList = parseSkillsListOutput(listOutput);
|
|
138
|
+
if (fromList.length > 0) {
|
|
139
|
+
return fromList;
|
|
140
|
+
}
|
|
141
|
+
const lockResult = await runSkillsCommand(["generate-lock"], "skills generate-lock");
|
|
142
|
+
const lockOutput = stringifyCommandResult(lockResult);
|
|
143
|
+
const candidatePaths = getCandidateSkillsLockPaths();
|
|
144
|
+
for (const lockPath of candidatePaths) {
|
|
145
|
+
const parsed = await tryReadSkillsLock(lockPath);
|
|
146
|
+
if (parsed) {
|
|
147
|
+
return (0, syncCore_1.normalizeSkills)(parsed);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (lockOutput.includes("No installed skills found") ||
|
|
151
|
+
listOutput.includes("No project skills found")) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
throw new Error([
|
|
155
|
+
"Unable to construct local skills list.",
|
|
156
|
+
"- skills list -g output:",
|
|
157
|
+
listOutput,
|
|
158
|
+
"",
|
|
159
|
+
`- Searched ${SKILLS_LOCK_FILENAME} paths:`,
|
|
160
|
+
...candidatePaths.map((p) => ` - ${p}`),
|
|
161
|
+
"",
|
|
162
|
+
"- npx skills generate-lock output:",
|
|
163
|
+
lockOutput,
|
|
164
|
+
].join("\n"));
|
|
165
|
+
}
|
|
166
|
+
async function installSkills(skills, options = {}) {
|
|
167
|
+
const succeeded = [];
|
|
168
|
+
const failed = [];
|
|
169
|
+
for (const skill of skills) {
|
|
170
|
+
if (!isValidSource(skill.source)) {
|
|
171
|
+
const reason = `Invalid source "${skill.source}". Expected owner/repo format.`;
|
|
172
|
+
failed.push({ skill, reason });
|
|
173
|
+
if (options.verbose) {
|
|
174
|
+
console.warn(`Skill install failed: ${skill.name} (from ${skill.source})`);
|
|
175
|
+
console.warn(` - ${reason}`);
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const result = await runSkillsCommand(["add", skill.source, "--skill", skill.name, "--global", "--yes"], `skills add ${skill.source} --skill ${skill.name}`);
|
|
181
|
+
const output = stringifyCommandResult(result);
|
|
182
|
+
if (options.verbose && output) {
|
|
183
|
+
console.log(output);
|
|
184
|
+
}
|
|
185
|
+
succeeded.push(skill);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
189
|
+
failed.push({ skill, reason });
|
|
190
|
+
if (options.verbose) {
|
|
191
|
+
console.warn(`Skill install failed: ${skill.name} (from ${skill.source})`);
|
|
192
|
+
console.warn(` - ${reason}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { succeeded, failed };
|
|
197
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emitOutput = emitOutput;
|
|
4
|
+
function emitOutput(payload, asJson, formatText) {
|
|
5
|
+
if (asJson) {
|
|
6
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
console.log(formatText(payload));
|
|
10
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isTransientError = isTransientError;
|
|
4
|
+
exports.retryAsync = retryAsync;
|
|
5
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
6
|
+
const DEFAULT_INITIAL_DELAY_MS = 300;
|
|
7
|
+
const DEFAULT_FACTOR = 3;
|
|
8
|
+
const TRANSIENT_ERROR_CODES = new Set([
|
|
9
|
+
"ECONNRESET",
|
|
10
|
+
"ECONNREFUSED",
|
|
11
|
+
"ETIMEDOUT",
|
|
12
|
+
"EAI_AGAIN",
|
|
13
|
+
"ENOTFOUND",
|
|
14
|
+
"ESOCKETTIMEDOUT",
|
|
15
|
+
"ERR_SOCKET_CONNECTION_TIMEOUT",
|
|
16
|
+
"ABORT_ERR",
|
|
17
|
+
"ERR_STREAM_PREMATURE_CLOSE",
|
|
18
|
+
]);
|
|
19
|
+
function sleep(ms) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
setTimeout(resolve, ms);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function getErrorMessage(error) {
|
|
25
|
+
if (error instanceof Error) {
|
|
26
|
+
return error.message;
|
|
27
|
+
}
|
|
28
|
+
return String(error);
|
|
29
|
+
}
|
|
30
|
+
function isTransientError(error) {
|
|
31
|
+
const candidate = error;
|
|
32
|
+
if (typeof candidate.status === "number") {
|
|
33
|
+
return candidate.status === 429 || candidate.status >= 500;
|
|
34
|
+
}
|
|
35
|
+
if (candidate.code && TRANSIENT_ERROR_CODES.has(candidate.code)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const message = `${candidate.message ?? ""}`.toLowerCase();
|
|
39
|
+
return (message.includes("timeout") ||
|
|
40
|
+
message.includes("timed out") ||
|
|
41
|
+
message.includes("network") ||
|
|
42
|
+
message.includes("socket hang up") ||
|
|
43
|
+
message.includes("temporarily unavailable") ||
|
|
44
|
+
message.includes("econnreset"));
|
|
45
|
+
}
|
|
46
|
+
async function retryAsync(fn, options = {}) {
|
|
47
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
48
|
+
const initialDelayMs = options.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
|
|
49
|
+
const factor = options.factor ?? DEFAULT_FACTOR;
|
|
50
|
+
const shouldRetry = options.shouldRetry ?? isTransientError;
|
|
51
|
+
let delayMs = initialDelayMs;
|
|
52
|
+
let lastError;
|
|
53
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
54
|
+
try {
|
|
55
|
+
return await fn();
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
lastError = error;
|
|
59
|
+
const canRetry = attempt < maxAttempts && shouldRetry(error);
|
|
60
|
+
if (!canRetry) {
|
|
61
|
+
const labelPrefix = options.label ? `${options.label} failed` : "Operation failed";
|
|
62
|
+
throw new Error(`${labelPrefix} after ${attempt} attempt(s): ${getErrorMessage(error)}`);
|
|
63
|
+
}
|
|
64
|
+
await sleep(delayMs);
|
|
65
|
+
delayMs *= factor;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error(getErrorMessage(lastError));
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,48 +1,55 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@yonpark/skillhub-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "SkillHub CLI - sync skills with GitHub Gist",
|
|
5
|
-
"bin": {
|
|
6
|
-
"skillhub": "bin/skillhub.js"
|
|
7
|
-
},
|
|
8
|
-
"main": "dist/index.js",
|
|
9
|
-
"type": "commonjs",
|
|
10
|
-
"scripts": {
|
|
11
|
-
"build": "tsc && tsc-alias",
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@yonpark/skillhub-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "SkillHub CLI - sync skills with GitHub Gist",
|
|
5
|
+
"bin": {
|
|
6
|
+
"skillhub": "bin/skillhub.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"type": "commonjs",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && tsc-alias",
|
|
12
|
+
"prepare": "npm run build",
|
|
13
|
+
"test": "vitest",
|
|
14
|
+
"test:watch": "vitest --watch",
|
|
15
|
+
"test:ci": "vitest run",
|
|
16
|
+
"changeset": "changeset",
|
|
17
|
+
"version-packages": "changeset version",
|
|
18
|
+
"release": "changeset publish --access public"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"bin",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/yw9142/skillhub-cli.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"cli",
|
|
31
|
+
"skills",
|
|
32
|
+
"agent",
|
|
33
|
+
"gist",
|
|
34
|
+
"sync"
|
|
35
|
+
],
|
|
36
|
+
"author": "",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@octokit/rest": "^20.0.2",
|
|
43
|
+
"commander": "^11.1.0",
|
|
44
|
+
"conf": "^12.0.0",
|
|
45
|
+
"inquirer": "^9.2.12"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@changesets/cli": "^2.29.7",
|
|
49
|
+
"@types/inquirer": "^9.0.7",
|
|
50
|
+
"@types/node": "^20.11.10",
|
|
51
|
+
"tsc-alias": "^1.8.8",
|
|
52
|
+
"typescript": "^5.3.3",
|
|
53
|
+
"vitest": "^2.1.8"
|
|
54
|
+
}
|
|
55
|
+
}
|