@alphafox/cli 0.3.4 → 0.3.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/LICENSE +1 -1
- package/README.md +27 -6
- package/dist/auth/loopback-callback.js +3 -3
- package/dist/commands/run.js +26 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +20 -1
- package/dist/install/types.d.ts +8 -1
- package/dist/install/types.js +1 -0
- package/dist/install/wizard.js +91 -53
- package/dist/keychain/windows-credential.d.ts +1 -1
- package/dist/keychain/windows-credential.js +1 -1
- package/dist/skills/manager.d.ts +96 -0
- package/dist/skills/manager.js +445 -0
- package/dist/skills/run-command.d.ts +22 -0
- package/dist/skills/run-command.js +143 -0
- package/dist/skills-manifest.json +141 -0
- package/dist/update/notify.d.ts +24 -0
- package/dist/update/notify.js +109 -0
- package/dist/update/run-command.d.ts +41 -0
- package/dist/update/run-command.js +204 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/agents/issue-tracker.md +2 -2
- package/docs/agents/triage-labels.md +1 -1
- package/docs/alphafox-cli-installation-guide.md +15 -16
- package/docs/index.html +11 -11
- package/docs/release-supply-chain.md +11 -6
- package/package.json +2 -2
- package/skills/account/SKILL.md +1 -1
- package/skills/admin/SKILL.md +1 -1
- package/skills/alphafox/SKILL.md +26 -4
- package/skills/alphafox-shared/SKILL.md +24 -4
- package/skills/auth/SKILL.md +2 -2
- package/skills/engine-backtest/SKILL.md +1 -1
- package/skills/exchange/SKILL.md +1 -1
- package/skills/market/SKILL.md +1 -1
- package/skills/notification/SKILL.md +1 -1
- package/skills/strategy/SKILL.md +1 -1
- package/skills/trading/SKILL.md +1 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSkillsManifest = buildSkillsManifest;
|
|
4
|
+
exports.writeSkillsManifest = writeSkillsManifest;
|
|
5
|
+
exports.loadAndVerifySkillsManifest = loadAndVerifySkillsManifest;
|
|
6
|
+
exports.inspectSkills = inspectSkills;
|
|
7
|
+
exports.syncSkills = syncSkills;
|
|
8
|
+
exports.loadSkillsState = loadSkillsState;
|
|
9
|
+
exports.hashSkillDirectory = hashSkillDirectory;
|
|
10
|
+
exports.readSkillVersion = readSkillVersion;
|
|
11
|
+
exports.readSkillName = readSkillName;
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const node_fs_1 = require("node:fs");
|
|
14
|
+
const node_path_1 = require("node:path");
|
|
15
|
+
function buildSkillsManifest(packageRoot, versions) {
|
|
16
|
+
const skillsRoot = (0, node_path_1.join)(packageRoot, "skills");
|
|
17
|
+
if (!(0, node_fs_1.existsSync)(skillsRoot)) {
|
|
18
|
+
throw new Error(`Skills directory is missing: ${skillsRoot}`);
|
|
19
|
+
}
|
|
20
|
+
const skills = (0, node_fs_1.readdirSync)(skillsRoot, { withFileTypes: true })
|
|
21
|
+
.filter((entry) => entry.isDirectory() &&
|
|
22
|
+
(0, node_fs_1.existsSync)((0, node_path_1.join)(skillsRoot, entry.name, "SKILL.md")))
|
|
23
|
+
.map((entry) => {
|
|
24
|
+
const root = (0, node_path_1.join)(skillsRoot, entry.name);
|
|
25
|
+
const skillPath = (0, node_path_1.join)(root, "SKILL.md");
|
|
26
|
+
const files = manifestFiles(root);
|
|
27
|
+
const name = readSkillName(skillPath);
|
|
28
|
+
if (!name) {
|
|
29
|
+
throw Object.assign(new Error(`Skill name is missing from ${skillPath}`), {
|
|
30
|
+
type: "install",
|
|
31
|
+
subtype: "skills_bundle_name_missing",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const version = readSkillVersion(skillPath) ?? versions.packageVersion;
|
|
35
|
+
if (version !== versions.packageVersion) {
|
|
36
|
+
throw Object.assign(new Error(`Skill ${entry.name} version ${version} does not match package ${versions.packageVersion}`), {
|
|
37
|
+
type: "install",
|
|
38
|
+
subtype: "skills_bundle_version_mismatch",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
name,
|
|
43
|
+
version,
|
|
44
|
+
files,
|
|
45
|
+
hash: hashManifestFiles(files),
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
49
|
+
if (skills.length === 0) {
|
|
50
|
+
throw new Error(`No Skills found under ${skillsRoot}`);
|
|
51
|
+
}
|
|
52
|
+
if (new Set(skills.map((skill) => skill.name)).size !== skills.length) {
|
|
53
|
+
throw Object.assign(new Error("Skills bundle contains duplicate names"), {
|
|
54
|
+
type: "install",
|
|
55
|
+
subtype: "skills_bundle_duplicate_name",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
schemaVersion: 1,
|
|
60
|
+
packageName: "@alphafox/cli",
|
|
61
|
+
packageVersion: versions.packageVersion,
|
|
62
|
+
contractVersion: versions.contractVersion,
|
|
63
|
+
bundleHash: sha256(skills.map((skill) => `${skill.name}\0${skill.hash}`).join("\n")),
|
|
64
|
+
skills,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function writeSkillsManifest(packageRoot, outputPath, versions) {
|
|
68
|
+
const manifest = buildSkillsManifest(packageRoot, versions);
|
|
69
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(outputPath), { recursive: true });
|
|
70
|
+
(0, node_fs_1.writeFileSync)(outputPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
71
|
+
return manifest;
|
|
72
|
+
}
|
|
73
|
+
function loadAndVerifySkillsManifest(packageRoot, manifestPath = (0, node_path_1.join)(packageRoot, "dist", "skills-manifest.json")) {
|
|
74
|
+
try {
|
|
75
|
+
const recorded = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, "utf8"));
|
|
76
|
+
if (recorded.schemaVersion !== 1 ||
|
|
77
|
+
recorded.packageName !== "@alphafox/cli" ||
|
|
78
|
+
typeof recorded.packageVersion !== "string" ||
|
|
79
|
+
typeof recorded.contractVersion !== "string" ||
|
|
80
|
+
typeof recorded.bundleHash !== "string" ||
|
|
81
|
+
!Array.isArray(recorded.skills)) {
|
|
82
|
+
throw new Error("manifest shape is invalid");
|
|
83
|
+
}
|
|
84
|
+
const packageJson = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(packageRoot, "package.json"), "utf8"));
|
|
85
|
+
if (packageJson.name !== recorded.packageName ||
|
|
86
|
+
packageJson.version !== recorded.packageVersion) {
|
|
87
|
+
throw new Error(`manifest ${recorded.packageName}@${recorded.packageVersion} does not match package ${String(packageJson.name)}@${String(packageJson.version)}`);
|
|
88
|
+
}
|
|
89
|
+
const actual = buildSkillsManifest(packageRoot, {
|
|
90
|
+
packageVersion: recorded.packageVersion,
|
|
91
|
+
contractVersion: recorded.contractVersion,
|
|
92
|
+
});
|
|
93
|
+
if (actual.bundleHash !== recorded.bundleHash) {
|
|
94
|
+
throw new Error(`bundle hash ${actual.bundleHash} does not match ${recorded.bundleHash}`);
|
|
95
|
+
}
|
|
96
|
+
if (JSON.stringify(actual.skills) !== JSON.stringify(recorded.skills)) {
|
|
97
|
+
throw new Error("manifest Skill entries do not match bundled files");
|
|
98
|
+
}
|
|
99
|
+
return actual;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
throw Object.assign(new Error(`AlphaFox Skills bundle integrity check failed: ${manifestPath}`), {
|
|
103
|
+
type: "install",
|
|
104
|
+
subtype: "skills_bundle_integrity",
|
|
105
|
+
details: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function inspectSkills(input) {
|
|
110
|
+
const skills = input.manifest.skills.map((expected) => {
|
|
111
|
+
const installedDir = (0, node_path_1.join)(input.installedRoot, expected.name);
|
|
112
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(installedDir, "SKILL.md"))) {
|
|
113
|
+
return {
|
|
114
|
+
name: expected.name,
|
|
115
|
+
expectedVersion: expected.version,
|
|
116
|
+
installedVersion: null,
|
|
117
|
+
expectedHash: expected.hash,
|
|
118
|
+
installedHash: null,
|
|
119
|
+
status: "missing",
|
|
120
|
+
managed: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const installedHash = hashSkillDirectory(installedDir);
|
|
124
|
+
const installedVersion = readSkillVersion((0, node_path_1.join)(installedDir, "SKILL.md"));
|
|
125
|
+
if (installedHash === expected.hash) {
|
|
126
|
+
return {
|
|
127
|
+
name: expected.name,
|
|
128
|
+
expectedVersion: expected.version,
|
|
129
|
+
installedVersion,
|
|
130
|
+
expectedHash: expected.hash,
|
|
131
|
+
installedHash,
|
|
132
|
+
status: "current",
|
|
133
|
+
managed: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const previous = input.state?.skills[expected.name];
|
|
137
|
+
const stateConfirmsUnmodified = previous?.hash === installedHash;
|
|
138
|
+
const legacyVersionIsOlder = installedVersion != null &&
|
|
139
|
+
compareSemver(installedVersion, expected.version) < 0;
|
|
140
|
+
return {
|
|
141
|
+
name: expected.name,
|
|
142
|
+
expectedVersion: expected.version,
|
|
143
|
+
installedVersion,
|
|
144
|
+
expectedHash: expected.hash,
|
|
145
|
+
installedHash,
|
|
146
|
+
status: stateConfirmsUnmodified || (!input.state && legacyVersionIsOlder)
|
|
147
|
+
? "stale"
|
|
148
|
+
: "modified",
|
|
149
|
+
managed: Boolean(previous) || legacyVersionIsOlder,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const summary = {
|
|
153
|
+
current: 0,
|
|
154
|
+
missing: 0,
|
|
155
|
+
stale: 0,
|
|
156
|
+
modified: 0,
|
|
157
|
+
};
|
|
158
|
+
for (const skill of skills)
|
|
159
|
+
summary[skill.status] += 1;
|
|
160
|
+
const expectedNames = new Set(input.manifest.skills.map((skill) => skill.name));
|
|
161
|
+
const orphans = Object.entries(input.state?.skills ?? {})
|
|
162
|
+
.filter(([name]) => !expectedNames.has(name))
|
|
163
|
+
.flatMap(([name, recorded]) => {
|
|
164
|
+
const installedDir = (0, node_path_1.join)(input.installedRoot, name);
|
|
165
|
+
if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(installedDir, "SKILL.md")))
|
|
166
|
+
return [];
|
|
167
|
+
const installedHash = hashSkillDirectory(installedDir);
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
name,
|
|
171
|
+
installedHash,
|
|
172
|
+
recordedHash: recorded.hash,
|
|
173
|
+
modified: installedHash !== recorded.hash,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
})
|
|
177
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
178
|
+
return {
|
|
179
|
+
bundleVersion: input.manifest.packageVersion,
|
|
180
|
+
contractVersion: input.manifest.contractVersion,
|
|
181
|
+
bundleHash: input.manifest.bundleHash,
|
|
182
|
+
installedRoot: input.installedRoot,
|
|
183
|
+
skills,
|
|
184
|
+
orphans,
|
|
185
|
+
summary,
|
|
186
|
+
restartRequired: summary.missing + summary.stale + summary.modified + orphans.length > 0,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async function syncSkills(input, deps) {
|
|
190
|
+
const state = loadSkillsState(input.statePath);
|
|
191
|
+
const before = inspectSkills({
|
|
192
|
+
manifest: input.manifest,
|
|
193
|
+
installedRoot: input.installedRoot,
|
|
194
|
+
state,
|
|
195
|
+
});
|
|
196
|
+
const selected = before.skills
|
|
197
|
+
.filter((skill) => skill.status === "missing" ||
|
|
198
|
+
skill.status === "stale" ||
|
|
199
|
+
(input.force && skill.status === "modified"))
|
|
200
|
+
.map((skill) => skill.name);
|
|
201
|
+
const blocked = before.skills
|
|
202
|
+
.filter((skill) => skill.status === "modified" && !input.force)
|
|
203
|
+
.map((skill) => skill.name);
|
|
204
|
+
const removed = before.orphans
|
|
205
|
+
.filter((skill) => !skill.modified || input.force)
|
|
206
|
+
.map((skill) => skill.name);
|
|
207
|
+
blocked.push(...before.orphans
|
|
208
|
+
.filter((skill) => skill.modified && !input.force)
|
|
209
|
+
.map((skill) => skill.name));
|
|
210
|
+
if (input.dryRun) {
|
|
211
|
+
return {
|
|
212
|
+
action: selected.length + removed.length > 0 ? "planned" : "skipped",
|
|
213
|
+
updated: selected,
|
|
214
|
+
removed,
|
|
215
|
+
blocked,
|
|
216
|
+
status: before,
|
|
217
|
+
restartRequired: selected.length + removed.length > 0,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const release = acquireSkillsLock(input.statePath);
|
|
221
|
+
let backupDir;
|
|
222
|
+
try {
|
|
223
|
+
if (selected.length + removed.length > 0) {
|
|
224
|
+
backupDir = backupSkills([...new Set([...selected, ...removed])], input.installedRoot, input.statePath, deps.now?.() ?? new Date());
|
|
225
|
+
}
|
|
226
|
+
if (selected.length > 0) {
|
|
227
|
+
await deps.install(selected);
|
|
228
|
+
}
|
|
229
|
+
if (removed.length > 0) {
|
|
230
|
+
if (!deps.remove) {
|
|
231
|
+
throw Object.assign(new Error("Skills remover is required for retired managed Skills"), {
|
|
232
|
+
type: "install",
|
|
233
|
+
subtype: "skills_remove_unavailable",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
await deps.remove(removed);
|
|
237
|
+
}
|
|
238
|
+
const after = inspectSkills({
|
|
239
|
+
manifest: input.manifest,
|
|
240
|
+
installedRoot: input.installedRoot,
|
|
241
|
+
state,
|
|
242
|
+
});
|
|
243
|
+
const failed = selected.filter((name) => after.skills.find((skill) => skill.name === name)?.status !== "current");
|
|
244
|
+
if (failed.length > 0) {
|
|
245
|
+
throw Object.assign(new Error(`Skills sync verification failed for: ${failed.join(", ")}`), {
|
|
246
|
+
type: "install",
|
|
247
|
+
subtype: "skills_verify_failed",
|
|
248
|
+
details: { failed, backupDir },
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const removalFailed = removed.filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(input.installedRoot, name, "SKILL.md")));
|
|
252
|
+
if (removalFailed.length > 0) {
|
|
253
|
+
throw Object.assign(new Error(`Retired Skills removal verification failed for: ${removalFailed.join(", ")}`), {
|
|
254
|
+
type: "install",
|
|
255
|
+
subtype: "skills_remove_verify_failed",
|
|
256
|
+
details: { failed: removalFailed, backupDir },
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
saveSkillsState(input.statePath, stateFromStatus(input.manifest, after, deps.now?.() ?? new Date(), state));
|
|
260
|
+
return {
|
|
261
|
+
action: selected.length + removed.length > 0 ? "synced" : "skipped",
|
|
262
|
+
updated: selected,
|
|
263
|
+
removed,
|
|
264
|
+
blocked,
|
|
265
|
+
...(backupDir ? { backupDir } : {}),
|
|
266
|
+
status: after,
|
|
267
|
+
restartRequired: selected.length + removed.length > 0,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
release();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function loadSkillsState(path) {
|
|
275
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
276
|
+
return null;
|
|
277
|
+
let parsed;
|
|
278
|
+
try {
|
|
279
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
throw Object.assign(new Error(`Invalid Skills state file: ${path}`), {
|
|
283
|
+
type: "install",
|
|
284
|
+
subtype: "skills_state_invalid",
|
|
285
|
+
details: err instanceof Error ? err.message : String(err),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (!parsed ||
|
|
289
|
+
typeof parsed !== "object" ||
|
|
290
|
+
parsed.schemaVersion !== 1 ||
|
|
291
|
+
typeof parsed.skills !== "object") {
|
|
292
|
+
throw Object.assign(new Error(`Invalid Skills state file: ${path}`), {
|
|
293
|
+
type: "install",
|
|
294
|
+
subtype: "skills_state_invalid",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return parsed;
|
|
298
|
+
}
|
|
299
|
+
function hashSkillDirectory(skillRoot) {
|
|
300
|
+
return hashManifestFiles(manifestFiles(skillRoot));
|
|
301
|
+
}
|
|
302
|
+
function readSkillVersion(skillPath) {
|
|
303
|
+
try {
|
|
304
|
+
const match = (0, node_fs_1.readFileSync)(skillPath, "utf8").match(/^version:\s*["']?([^"'\s]+)["']?\s*$/m);
|
|
305
|
+
return match?.[1] ?? null;
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function readSkillName(skillPath) {
|
|
312
|
+
try {
|
|
313
|
+
const match = (0, node_fs_1.readFileSync)(skillPath, "utf8").match(/^name:\s*["']?([^"'\s]+)["']?\s*$/m);
|
|
314
|
+
return match?.[1] ?? null;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function manifestFiles(root) {
|
|
321
|
+
return listFiles(root)
|
|
322
|
+
.map((path) => {
|
|
323
|
+
const body = (0, node_fs_1.readFileSync)(path);
|
|
324
|
+
return {
|
|
325
|
+
path: (0, node_path_1.relative)(root, path).split(node_path_1.sep).join("/"),
|
|
326
|
+
sha256: sha256(body),
|
|
327
|
+
size: body.length,
|
|
328
|
+
};
|
|
329
|
+
})
|
|
330
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
331
|
+
}
|
|
332
|
+
function listFiles(root) {
|
|
333
|
+
const out = [];
|
|
334
|
+
for (const entry of (0, node_fs_1.readdirSync)(root, { withFileTypes: true })) {
|
|
335
|
+
const path = (0, node_path_1.join)(root, entry.name);
|
|
336
|
+
if (entry.isDirectory()) {
|
|
337
|
+
out.push(...listFiles(path));
|
|
338
|
+
}
|
|
339
|
+
else if (entry.isFile() || (0, node_fs_1.statSync)(path).isFile()) {
|
|
340
|
+
out.push(path);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
function hashManifestFiles(files) {
|
|
346
|
+
return sha256(files
|
|
347
|
+
.map((file) => `${file.path}\0${file.size}\0${file.sha256}`)
|
|
348
|
+
.join("\n"));
|
|
349
|
+
}
|
|
350
|
+
function sha256(value) {
|
|
351
|
+
return (0, node_crypto_1.createHash)("sha256").update(value).digest("hex");
|
|
352
|
+
}
|
|
353
|
+
function compareSemver(a, b) {
|
|
354
|
+
const pa = a.replace(/-.*$/, "").split(".").map(Number);
|
|
355
|
+
const pb = b.replace(/-.*$/, "").split(".").map(Number);
|
|
356
|
+
for (let i = 0; i < 3; i += 1) {
|
|
357
|
+
const delta = (pa[i] || 0) - (pb[i] || 0);
|
|
358
|
+
if (delta !== 0)
|
|
359
|
+
return delta < 0 ? -1 : 1;
|
|
360
|
+
}
|
|
361
|
+
return 0;
|
|
362
|
+
}
|
|
363
|
+
function saveSkillsState(path, state) {
|
|
364
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
365
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
366
|
+
(0, node_fs_1.writeFileSync)(tmp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
367
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
368
|
+
}
|
|
369
|
+
function stateFromStatus(manifest, status, now, previous) {
|
|
370
|
+
const skills = {};
|
|
371
|
+
for (const item of status.skills) {
|
|
372
|
+
if (item.status === "current" && item.installedHash) {
|
|
373
|
+
skills[item.name] = {
|
|
374
|
+
version: item.expectedVersion,
|
|
375
|
+
hash: item.installedHash,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (const orphan of status.orphans) {
|
|
380
|
+
const recorded = previous?.skills[orphan.name];
|
|
381
|
+
if (orphan.modified && recorded) {
|
|
382
|
+
skills[orphan.name] = recorded;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
schemaVersion: 1,
|
|
387
|
+
packageVersion: manifest.packageVersion,
|
|
388
|
+
contractVersion: manifest.contractVersion,
|
|
389
|
+
bundleHash: manifest.bundleHash,
|
|
390
|
+
syncedAt: now.toISOString(),
|
|
391
|
+
skills,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function backupSkills(names, installedRoot, statePath, now) {
|
|
395
|
+
const existing = names.filter((name) => (0, node_fs_1.existsSync)((0, node_path_1.join)(installedRoot, name, "SKILL.md")));
|
|
396
|
+
if (existing.length === 0)
|
|
397
|
+
return undefined;
|
|
398
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
399
|
+
const root = (0, node_path_1.join)((0, node_path_1.dirname)(statePath), "skill-backups", stamp);
|
|
400
|
+
(0, node_fs_1.mkdirSync)(root, { recursive: true });
|
|
401
|
+
for (const name of existing) {
|
|
402
|
+
(0, node_fs_1.cpSync)((0, node_path_1.join)(installedRoot, name), (0, node_path_1.join)(root, name), {
|
|
403
|
+
recursive: true,
|
|
404
|
+
dereference: true,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return root;
|
|
408
|
+
}
|
|
409
|
+
function acquireSkillsLock(statePath) {
|
|
410
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(statePath), { recursive: true });
|
|
411
|
+
const path = `${statePath}.lock`;
|
|
412
|
+
let fd;
|
|
413
|
+
try {
|
|
414
|
+
fd = (0, node_fs_1.openSync)(path, "wx", 0o600);
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
const code = err.code;
|
|
418
|
+
if (code === "EEXIST") {
|
|
419
|
+
const ageMs = Date.now() - (0, node_fs_1.statSync)(path).mtimeMs;
|
|
420
|
+
if (ageMs > 5 * 60_000) {
|
|
421
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
422
|
+
fd = (0, node_fs_1.openSync)(path, "wx", 0o600);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
throw Object.assign(new Error("Another AlphaFox Skills sync is already running"), {
|
|
426
|
+
type: "install",
|
|
427
|
+
subtype: "skills_sync_in_progress",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
throw err;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
(0, node_fs_1.writeFileSync)(fd, `${process.pid}\n`);
|
|
436
|
+
(0, node_fs_1.closeSync)(fd);
|
|
437
|
+
return () => {
|
|
438
|
+
try {
|
|
439
|
+
(0, node_fs_1.unlinkSync)(path);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
// Best effort: a stale lock is recovered after five minutes.
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { InstallRunner } from "../install/types";
|
|
2
|
+
import { type SkillsStatus, type SkillsSyncResult } from "./manager";
|
|
3
|
+
export interface SkillsCliFlags {
|
|
4
|
+
readonly format: "json" | "jsonl" | "text";
|
|
5
|
+
readonly yes: boolean;
|
|
6
|
+
readonly dryRun: boolean;
|
|
7
|
+
readonly noInput: boolean;
|
|
8
|
+
readonly jq?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface SkillsCommandDeps {
|
|
11
|
+
readonly runner?: InstallRunner;
|
|
12
|
+
readonly packageRoot?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function installedSkillsRoot(env?: NodeJS.ProcessEnv): string;
|
|
15
|
+
export declare function skillsStatePath(env?: NodeJS.ProcessEnv): string;
|
|
16
|
+
export declare function resolveCurrentSkillsPackageRoot(searchDirs?: readonly string[]): string;
|
|
17
|
+
export declare function inspectCurrentSkills(env?: NodeJS.ProcessEnv, packageRoot?: string): SkillsStatus;
|
|
18
|
+
export declare function syncCurrentSkills(input: {
|
|
19
|
+
readonly force: boolean;
|
|
20
|
+
readonly dryRun: boolean;
|
|
21
|
+
}, env?: NodeJS.ProcessEnv, deps?: SkillsCommandDeps): Promise<SkillsSyncResult>;
|
|
22
|
+
export declare function cmdSkills(args: string[], flags: SkillsCliFlags, env?: NodeJS.ProcessEnv, deps?: SkillsCommandDeps): Promise<number>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installedSkillsRoot = installedSkillsRoot;
|
|
4
|
+
exports.skillsStatePath = skillsStatePath;
|
|
5
|
+
exports.resolveCurrentSkillsPackageRoot = resolveCurrentSkillsPackageRoot;
|
|
6
|
+
exports.inspectCurrentSkills = inspectCurrentSkills;
|
|
7
|
+
exports.syncCurrentSkills = syncCurrentSkills;
|
|
8
|
+
exports.cmdSkills = cmdSkills;
|
|
9
|
+
const node_os_1 = require("node:os");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const profiles_1 = require("../config/profiles");
|
|
12
|
+
const envelope_1 = require("../envelope");
|
|
13
|
+
const exec_1 = require("../install/exec");
|
|
14
|
+
const package_root_1 = require("../install/package-root");
|
|
15
|
+
const manager_1 = require("./manager");
|
|
16
|
+
const SKILLS_TIMEOUT_MS = 120_000;
|
|
17
|
+
function installedSkillsRoot(env = process.env) {
|
|
18
|
+
return (env.ALPHAFOX_SKILLS_DIR?.trim() ||
|
|
19
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), ".agents", "skills"));
|
|
20
|
+
}
|
|
21
|
+
function skillsStatePath(env = process.env) {
|
|
22
|
+
return (0, node_path_1.join)((0, profiles_1.defaultConfigDir)(env), "skills-state.json");
|
|
23
|
+
}
|
|
24
|
+
function resolveCurrentSkillsPackageRoot(searchDirs = [__dirname, process.cwd()]) {
|
|
25
|
+
const root = (0, package_root_1.findAlphafoxPackageRoot)(searchDirs);
|
|
26
|
+
if (!root) {
|
|
27
|
+
throw Object.assign(new Error("Could not locate the installed @alphafox/cli Skills bundle"), {
|
|
28
|
+
type: "install",
|
|
29
|
+
subtype: "skills_package_missing",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
function inspectCurrentSkills(env = process.env, packageRoot = resolveCurrentSkillsPackageRoot()) {
|
|
35
|
+
const manifest = (0, manager_1.loadAndVerifySkillsManifest)(packageRoot);
|
|
36
|
+
return (0, manager_1.inspectSkills)({
|
|
37
|
+
manifest,
|
|
38
|
+
installedRoot: installedSkillsRoot(env),
|
|
39
|
+
state: (0, manager_1.loadSkillsState)(skillsStatePath(env)),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
async function syncCurrentSkills(input, env = process.env, deps = {}) {
|
|
43
|
+
const packageRoot = deps.packageRoot ?? resolveCurrentSkillsPackageRoot();
|
|
44
|
+
const runner = deps.runner ??
|
|
45
|
+
(0, exec_1.createDefaultInstallRunner)(env, [__dirname, process.cwd()]);
|
|
46
|
+
const manifest = (0, manager_1.loadAndVerifySkillsManifest)(packageRoot);
|
|
47
|
+
return await (0, manager_1.syncSkills)({
|
|
48
|
+
manifest,
|
|
49
|
+
packageRoot,
|
|
50
|
+
installedRoot: installedSkillsRoot(env),
|
|
51
|
+
statePath: skillsStatePath(env),
|
|
52
|
+
dryRun: input.dryRun,
|
|
53
|
+
force: input.force,
|
|
54
|
+
}, {
|
|
55
|
+
install: async (names) => {
|
|
56
|
+
await runner.exec("npx", [
|
|
57
|
+
"-y",
|
|
58
|
+
"skills",
|
|
59
|
+
"add",
|
|
60
|
+
packageRoot,
|
|
61
|
+
"-y",
|
|
62
|
+
"-g",
|
|
63
|
+
"--skill",
|
|
64
|
+
...names,
|
|
65
|
+
], { timeoutMs: SKILLS_TIMEOUT_MS });
|
|
66
|
+
},
|
|
67
|
+
remove: async (names) => {
|
|
68
|
+
await runner.exec("npx", ["-y", "skills", "remove", ...names, "-y", "-g"], { timeoutMs: SKILLS_TIMEOUT_MS });
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function cmdSkills(args, flags, env = process.env, deps = {}) {
|
|
73
|
+
const parsed = parseSkillsArgs(args);
|
|
74
|
+
if (parsed.action === "help") {
|
|
75
|
+
(0, envelope_1.writeSuccess)({
|
|
76
|
+
name: "skills",
|
|
77
|
+
usage: [
|
|
78
|
+
"alphafox skills status",
|
|
79
|
+
"alphafox skills sync [--dry-run]",
|
|
80
|
+
"alphafox skills sync --force --yes",
|
|
81
|
+
],
|
|
82
|
+
notes: [
|
|
83
|
+
"Skills are synced only from the verified bundle inside the installed @alphafox/cli package",
|
|
84
|
+
"Modified Skills are preserved unless --force --yes is explicit",
|
|
85
|
+
"Restart the AI tool after a successful sync",
|
|
86
|
+
],
|
|
87
|
+
}, { format: flags.format, jq: flags.jq });
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
if (parsed.action === "status") {
|
|
91
|
+
(0, envelope_1.writeSuccess)(inspectCurrentSkills(env, deps.packageRoot ?? resolveCurrentSkillsPackageRoot()), { format: flags.format, jq: flags.jq });
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
const runner = deps.runner ??
|
|
95
|
+
(0, exec_1.createDefaultInstallRunner)(env, [__dirname, process.cwd()]);
|
|
96
|
+
if (parsed.force && !flags.yes) {
|
|
97
|
+
const confirmed = !flags.noInput &&
|
|
98
|
+
runner.isTty() &&
|
|
99
|
+
(await runner.confirm("Modified AlphaFox Skills will be backed up and replaced. Continue?"));
|
|
100
|
+
if (!confirmed) {
|
|
101
|
+
throw Object.assign(new Error("Replacing modified Skills requires --force --yes"), {
|
|
102
|
+
type: "confirmation",
|
|
103
|
+
subtype: "skills_force_confirmation_required",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const result = await syncCurrentSkills({ force: parsed.force, dryRun: flags.dryRun }, env, { ...deps, runner });
|
|
108
|
+
(0, envelope_1.writeSuccess)(result, { format: flags.format, jq: flags.jq });
|
|
109
|
+
return result.blocked.length > 0 ? 1 : 0;
|
|
110
|
+
}
|
|
111
|
+
function parseSkillsArgs(args) {
|
|
112
|
+
const actionToken = args[0] ?? "status";
|
|
113
|
+
if (actionToken === "help" ||
|
|
114
|
+
actionToken === "--help" ||
|
|
115
|
+
actionToken === "-h") {
|
|
116
|
+
return { action: "help", force: false };
|
|
117
|
+
}
|
|
118
|
+
if (actionToken !== "status" && actionToken !== "sync") {
|
|
119
|
+
throw usageError(`Unknown skills action: ${actionToken}`);
|
|
120
|
+
}
|
|
121
|
+
let force = false;
|
|
122
|
+
for (const arg of args.slice(1)) {
|
|
123
|
+
if (arg === "--force")
|
|
124
|
+
force = true;
|
|
125
|
+
else if (arg === "--help" || arg === "-h") {
|
|
126
|
+
return { action: "help", force: false };
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
throw usageError(`Unknown skills argument: ${arg}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (actionToken === "status" && force) {
|
|
133
|
+
throw usageError("--force is only valid with skills sync");
|
|
134
|
+
}
|
|
135
|
+
return { action: actionToken, force };
|
|
136
|
+
}
|
|
137
|
+
function usageError(message) {
|
|
138
|
+
return Object.assign(new Error(message), {
|
|
139
|
+
type: "usage",
|
|
140
|
+
subtype: "invalid_skills_args",
|
|
141
|
+
status: 64,
|
|
142
|
+
});
|
|
143
|
+
}
|