@yonpark/skillhub-cli 0.1.3 → 0.3.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 +136 -185
- package/dist/commands/logout.js +44 -0
- package/dist/commands/status.js +61 -0
- package/dist/commands/sync.js +467 -279
- package/dist/core/syncCore.js +145 -0
- package/dist/index.js +106 -9
- package/dist/service/config.js +26 -0
- package/dist/service/gistService.js +25 -15
- package/dist/service/skillsService.js +227 -0
- package/dist/utils/output.js +10 -0
- package/dist/utils/retry.js +69 -0
- package/package.json +55 -48
package/dist/commands/sync.js
CHANGED
|
@@ -1,31 +1,75 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
3
|
+
exports.runSyncMerge = runSyncMerge;
|
|
4
|
+
exports.runSyncAuto = runSyncAuto;
|
|
5
|
+
exports.runSyncPush = runSyncPush;
|
|
6
|
+
exports.runSyncPull = runSyncPull;
|
|
7
|
+
const syncCore_1 = require("../core/syncCore");
|
|
12
8
|
const config_1 = require("../service/config");
|
|
13
9
|
const gistService_1 = require("../service/gistService");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
const skillsService_1 = require("../service/skillsService");
|
|
11
|
+
const output_1 = require("../utils/output");
|
|
12
|
+
function formatSyncSummary(summary) {
|
|
13
|
+
const prefix = summary.dryRun ? "Dry-run" : "Sync";
|
|
14
|
+
const failurePart = summary.failed.length > 0
|
|
15
|
+
? ` (${summary.failed.length} failed - check logs or JSON output)`
|
|
16
|
+
: "";
|
|
17
|
+
const actionLine = summary.dryRun
|
|
18
|
+
? `${prefix} ${summary.mode}: would upload ${summary.uploaded} change(s), would install ${summary.installPlanned} skill(s), would remove ${summary.removePlanned} skill(s)`
|
|
19
|
+
: `${prefix} ${summary.mode}: uploaded ${summary.uploaded} change(s), installed ${summary.installed} skill(s), removed ${summary.removed} skill(s)`;
|
|
20
|
+
const details = [
|
|
21
|
+
actionLine + failurePart,
|
|
22
|
+
`mode=${summary.mode}`,
|
|
23
|
+
`gistFound=${summary.gistFound}`,
|
|
24
|
+
`gistCreated=${summary.gistCreated}`,
|
|
25
|
+
`remoteNewer=${summary.remoteNewer === null ? "n/a" : String(summary.remoteNewer)}`,
|
|
26
|
+
`lastSyncAtUpdated=${summary.lastSyncAtUpdated}`,
|
|
27
|
+
];
|
|
28
|
+
return details.join("\n");
|
|
29
|
+
}
|
|
30
|
+
function createSummary(params) {
|
|
31
|
+
return {
|
|
32
|
+
ok: params.failed.length === 0,
|
|
33
|
+
mode: params.mode,
|
|
34
|
+
dryRun: params.dryRun,
|
|
35
|
+
gistFound: params.gistFound,
|
|
36
|
+
gistCreated: params.gistCreated,
|
|
37
|
+
remoteNewer: params.remoteNewer,
|
|
38
|
+
uploaded: params.uploaded,
|
|
39
|
+
installPlanned: params.installPlanned,
|
|
40
|
+
installed: params.installed,
|
|
41
|
+
removePlanned: params.removePlanned,
|
|
42
|
+
removed: params.removed,
|
|
43
|
+
failed: params.failed,
|
|
44
|
+
lastSyncAtUpdated: params.lastSyncAtUpdated,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function finalizeWithFailures(summary, asJson) {
|
|
48
|
+
if (summary.failed.length === 0) {
|
|
49
|
+
return summary;
|
|
50
|
+
}
|
|
51
|
+
if (asJson) {
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return summary;
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`Sync ${summary.mode} completed with ${summary.failed.length} failed operation(s). Check logs above.`);
|
|
56
|
+
}
|
|
57
|
+
async function ensureToken() {
|
|
20
58
|
const token = await config_1.configStore.getToken();
|
|
21
59
|
if (!token) {
|
|
22
|
-
throw new Error("You must login first. Run `skillhub login` and try again.");
|
|
60
|
+
throw new Error("You must login first. Run `skillhub auth login` and try again.");
|
|
23
61
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
return token;
|
|
63
|
+
}
|
|
64
|
+
async function safeGetPayload(octokit, gistId) {
|
|
65
|
+
try {
|
|
66
|
+
return await (0, gistService_1.getSkillhubPayload)(octokit, gistId);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function resolveRemoteState(token) {
|
|
29
73
|
const octokit = (0, gistService_1.createOctokit)(token);
|
|
30
74
|
let gistId = await config_1.configStore.getGistId();
|
|
31
75
|
let remotePayload = null;
|
|
@@ -38,292 +82,436 @@ async function runSync(strategyInput) {
|
|
|
38
82
|
if (!gistId) {
|
|
39
83
|
const found = await (0, gistService_1.findSkillhubGist)(octokit);
|
|
40
84
|
if (found?.id) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
await
|
|
44
|
-
remotePayload = await safeGetPayload(octokit, foundId);
|
|
85
|
+
gistId = found.id;
|
|
86
|
+
await config_1.configStore.setGistId(found.id);
|
|
87
|
+
remotePayload = await safeGetPayload(octokit, found.id);
|
|
45
88
|
}
|
|
46
89
|
}
|
|
47
|
-
|
|
48
|
-
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
49
|
-
if (!created.id) {
|
|
50
|
-
throw new Error("Gist was created, but the ID could not be determined.");
|
|
51
|
-
}
|
|
52
|
-
await config_1.configStore.setGistId(created.id);
|
|
53
|
-
console.log("No existing SkillHub Gist found. A new one has been created.");
|
|
54
|
-
console.log(`Uploaded 1 change, installed 0 skills`);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const resolvedRemote = remotePayload
|
|
58
|
-
? {
|
|
59
|
-
...remotePayload,
|
|
60
|
-
skills: normalizeSkills(remotePayload.skills ?? []),
|
|
61
|
-
}
|
|
62
|
-
: { skills: [], updatedAt: "" };
|
|
63
|
-
if (strategy === "latest") {
|
|
64
|
-
await applyLatestStrategy({
|
|
65
|
-
octokit,
|
|
66
|
-
gistId,
|
|
67
|
-
local: localPayload,
|
|
68
|
-
remote: resolvedRemote,
|
|
69
|
-
});
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
await applyUnionStrategy({
|
|
90
|
+
return {
|
|
73
91
|
octokit,
|
|
74
92
|
gistId,
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
93
|
+
gistFound: Boolean(gistId),
|
|
94
|
+
remotePayload,
|
|
95
|
+
};
|
|
78
96
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
return input;
|
|
82
|
-
}
|
|
83
|
-
return DEFAULT_STRATEGY;
|
|
97
|
+
function asPlanPayload(payload) {
|
|
98
|
+
return payload ?? { skills: [], updatedAt: "" };
|
|
84
99
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
function splitInstallCandidates(candidates) {
|
|
101
|
+
const invalidInstallCandidates = candidates
|
|
102
|
+
.filter((skill) => !(0, skillsService_1.isValidSource)(skill.source))
|
|
103
|
+
.map((skill) => ({
|
|
104
|
+
skill,
|
|
105
|
+
reason: `Invalid source "${skill.source}". Expected owner/repo format.`,
|
|
106
|
+
}));
|
|
107
|
+
const validInstallCandidates = candidates.filter((skill) => (0, skillsService_1.isValidSource)(skill.source));
|
|
108
|
+
return {
|
|
109
|
+
invalidInstallCandidates,
|
|
110
|
+
validInstallCandidates,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function confirmPullRemovalsIfNeeded(removeCandidates, options) {
|
|
114
|
+
if (removeCandidates.length === 0 || options.yes === true) {
|
|
115
|
+
return;
|
|
97
116
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
const { default: inquirer } = await import("inquirer");
|
|
118
|
+
const { confirm } = await inquirer.prompt([
|
|
119
|
+
{
|
|
120
|
+
type: "confirm",
|
|
121
|
+
name: "confirm",
|
|
122
|
+
default: false,
|
|
123
|
+
message: `Pull sync will remove ${removeCandidates.length} local skill(s). Continue?`,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
if (!confirm) {
|
|
127
|
+
throw new Error("Sync pull cancelled.");
|
|
101
128
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
129
|
+
}
|
|
130
|
+
async function runSyncMerge(options = {}) {
|
|
131
|
+
const dryRun = options.dryRun === true;
|
|
132
|
+
const asJson = options.json === true;
|
|
133
|
+
const token = await ensureToken();
|
|
134
|
+
const nowIso = new Date().toISOString();
|
|
135
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
136
|
+
const localPayload = {
|
|
137
|
+
skills: localSkills,
|
|
138
|
+
updatedAt: nowIso,
|
|
139
|
+
};
|
|
140
|
+
const { octokit, gistId, gistFound, remotePayload } = await resolveRemoteState(token);
|
|
141
|
+
if (!gistFound) {
|
|
142
|
+
if (dryRun) {
|
|
143
|
+
const summary = createSummary({
|
|
144
|
+
mode: "merge",
|
|
145
|
+
dryRun: true,
|
|
146
|
+
gistFound: false,
|
|
147
|
+
gistCreated: false,
|
|
148
|
+
remoteNewer: null,
|
|
149
|
+
uploaded: 1,
|
|
150
|
+
installPlanned: 0,
|
|
151
|
+
installed: 0,
|
|
152
|
+
removePlanned: 0,
|
|
153
|
+
removed: 0,
|
|
154
|
+
failed: [],
|
|
155
|
+
lastSyncAtUpdated: false,
|
|
156
|
+
});
|
|
157
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
158
|
+
return summary;
|
|
159
|
+
}
|
|
160
|
+
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
161
|
+
if (!created.id) {
|
|
162
|
+
throw new Error("Gist was created, but the ID could not be determined.");
|
|
110
163
|
}
|
|
164
|
+
await config_1.configStore.setGistId(created.id);
|
|
165
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
166
|
+
const summary = createSummary({
|
|
167
|
+
mode: "merge",
|
|
168
|
+
dryRun: false,
|
|
169
|
+
gistFound: false,
|
|
170
|
+
gistCreated: true,
|
|
171
|
+
remoteNewer: null,
|
|
172
|
+
uploaded: 1,
|
|
173
|
+
installPlanned: 0,
|
|
174
|
+
installed: 0,
|
|
175
|
+
removePlanned: 0,
|
|
176
|
+
removed: 0,
|
|
177
|
+
failed: [],
|
|
178
|
+
lastSyncAtUpdated: true,
|
|
179
|
+
});
|
|
180
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
181
|
+
return summary;
|
|
111
182
|
}
|
|
112
|
-
|
|
113
|
-
|
|
183
|
+
const plan = (0, syncCore_1.buildMergePlan)({
|
|
184
|
+
localPayload,
|
|
185
|
+
remotePayload: asPlanPayload(remotePayload),
|
|
186
|
+
nowIso,
|
|
187
|
+
});
|
|
188
|
+
const { invalidInstallCandidates, validInstallCandidates } = splitInstallCandidates(plan.installCandidates);
|
|
189
|
+
if (dryRun) {
|
|
190
|
+
const summary = createSummary({
|
|
191
|
+
mode: "merge",
|
|
192
|
+
dryRun: true,
|
|
193
|
+
gistFound: true,
|
|
194
|
+
gistCreated: false,
|
|
195
|
+
remoteNewer: null,
|
|
196
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
197
|
+
installPlanned: plan.installCandidates.length,
|
|
198
|
+
installed: 0,
|
|
199
|
+
removePlanned: 0,
|
|
200
|
+
removed: 0,
|
|
201
|
+
failed: invalidInstallCandidates,
|
|
202
|
+
lastSyncAtUpdated: false,
|
|
203
|
+
});
|
|
204
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
205
|
+
return summary;
|
|
114
206
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
...candidatePaths.map((p) => ` - ${p}`),
|
|
122
|
-
``,
|
|
123
|
-
`- npx skills generate-lock output:`,
|
|
124
|
-
output,
|
|
125
|
-
].join("\n"));
|
|
126
|
-
}
|
|
127
|
-
function getCandidateSkillsLockPaths() {
|
|
128
|
-
const cwdPath = node_path_1.default.resolve(process.cwd(), SKILLS_LOCK_FILENAME);
|
|
129
|
-
const homePath = node_path_1.default.resolve(node_os_1.default.homedir(), SKILLS_LOCK_FILENAME);
|
|
130
|
-
// Cross-platform candidates when we don't know exactly where the tool writes
|
|
131
|
-
const homeConfigPaths = [
|
|
132
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skills", SKILLS_LOCK_FILENAME),
|
|
133
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".config", "skillhub", SKILLS_LOCK_FILENAME),
|
|
134
|
-
node_path_1.default.resolve(node_os_1.default.homedir(), ".skills", SKILLS_LOCK_FILENAME),
|
|
135
|
-
];
|
|
136
|
-
// Windows-specific candidates
|
|
137
|
-
const winAppData = process.env.APPDATA;
|
|
138
|
-
const winLocalAppData = process.env.LOCALAPPDATA;
|
|
139
|
-
const windowsConfigPaths = [
|
|
140
|
-
...(winAppData
|
|
141
|
-
? [node_path_1.default.resolve(winAppData, "skills", SKILLS_LOCK_FILENAME)]
|
|
142
|
-
: []),
|
|
143
|
-
...(winLocalAppData
|
|
144
|
-
? [node_path_1.default.resolve(winLocalAppData, "skills", SKILLS_LOCK_FILENAME)]
|
|
145
|
-
: []),
|
|
207
|
+
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
208
|
+
verbose: !asJson,
|
|
209
|
+
});
|
|
210
|
+
const failed = [
|
|
211
|
+
...invalidInstallCandidates,
|
|
212
|
+
...installResult.failed,
|
|
146
213
|
];
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
async function tryReadSkillsLock(lockPath) {
|
|
150
|
-
try {
|
|
151
|
-
const raw = await promises_1.default.readFile(lockPath, "utf-8");
|
|
152
|
-
return parseSkillsLock(raw);
|
|
214
|
+
if (plan.uploadPayload) {
|
|
215
|
+
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
153
216
|
}
|
|
154
|
-
|
|
155
|
-
|
|
217
|
+
const summary = createSummary({
|
|
218
|
+
mode: "merge",
|
|
219
|
+
dryRun: false,
|
|
220
|
+
gistFound: true,
|
|
221
|
+
gistCreated: false,
|
|
222
|
+
remoteNewer: null,
|
|
223
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
224
|
+
installPlanned: plan.installCandidates.length,
|
|
225
|
+
installed: installResult.succeeded.length,
|
|
226
|
+
removePlanned: 0,
|
|
227
|
+
removed: 0,
|
|
228
|
+
failed,
|
|
229
|
+
lastSyncAtUpdated: false,
|
|
230
|
+
});
|
|
231
|
+
if (failed.length === 0) {
|
|
232
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
233
|
+
summary.lastSyncAtUpdated = true;
|
|
156
234
|
}
|
|
235
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
236
|
+
return finalizeWithFailures(summary, asJson);
|
|
157
237
|
}
|
|
158
|
-
function
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const name = item.name || item.skill || String(item);
|
|
168
|
-
const source = item.source || item.repo || DEFAULT_SKILL_SOURCE_REPO;
|
|
169
|
-
return { name: String(name), source: String(source) };
|
|
170
|
-
}
|
|
171
|
-
return { name: String(item), source: DEFAULT_SKILL_SOURCE_REPO };
|
|
172
|
-
});
|
|
238
|
+
async function runSyncAuto(options = {}) {
|
|
239
|
+
const dryRun = options.dryRun === true;
|
|
240
|
+
const asJson = options.json === true;
|
|
241
|
+
const token = await ensureToken();
|
|
242
|
+
const nowIso = new Date().toISOString();
|
|
243
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
244
|
+
const localPayload = {
|
|
245
|
+
skills: localSkills,
|
|
246
|
+
updatedAt: nowIso,
|
|
173
247
|
};
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
248
|
+
const { octokit, gistId, gistFound, remotePayload } = await resolveRemoteState(token);
|
|
249
|
+
if (!gistFound) {
|
|
250
|
+
if (dryRun) {
|
|
251
|
+
const summary = createSummary({
|
|
252
|
+
mode: "auto",
|
|
253
|
+
dryRun: true,
|
|
254
|
+
gistFound: false,
|
|
255
|
+
gistCreated: false,
|
|
256
|
+
remoteNewer: null,
|
|
257
|
+
uploaded: 1,
|
|
258
|
+
installPlanned: 0,
|
|
259
|
+
installed: 0,
|
|
260
|
+
removePlanned: 0,
|
|
261
|
+
removed: 0,
|
|
262
|
+
failed: [],
|
|
263
|
+
lastSyncAtUpdated: false,
|
|
264
|
+
});
|
|
265
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
266
|
+
return summary;
|
|
267
|
+
}
|
|
268
|
+
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
269
|
+
if (!created.id) {
|
|
270
|
+
throw new Error("Gist was created, but the ID could not be determined.");
|
|
271
|
+
}
|
|
272
|
+
await config_1.configStore.setGistId(created.id);
|
|
273
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
274
|
+
const summary = createSummary({
|
|
275
|
+
mode: "auto",
|
|
276
|
+
dryRun: false,
|
|
277
|
+
gistFound: false,
|
|
278
|
+
gistCreated: true,
|
|
279
|
+
remoteNewer: null,
|
|
280
|
+
uploaded: 1,
|
|
281
|
+
installPlanned: 0,
|
|
282
|
+
installed: 0,
|
|
283
|
+
removePlanned: 0,
|
|
284
|
+
removed: 0,
|
|
285
|
+
failed: [],
|
|
286
|
+
lastSyncAtUpdated: true,
|
|
287
|
+
});
|
|
288
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
289
|
+
return summary;
|
|
184
290
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
continue;
|
|
211
|
-
// Skip obvious path-like tokens as a safety net
|
|
212
|
-
if (name.includes("\\") || name.includes("/") || name.includes("~"))
|
|
213
|
-
continue;
|
|
214
|
-
// skills list 출력에는 source 정보가 없으므로 기본값 사용
|
|
215
|
-
skills.push({ name, source: DEFAULT_SKILL_SOURCE_REPO });
|
|
291
|
+
const lastSyncAt = await config_1.configStore.getLastSyncAt();
|
|
292
|
+
const plan = (0, syncCore_1.buildAutoPlan)({
|
|
293
|
+
localPayload,
|
|
294
|
+
remotePayload: asPlanPayload(remotePayload),
|
|
295
|
+
lastSyncAt,
|
|
296
|
+
nowIso,
|
|
297
|
+
});
|
|
298
|
+
const { invalidInstallCandidates, validInstallCandidates } = splitInstallCandidates(plan.installCandidates);
|
|
299
|
+
if (dryRun) {
|
|
300
|
+
const summary = createSummary({
|
|
301
|
+
mode: "auto",
|
|
302
|
+
dryRun: true,
|
|
303
|
+
gistFound: true,
|
|
304
|
+
gistCreated: false,
|
|
305
|
+
remoteNewer: plan.isRemoteNewer,
|
|
306
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
307
|
+
installPlanned: plan.installCandidates.length,
|
|
308
|
+
installed: 0,
|
|
309
|
+
removePlanned: 0,
|
|
310
|
+
removed: 0,
|
|
311
|
+
failed: invalidInstallCandidates,
|
|
312
|
+
lastSyncAtUpdated: false,
|
|
313
|
+
});
|
|
314
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
315
|
+
return summary;
|
|
216
316
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
"No project skills found",
|
|
224
|
-
"Try listing global skills",
|
|
317
|
+
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
318
|
+
verbose: !asJson,
|
|
319
|
+
});
|
|
320
|
+
const failed = [
|
|
321
|
+
...invalidInstallCandidates,
|
|
322
|
+
...installResult.failed,
|
|
225
323
|
];
|
|
226
|
-
|
|
227
|
-
.
|
|
228
|
-
if (typeof skill === "string") {
|
|
229
|
-
return { name: skill, source: DEFAULT_SKILL_SOURCE_REPO };
|
|
230
|
-
}
|
|
231
|
-
return skill;
|
|
232
|
-
})
|
|
233
|
-
.filter((skill) => !!skill.name &&
|
|
234
|
-
!bannedSubstrings.some((bad) => skill.name.includes(bad)));
|
|
235
|
-
return uniqueSortedSkills(normalized);
|
|
236
|
-
}
|
|
237
|
-
async function safeGetPayload(octokit, gistId) {
|
|
238
|
-
try {
|
|
239
|
-
return await (0, gistService_1.getSkillhubPayload)(octokit, gistId);
|
|
324
|
+
if (plan.uploadPayload) {
|
|
325
|
+
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
240
326
|
}
|
|
241
|
-
|
|
242
|
-
|
|
327
|
+
const summary = createSummary({
|
|
328
|
+
mode: "auto",
|
|
329
|
+
dryRun: false,
|
|
330
|
+
gistFound: true,
|
|
331
|
+
gistCreated: false,
|
|
332
|
+
remoteNewer: plan.isRemoteNewer,
|
|
333
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
334
|
+
installPlanned: plan.installCandidates.length,
|
|
335
|
+
installed: installResult.succeeded.length,
|
|
336
|
+
removePlanned: 0,
|
|
337
|
+
removed: 0,
|
|
338
|
+
failed,
|
|
339
|
+
lastSyncAtUpdated: false,
|
|
340
|
+
});
|
|
341
|
+
if (failed.length === 0) {
|
|
342
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
343
|
+
summary.lastSyncAtUpdated = true;
|
|
243
344
|
}
|
|
345
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
346
|
+
return finalizeWithFailures(summary, asJson);
|
|
244
347
|
}
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
348
|
+
async function runSyncPush(options = {}) {
|
|
349
|
+
const dryRun = options.dryRun === true;
|
|
350
|
+
const asJson = options.json === true;
|
|
351
|
+
const token = await ensureToken();
|
|
352
|
+
const nowIso = new Date().toISOString();
|
|
353
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
354
|
+
const localPayload = {
|
|
355
|
+
skills: localSkills,
|
|
356
|
+
updatedAt: nowIso,
|
|
357
|
+
};
|
|
358
|
+
const { octokit, gistId, gistFound, remotePayload } = await resolveRemoteState(token);
|
|
359
|
+
if (!gistFound) {
|
|
360
|
+
if (dryRun) {
|
|
361
|
+
const summary = createSummary({
|
|
362
|
+
mode: "push",
|
|
363
|
+
dryRun: true,
|
|
364
|
+
gistFound: false,
|
|
365
|
+
gistCreated: false,
|
|
366
|
+
remoteNewer: null,
|
|
367
|
+
uploaded: 1,
|
|
368
|
+
installPlanned: 0,
|
|
369
|
+
installed: 0,
|
|
370
|
+
removePlanned: 0,
|
|
371
|
+
removed: 0,
|
|
372
|
+
failed: [],
|
|
373
|
+
lastSyncAtUpdated: false,
|
|
374
|
+
});
|
|
375
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
376
|
+
return summary;
|
|
253
377
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return a.source.localeCompare(b.source);
|
|
378
|
+
const created = await (0, gistService_1.createSkillhubGist)(octokit, localPayload);
|
|
379
|
+
if (!created.id) {
|
|
380
|
+
throw new Error("Gist was created, but the ID could not be determined.");
|
|
258
381
|
}
|
|
259
|
-
|
|
382
|
+
await config_1.configStore.setGistId(created.id);
|
|
383
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
384
|
+
const summary = createSummary({
|
|
385
|
+
mode: "push",
|
|
386
|
+
dryRun: false,
|
|
387
|
+
gistFound: false,
|
|
388
|
+
gistCreated: true,
|
|
389
|
+
remoteNewer: null,
|
|
390
|
+
uploaded: 1,
|
|
391
|
+
installPlanned: 0,
|
|
392
|
+
installed: 0,
|
|
393
|
+
removePlanned: 0,
|
|
394
|
+
removed: 0,
|
|
395
|
+
failed: [],
|
|
396
|
+
lastSyncAtUpdated: true,
|
|
397
|
+
});
|
|
398
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
399
|
+
return summary;
|
|
400
|
+
}
|
|
401
|
+
const plan = (0, syncCore_1.buildPushPlan)({
|
|
402
|
+
localPayload,
|
|
403
|
+
remotePayload: asPlanPayload(remotePayload),
|
|
404
|
+
nowIso,
|
|
260
405
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
reason && ` └─ ${reason}`,
|
|
282
|
-
]
|
|
283
|
-
.filter(Boolean)
|
|
284
|
-
.join("\n"));
|
|
285
|
-
}
|
|
406
|
+
if (dryRun) {
|
|
407
|
+
const summary = createSummary({
|
|
408
|
+
mode: "push",
|
|
409
|
+
dryRun: true,
|
|
410
|
+
gistFound: true,
|
|
411
|
+
gistCreated: false,
|
|
412
|
+
remoteNewer: null,
|
|
413
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
414
|
+
installPlanned: 0,
|
|
415
|
+
installed: 0,
|
|
416
|
+
removePlanned: 0,
|
|
417
|
+
removed: 0,
|
|
418
|
+
failed: [],
|
|
419
|
+
lastSyncAtUpdated: false,
|
|
420
|
+
});
|
|
421
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
422
|
+
return summary;
|
|
423
|
+
}
|
|
424
|
+
if (plan.uploadPayload) {
|
|
425
|
+
await (0, gistService_1.updateSkillhubGist)(octokit, gistId, plan.uploadPayload);
|
|
286
426
|
}
|
|
287
|
-
|
|
427
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
428
|
+
const summary = createSummary({
|
|
429
|
+
mode: "push",
|
|
430
|
+
dryRun: false,
|
|
431
|
+
gistFound: true,
|
|
432
|
+
gistCreated: false,
|
|
433
|
+
remoteNewer: null,
|
|
434
|
+
uploaded: plan.uploadPayload ? 1 : 0,
|
|
435
|
+
installPlanned: 0,
|
|
436
|
+
installed: 0,
|
|
437
|
+
removePlanned: 0,
|
|
438
|
+
removed: 0,
|
|
439
|
+
failed: [],
|
|
440
|
+
lastSyncAtUpdated: true,
|
|
441
|
+
});
|
|
442
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
443
|
+
return summary;
|
|
288
444
|
}
|
|
289
|
-
async function
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
445
|
+
async function runSyncPull(options = {}) {
|
|
446
|
+
const dryRun = options.dryRun === true;
|
|
447
|
+
const asJson = options.json === true;
|
|
448
|
+
const token = await ensureToken();
|
|
449
|
+
const nowIso = new Date().toISOString();
|
|
450
|
+
const localSkills = await (0, skillsService_1.getLocalSkills)();
|
|
451
|
+
const localPayload = {
|
|
452
|
+
skills: localSkills,
|
|
453
|
+
updatedAt: nowIso,
|
|
297
454
|
};
|
|
298
|
-
const {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const installCount = succeeded.length;
|
|
302
|
-
const failedCount = failed.length;
|
|
303
|
-
console.log(`업로드 ${uploadCount}건, 설치 ${installCount}건${failedCount ? ` (실패 ${failedCount}건은 로그 참고)` : ""}`);
|
|
304
|
-
}
|
|
305
|
-
async function applyLatestStrategy(params) {
|
|
306
|
-
const localTime = Date.parse(params.local.updatedAt);
|
|
307
|
-
const remoteTime = Date.parse(params.remote.updatedAt);
|
|
308
|
-
const isRemoteNewer = Number.isFinite(remoteTime) && remoteTime > localTime;
|
|
309
|
-
if (isRemoteNewer) {
|
|
310
|
-
const localSkills = normalizeSkills(params.local.skills);
|
|
311
|
-
const remoteSkills = normalizeSkills(params.remote.skills);
|
|
312
|
-
const missingLocally = remoteSkills.filter((skill) => !localSkills.some((local) => local.name === skill.name && local.source === skill.source));
|
|
313
|
-
const { succeeded, failed } = await installSkills(missingLocally);
|
|
314
|
-
const failedCount = failed.length;
|
|
315
|
-
console.log(`업로드 0건, 설치 ${succeeded.length}건${failedCount ? ` (실패 ${failedCount}건은 로그 참고)` : ""}`);
|
|
316
|
-
return;
|
|
455
|
+
const { gistFound, remotePayload } = await resolveRemoteState(token);
|
|
456
|
+
if (!gistFound) {
|
|
457
|
+
throw new Error("Remote SkillHub Gist not found. Run `skillhub sync push` to create it first.");
|
|
317
458
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
459
|
+
if (!remotePayload) {
|
|
460
|
+
throw new Error("Remote SkillHub payload is missing or invalid. Fix the remote `skillhub.json` and retry.");
|
|
461
|
+
}
|
|
462
|
+
const plan = (0, syncCore_1.buildPullPlan)({
|
|
463
|
+
localPayload,
|
|
464
|
+
remotePayload,
|
|
465
|
+
});
|
|
466
|
+
const { invalidInstallCandidates, validInstallCandidates } = splitInstallCandidates(plan.installCandidates);
|
|
467
|
+
if (dryRun) {
|
|
468
|
+
const summary = createSummary({
|
|
469
|
+
mode: "pull",
|
|
470
|
+
dryRun: true,
|
|
471
|
+
gistFound: true,
|
|
472
|
+
gistCreated: false,
|
|
473
|
+
remoteNewer: null,
|
|
474
|
+
uploaded: 0,
|
|
475
|
+
installPlanned: plan.installCandidates.length,
|
|
476
|
+
installed: 0,
|
|
477
|
+
removePlanned: plan.removeCandidates.length,
|
|
478
|
+
removed: 0,
|
|
479
|
+
failed: invalidInstallCandidates,
|
|
480
|
+
lastSyncAtUpdated: false,
|
|
481
|
+
});
|
|
482
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
483
|
+
return summary;
|
|
484
|
+
}
|
|
485
|
+
await confirmPullRemovalsIfNeeded(plan.removeCandidates, options);
|
|
486
|
+
const installResult = await (0, skillsService_1.installSkills)(validInstallCandidates, {
|
|
487
|
+
verbose: !asJson,
|
|
488
|
+
});
|
|
489
|
+
const removeResult = await (0, skillsService_1.removeSkills)(plan.removeCandidates, {
|
|
490
|
+
verbose: !asJson,
|
|
491
|
+
});
|
|
492
|
+
const failed = [
|
|
493
|
+
...invalidInstallCandidates,
|
|
494
|
+
...installResult.failed,
|
|
495
|
+
...removeResult.failed,
|
|
496
|
+
];
|
|
497
|
+
const summary = createSummary({
|
|
498
|
+
mode: "pull",
|
|
499
|
+
dryRun: false,
|
|
500
|
+
gistFound: true,
|
|
501
|
+
gistCreated: false,
|
|
502
|
+
remoteNewer: null,
|
|
503
|
+
uploaded: 0,
|
|
504
|
+
installPlanned: plan.installCandidates.length,
|
|
505
|
+
installed: installResult.succeeded.length,
|
|
506
|
+
removePlanned: plan.removeCandidates.length,
|
|
507
|
+
removed: removeResult.succeeded.length,
|
|
508
|
+
failed,
|
|
509
|
+
lastSyncAtUpdated: false,
|
|
510
|
+
});
|
|
511
|
+
if (failed.length === 0) {
|
|
512
|
+
await config_1.configStore.setLastSyncAt(nowIso);
|
|
513
|
+
summary.lastSyncAtUpdated = true;
|
|
326
514
|
}
|
|
327
|
-
|
|
328
|
-
|
|
515
|
+
(0, output_1.emitOutput)(summary, asJson, formatSyncSummary);
|
|
516
|
+
return finalizeWithFailures(summary, asJson);
|
|
329
517
|
}
|