@foxian/esl 0.1.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/LICENSE +21 -0
- package/dist/api-error.d.ts +13 -0
- package/dist/api-error.js +31 -0
- package/dist/bin/esl.d.ts +35 -0
- package/dist/bin/esl.js +1033 -0
- package/dist/builtin/builtin-packages.json +11 -0
- package/dist/builtin/esl-operator/SKILL.md +65 -0
- package/dist/builtin/esl-operator/references/agent-interaction.md +110 -0
- package/dist/builtin/esl-operator/references/author.md +98 -0
- package/dist/builtin/esl-operator/references/consumer.md +109 -0
- package/dist/builtin/esl-operator/references/setup.md +41 -0
- package/dist/builtin/esl-operator/skill.json +7 -0
- package/dist/builtin-dir.d.ts +1 -0
- package/dist/builtin-dir.js +11 -0
- package/dist/commands/adapt.d.ts +8 -0
- package/dist/commands/adapt.js +27 -0
- package/dist/commands/admin.d.ts +11 -0
- package/dist/commands/admin.js +96 -0
- package/dist/commands/clone.d.ts +10 -0
- package/dist/commands/clone.js +21 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/commands/config.js +19 -0
- package/dist/commands/delete.d.ts +13 -0
- package/dist/commands/delete.js +51 -0
- package/dist/commands/deprecate.d.ts +16 -0
- package/dist/commands/deprecate.js +29 -0
- package/dist/commands/import.d.ts +14 -0
- package/dist/commands/import.js +21 -0
- package/dist/commands/info.d.ts +28 -0
- package/dist/commands/info.js +69 -0
- package/dist/commands/init.d.ts +28 -0
- package/dist/commands/init.js +252 -0
- package/dist/commands/install.d.ts +17 -0
- package/dist/commands/install.js +435 -0
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.js +170 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.js +7 -0
- package/dist/commands/login.d.ts +20 -0
- package/dist/commands/login.js +132 -0
- package/dist/commands/logout.d.ts +6 -0
- package/dist/commands/logout.js +18 -0
- package/dist/commands/network-options.d.ts +27 -0
- package/dist/commands/network-options.js +133 -0
- package/dist/commands/notes.d.ts +10 -0
- package/dist/commands/notes.js +24 -0
- package/dist/commands/publish.d.ts +19 -0
- package/dist/commands/publish.js +271 -0
- package/dist/commands/release-delete.d.ts +21 -0
- package/dist/commands/release-delete.js +34 -0
- package/dist/commands/release-manifest.d.ts +5 -0
- package/dist/commands/release-manifest.js +23 -0
- package/dist/commands/rename.d.ts +5 -0
- package/dist/commands/rename.js +25 -0
- package/dist/commands/repair-tag.d.ts +5 -0
- package/dist/commands/repair-tag.js +14 -0
- package/dist/commands/reset-source.d.ts +16 -0
- package/dist/commands/reset-source.js +71 -0
- package/dist/commands/resolve-unlink-identity.d.ts +14 -0
- package/dist/commands/resolve-unlink-identity.js +47 -0
- package/dist/commands/search.d.ts +19 -0
- package/dist/commands/search.js +33 -0
- package/dist/commands/share.d.ts +17 -0
- package/dist/commands/share.js +56 -0
- package/dist/commands/source.d.ts +10 -0
- package/dist/commands/source.js +34 -0
- package/dist/commands/status.d.ts +16 -0
- package/dist/commands/status.js +77 -0
- package/dist/commands/sync-builtin.d.ts +10 -0
- package/dist/commands/sync-builtin.js +68 -0
- package/dist/commands/tools.d.ts +19 -0
- package/dist/commands/tools.js +73 -0
- package/dist/commands/uninstall.d.ts +19 -0
- package/dist/commands/uninstall.js +75 -0
- package/dist/commands/unlink.d.ts +14 -0
- package/dist/commands/unlink.js +108 -0
- package/dist/commands/update.d.ts +18 -0
- package/dist/commands/update.js +176 -0
- package/dist/commands/upload.d.ts +27 -0
- package/dist/commands/upload.js +446 -0
- package/dist/commands/use.d.ts +9 -0
- package/dist/commands/use.js +51 -0
- package/dist/commands/validate.d.ts +4 -0
- package/dist/commands/validate.js +8 -0
- package/dist/commands/version.d.ts +16 -0
- package/dist/commands/version.js +147 -0
- package/dist/commands/whoami.d.ts +14 -0
- package/dist/commands/whoami.js +54 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/output.d.ts +1 -0
- package/dist/output.js +3 -0
- package/dist/postinstall.d.ts +1 -0
- package/dist/postinstall.js +8 -0
- package/dist/prompt.d.ts +9 -0
- package/dist/prompt.js +24 -0
- package/dist/vendor/core/adapt/adapt-engine.d.ts +24 -0
- package/dist/vendor/core/adapt/adapt-engine.js +99 -0
- package/dist/vendor/core/adapt/adapted-skill-copy.d.ts +2 -0
- package/dist/vendor/core/adapt/adapted-skill-copy.js +17 -0
- package/dist/vendor/core/adapt/claude-adapter.d.ts +8 -0
- package/dist/vendor/core/adapt/claude-adapter.js +19 -0
- package/dist/vendor/core/adapt/codex-adapter.d.ts +8 -0
- package/dist/vendor/core/adapt/codex-adapter.js +19 -0
- package/dist/vendor/core/adapt/index.d.ts +2 -0
- package/dist/vendor/core/adapt/index.js +2 -0
- package/dist/vendor/core/adapt/tool-adapter.d.ts +12 -0
- package/dist/vendor/core/adapt/tool-adapter.js +1 -0
- package/dist/vendor/core/adapt/trae-adapter.d.ts +8 -0
- package/dist/vendor/core/adapt/trae-adapter.js +19 -0
- package/dist/vendor/core/adapt/trae-cn-adapter.d.ts +8 -0
- package/dist/vendor/core/adapt/trae-cn-adapter.js +19 -0
- package/dist/vendor/core/agent-interaction.d.ts +58 -0
- package/dist/vendor/core/agent-interaction.js +58 -0
- package/dist/vendor/core/command-params.d.ts +5 -0
- package/dist/vendor/core/command-params.js +46 -0
- package/dist/vendor/core/index.d.ts +22 -0
- package/dist/vendor/core/index.js +22 -0
- package/dist/vendor/core/link/skill-links.d.ts +38 -0
- package/dist/vendor/core/link/skill-links.js +245 -0
- package/dist/vendor/core/link/tool-links.d.ts +83 -0
- package/dist/vendor/core/link/tool-links.js +376 -0
- package/dist/vendor/core/org/account-policy.d.ts +8 -0
- package/dist/vendor/core/org/account-policy.js +57 -0
- package/dist/vendor/core/org/org-name.d.ts +2 -0
- package/dist/vendor/core/org/org-name.js +22 -0
- package/dist/vendor/core/org/standing-teams.d.ts +10 -0
- package/dist/vendor/core/org/standing-teams.js +27 -0
- package/dist/vendor/core/schema/compatibility.d.ts +17 -0
- package/dist/vendor/core/schema/compatibility.js +45 -0
- package/dist/vendor/core/schema/release-manifest.d.ts +88 -0
- package/dist/vendor/core/schema/release-manifest.js +161 -0
- package/dist/vendor/core/schema/skill-json.d.ts +61 -0
- package/dist/vendor/core/schema/skill-json.js +47 -0
- package/dist/vendor/core/schema/validation-result.d.ts +7 -0
- package/dist/vendor/core/schema/validation-result.js +1 -0
- package/dist/vendor/core/skill/builtin-package.d.ts +32 -0
- package/dist/vendor/core/skill/builtin-package.js +143 -0
- package/dist/vendor/core/skill/directory-validator.d.ts +9 -0
- package/dist/vendor/core/skill/directory-validator.js +72 -0
- package/dist/vendor/core/skill/import-skill.d.ts +10 -0
- package/dist/vendor/core/skill/import-skill.js +68 -0
- package/dist/vendor/core/skill/published-package.d.ts +1 -0
- package/dist/vendor/core/skill/published-package.js +9 -0
- package/dist/vendor/core/skill/skill-md.d.ts +15 -0
- package/dist/vendor/core/skill/skill-md.js +32 -0
- package/dist/vendor/core/store/file-copy.d.ts +3 -0
- package/dist/vendor/core/store/file-copy.js +102 -0
- package/dist/vendor/core/store/local-store.d.ts +43 -0
- package/dist/vendor/core/store/local-store.js +104 -0
- package/dist/vendor/core/store/skill-store.d.ts +24 -0
- package/dist/vendor/core/store/skill-store.js +41 -0
- package/dist/vendor/core/store/skills-json.d.ts +34 -0
- package/dist/vendor/core/store/skills-json.js +146 -0
- package/dist/vendor/core/version/select-version.d.ts +16 -0
- package/dist/vendor/core/version/select-version.js +34 -0
- package/dist/vendor/core/version/version.d.ts +2 -0
- package/dist/vendor/core/version/version.js +25 -0
- package/dist/vendor/i18n/api-errors.d.ts +121 -0
- package/dist/vendor/i18n/api-errors.js +240 -0
- package/dist/vendor/i18n/i18n-runtime.d.ts +4 -0
- package/dist/vendor/i18n/i18n-runtime.js +16 -0
- package/dist/vendor/i18n/index.d.ts +5 -0
- package/dist/vendor/i18n/index.js +5 -0
- package/dist/vendor/i18n/locales.d.ts +9 -0
- package/dist/vendor/i18n/locales.js +33 -0
- package/dist/vendor/i18n/resources.d.ts +11 -0
- package/dist/vendor/i18n/resources.js +925 -0
- package/dist/vendor/i18n/translate-api-error.d.ts +8 -0
- package/dist/vendor/i18n/translate-api-error.js +12 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +24 -0
- package/package.json +46 -0
package/dist/bin/esl.js
ADDED
|
@@ -0,0 +1,1033 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
import { checkbox, confirm, input, select } from '@inquirer/prompts';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { readHidden, readStdinText, readText, isInteractive } from '../prompt.js';
|
|
8
|
+
import { AgentInteractionRequiredError, loadConfig, SUPPORTED_TOOLS, assertNoDuplicateCommandParams, createAgentInteractionRequest, parseCommandParams, readOptionalStringArrayParam, readOptionalStringParam, resolveToolName, toAskUserQuestionPayload } from '../vendor/core/index.js';
|
|
9
|
+
import { DEFAULT_LOCALE, resolveLocale, SUPPORTED_LOCALES, translateApiError } from '../vendor/i18n/index.js';
|
|
10
|
+
import { executeInfo, formatSkillInfo } from '../commands/info.js';
|
|
11
|
+
import { executeChangeOwnPassword } from '../commands/admin.js';
|
|
12
|
+
import { executeAdapt, formatAdaptResults } from '../commands/adapt.js';
|
|
13
|
+
import { executeSource } from '../commands/source.js';
|
|
14
|
+
import { executeList } from '../commands/list.js';
|
|
15
|
+
import { executeUse } from '../commands/use.js';
|
|
16
|
+
import { executeInit } from '../commands/init.js';
|
|
17
|
+
import { executeInstall, resolveDefaultInstallTools } from '../commands/install.js';
|
|
18
|
+
import { executeLink } from '../commands/link.js';
|
|
19
|
+
import { executeUnlink } from '../commands/unlink.js';
|
|
20
|
+
import { executeLogin } from '../commands/login.js';
|
|
21
|
+
import { executeLogout, formatLogout } from '../commands/logout.js';
|
|
22
|
+
import { executeSetServer } from '../commands/config.js';
|
|
23
|
+
import { executeWhoami, formatWhoami } from '../commands/whoami.js';
|
|
24
|
+
import { executePublish } from '../commands/publish.js';
|
|
25
|
+
import { executeDeprecate } from '../commands/deprecate.js';
|
|
26
|
+
import { executeReleaseDelete } from '../commands/release-delete.js';
|
|
27
|
+
import { executeUpload } from '../commands/upload.js';
|
|
28
|
+
import { executeResetSource } from '../commands/reset-source.js';
|
|
29
|
+
import { executeStatus } from '../commands/status.js';
|
|
30
|
+
import { executeRename } from '../commands/rename.js';
|
|
31
|
+
import { executeNotes } from '../commands/notes.js';
|
|
32
|
+
import { executeRepairTag } from '../commands/repair-tag.js';
|
|
33
|
+
import { executeSearch } from '../commands/search.js';
|
|
34
|
+
import { executeShare } from '../commands/share.js';
|
|
35
|
+
import { executeUpdate } from '../commands/update.js';
|
|
36
|
+
import { executeUninstall } from '../commands/uninstall.js';
|
|
37
|
+
import { executeToolsList, executeToolsRemove, formatToolsList, parseToolsOption } from '../commands/tools.js';
|
|
38
|
+
import { executeValidate } from '../commands/validate.js';
|
|
39
|
+
import { executeVersion, inspectVersion, isValidExplicitVersion, nextVersion } from '../commands/version.js';
|
|
40
|
+
import { readCliVersion } from '../version.js';
|
|
41
|
+
import { retryPendingGlobalSync } from '../commands/sync-builtin.js';
|
|
42
|
+
function example(text) {
|
|
43
|
+
return `\nExample:\n ${text}\n`;
|
|
44
|
+
}
|
|
45
|
+
const SUPPORTED_AGENT_TOOL_NAMES = [
|
|
46
|
+
'claude-code (alias: claude)',
|
|
47
|
+
...SUPPORTED_TOOLS.filter((tool) => tool !== 'claude')
|
|
48
|
+
].join(', ');
|
|
49
|
+
export async function promptToolSelection(selectTools = checkbox) {
|
|
50
|
+
const selected = await selectTools({
|
|
51
|
+
message: 'Select AI tools',
|
|
52
|
+
choices: SUPPORTED_TOOLS.map((tool) => ({ name: tool, value: tool })),
|
|
53
|
+
required: true
|
|
54
|
+
});
|
|
55
|
+
if (selected.length === 0) {
|
|
56
|
+
throw new Error('No tools selected');
|
|
57
|
+
}
|
|
58
|
+
return selected;
|
|
59
|
+
}
|
|
60
|
+
export async function promptVersionSelection(inspection, selectVersion = select) {
|
|
61
|
+
const choices = inspection.currentVersion
|
|
62
|
+
? [
|
|
63
|
+
{
|
|
64
|
+
name: 'patch',
|
|
65
|
+
value: 'patch',
|
|
66
|
+
description: `${nextVersion(inspection.currentVersion, 'patch')} — 修复缺陷`
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: 'minor',
|
|
70
|
+
value: 'minor',
|
|
71
|
+
description: `${nextVersion(inspection.currentVersion, 'minor')} — 兼容的新能力`
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'major',
|
|
75
|
+
value: 'major',
|
|
76
|
+
description: `${nextVersion(inspection.currentVersion, 'major')} — 破坏性变更`
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'custom',
|
|
80
|
+
value: 'custom',
|
|
81
|
+
description: '输入明确的 SemVer(例如 1.4.2)'
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
: [
|
|
85
|
+
{
|
|
86
|
+
name: 'custom',
|
|
87
|
+
value: 'custom',
|
|
88
|
+
description: '输入明确的 SemVer(例如 1.4.2)'
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
return selectVersion({
|
|
92
|
+
message: 'Select release type',
|
|
93
|
+
choices,
|
|
94
|
+
default: inspection.currentVersion ? 'patch' : 'custom'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// 可安装技能发现面(ADR-0049):search TTY 会话的两步选择。
|
|
98
|
+
// 列表中「Adjust filters…」视觉置顶,default 高亮第一条技能;末项 Exit。
|
|
99
|
+
const SEARCH_ADJUST = '__adjust__';
|
|
100
|
+
const SEARCH_EXIT = '__exit__';
|
|
101
|
+
function searchChoiceLabel(skill) {
|
|
102
|
+
const parts = [skill.name];
|
|
103
|
+
if (skill.displayName && skill.displayName !== skill.skillName)
|
|
104
|
+
parts.push(skill.displayName);
|
|
105
|
+
if (skill.latestStableVersion)
|
|
106
|
+
parts.push(`v${skill.latestStableVersion}`);
|
|
107
|
+
if (skill.visibility)
|
|
108
|
+
parts.push(`(${skill.visibility})`);
|
|
109
|
+
return parts.join(' ');
|
|
110
|
+
}
|
|
111
|
+
function installCommandFor(skillName, server) {
|
|
112
|
+
return `esl install ${skillName}${server ? ` --server ${server}` : ''}`;
|
|
113
|
+
}
|
|
114
|
+
function printSearchDetails(skill, server) {
|
|
115
|
+
console.log(`\n${skill.name}`);
|
|
116
|
+
if (skill.displayName && skill.displayName !== skill.skillName) {
|
|
117
|
+
console.log(`Display name: ${skill.displayName}`);
|
|
118
|
+
}
|
|
119
|
+
console.log(`Description: ${skill.description}`);
|
|
120
|
+
if (skill.latestStableVersion)
|
|
121
|
+
console.log(`Latest stable version: v${skill.latestStableVersion}`);
|
|
122
|
+
if (skill.visibility)
|
|
123
|
+
console.log(`Visibility: ${skill.visibility}`);
|
|
124
|
+
console.log(`More: ${installCommandFor(skill.name, server)} (or "esl info ${skill.name}")`);
|
|
125
|
+
}
|
|
126
|
+
// 调整筛选:query / namespace / keyword / visibility 循环修改,Done 返回列表。
|
|
127
|
+
export async function adjustSearchFilters(filters, selectPrompt = select, inputPrompt = input) {
|
|
128
|
+
const next = { ...filters };
|
|
129
|
+
for (;;) {
|
|
130
|
+
const field = await selectPrompt({
|
|
131
|
+
message: 'Adjust filters',
|
|
132
|
+
choices: [
|
|
133
|
+
{ name: `Query: ${next.query ?? '(browse all)'}`, value: 'query' },
|
|
134
|
+
{ name: `Namespace: ${next.namespace ?? '(all)'}`, value: 'namespace' },
|
|
135
|
+
{ name: `Keyword: ${next.keyword ?? '(all)'}`, value: 'keyword' },
|
|
136
|
+
{ name: `Visibility: ${next.visibility ?? '(all)'}`, value: 'visibility' },
|
|
137
|
+
{ name: 'Done', value: 'done' }
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
if (field === 'done')
|
|
141
|
+
return next;
|
|
142
|
+
if (field === 'query') {
|
|
143
|
+
const value = await inputPrompt({ message: 'Search query (empty to browse all)' });
|
|
144
|
+
next.query = value.trim() || undefined;
|
|
145
|
+
}
|
|
146
|
+
else if (field === 'namespace') {
|
|
147
|
+
const value = await inputPrompt({ message: 'Namespace (empty for all)' });
|
|
148
|
+
next.namespace = value.trim() || undefined;
|
|
149
|
+
}
|
|
150
|
+
else if (field === 'keyword') {
|
|
151
|
+
const value = await inputPrompt({ message: 'Keyword (empty for all)' });
|
|
152
|
+
next.keyword = value.trim() || undefined;
|
|
153
|
+
}
|
|
154
|
+
else if (field === 'visibility') {
|
|
155
|
+
const value = await selectPrompt({
|
|
156
|
+
message: 'Visibility',
|
|
157
|
+
choices: [
|
|
158
|
+
{ name: 'All', value: 'all' },
|
|
159
|
+
{ name: 'Public', value: 'public' },
|
|
160
|
+
{ name: 'Private', value: 'private' }
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
next.visibility = value === 'all' ? undefined : value;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export async function runSearchSession(program, initialFilters, deps = {}) {
|
|
168
|
+
const selectPrompt = deps.select ?? select;
|
|
169
|
+
const confirmPrompt = deps.confirm ?? confirm;
|
|
170
|
+
const inputPrompt = deps.input ?? input;
|
|
171
|
+
let filters = { ...initialFilters };
|
|
172
|
+
const server = filters.server;
|
|
173
|
+
const search = deps.search ?? ((filters) => executeSearch(filters.query, filters));
|
|
174
|
+
const install = deps.install ?? ((nameOrPath) => installSkill(program, nameOrPath, { server }));
|
|
175
|
+
for (;;) {
|
|
176
|
+
const results = await search(filters);
|
|
177
|
+
if (results.length === 0) {
|
|
178
|
+
// 0 条结果不进入空技能列表,直接提供调整筛选或退出。
|
|
179
|
+
const action = await selectPrompt({
|
|
180
|
+
message: 'No skills match the current filters',
|
|
181
|
+
choices: [
|
|
182
|
+
{ name: 'Adjust filters…', value: SEARCH_ADJUST },
|
|
183
|
+
{ name: 'Exit', value: SEARCH_EXIT }
|
|
184
|
+
]
|
|
185
|
+
});
|
|
186
|
+
if (action === SEARCH_EXIT)
|
|
187
|
+
return;
|
|
188
|
+
filters = await adjustSearchFilters(filters, selectPrompt, inputPrompt);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// 同一次筛选结果在浏览、详情和确认拒绝之间复用,避免重复请求。
|
|
192
|
+
for (;;) {
|
|
193
|
+
const chosen = await selectPrompt({
|
|
194
|
+
message: `Select a skill (${results.length} found)`,
|
|
195
|
+
default: results[0].name,
|
|
196
|
+
choices: [
|
|
197
|
+
{ name: 'Adjust filters…', value: SEARCH_ADJUST },
|
|
198
|
+
...results.map((skill) => ({
|
|
199
|
+
name: searchChoiceLabel(skill),
|
|
200
|
+
value: skill.name,
|
|
201
|
+
description: skill.description
|
|
202
|
+
})),
|
|
203
|
+
{ name: 'Exit', value: SEARCH_EXIT }
|
|
204
|
+
]
|
|
205
|
+
});
|
|
206
|
+
if (chosen === SEARCH_EXIT)
|
|
207
|
+
return;
|
|
208
|
+
if (chosen === SEARCH_ADJUST)
|
|
209
|
+
break;
|
|
210
|
+
const skill = results.find((entry) => entry.name === chosen);
|
|
211
|
+
const action = await selectPrompt({
|
|
212
|
+
message: chosen,
|
|
213
|
+
choices: [
|
|
214
|
+
{ name: 'View details', value: 'details' },
|
|
215
|
+
{ name: 'Install', value: 'install' },
|
|
216
|
+
{ name: 'Back to list', value: 'back' }
|
|
217
|
+
]
|
|
218
|
+
});
|
|
219
|
+
if (action === 'details') {
|
|
220
|
+
printSearchDetails(skill, server);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (action === 'back')
|
|
224
|
+
continue;
|
|
225
|
+
// 安装前回显完整命令并确认;拒绝则回到列表继续浏览。
|
|
226
|
+
console.log(`\n$ ${installCommandFor(chosen, server)}`);
|
|
227
|
+
const proceed = await confirmPrompt({ message: 'Run this install command?', default: false });
|
|
228
|
+
if (!proceed)
|
|
229
|
+
continue;
|
|
230
|
+
await install(chosen);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// install 命令主体:search TTY 会话确认后也走同一条安装路径(含 tools 选择)。
|
|
236
|
+
export async function installSkill(program, nameOrPath, options) {
|
|
237
|
+
const skipToolLinks = options.tools === false || options.adapt === false;
|
|
238
|
+
let tools = skipToolLinks ? [] : parseToolsOption(options.tools);
|
|
239
|
+
if (!skipToolLinks && tools.length === 0) {
|
|
240
|
+
const configured = await resolveDefaultInstallTools(process.cwd(), {
|
|
241
|
+
...options,
|
|
242
|
+
tools: undefined,
|
|
243
|
+
global: options.global
|
|
244
|
+
});
|
|
245
|
+
if (configured.length === 0) {
|
|
246
|
+
if (program.opts().input === false || !isInteractive()) {
|
|
247
|
+
throw new Error('No tools configured; pass --tools or run interactively');
|
|
248
|
+
}
|
|
249
|
+
tools = await promptToolSelection();
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
tools = configured;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const targetDir = await executeInstall(nameOrPath, {
|
|
256
|
+
...options,
|
|
257
|
+
tools,
|
|
258
|
+
noAdapt: skipToolLinks
|
|
259
|
+
});
|
|
260
|
+
console.log(`Skill installed at ${targetDir}`);
|
|
261
|
+
}
|
|
262
|
+
async function promptCustomVersion() {
|
|
263
|
+
while (true) {
|
|
264
|
+
const version = await readText('Version (SemVer)');
|
|
265
|
+
if (isValidExplicitVersion(version)) {
|
|
266
|
+
return version;
|
|
267
|
+
}
|
|
268
|
+
console.error('Version must be valid SemVer (e.g. 1.2.3).');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function versionAgentFields(inspection) {
|
|
272
|
+
if (!inspection.currentVersion) {
|
|
273
|
+
return [
|
|
274
|
+
{
|
|
275
|
+
id: 'release',
|
|
276
|
+
kind: 'text',
|
|
277
|
+
label: 'Version',
|
|
278
|
+
required: true
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
}
|
|
282
|
+
return [
|
|
283
|
+
{
|
|
284
|
+
id: 'release',
|
|
285
|
+
kind: 'select',
|
|
286
|
+
label: 'Release type',
|
|
287
|
+
required: true,
|
|
288
|
+
default: 'patch',
|
|
289
|
+
options: [
|
|
290
|
+
{
|
|
291
|
+
label: 'patch',
|
|
292
|
+
description: `${nextVersion(inspection.currentVersion, 'patch')} — 修复缺陷`
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
label: 'minor',
|
|
296
|
+
description: `${nextVersion(inspection.currentVersion, 'minor')} — 兼容的新能力`
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
label: 'major',
|
|
300
|
+
description: `${nextVersion(inspection.currentVersion, 'major')} — 破坏性变更`
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
/** A bare SemVer in the publish path slot is a leftover `esl publish <version>` call, not a directory. */
|
|
307
|
+
const SEMVER_ARGUMENT_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
308
|
+
const AGENT_INTERACTION_COMMANDS = new Set(['init', 'version']);
|
|
309
|
+
let activeLocale = DEFAULT_LOCALE;
|
|
310
|
+
export function createProgram() {
|
|
311
|
+
const program = new Command();
|
|
312
|
+
program.name('esl').description('Enterprise Skill Library CLI').version(readCliVersion());
|
|
313
|
+
program.option('-d, --debug', 'print stack traces on error');
|
|
314
|
+
program.option('--no-input', 'disable all prompts');
|
|
315
|
+
program.option('--agent-interaction', 'return structured interaction requests instead of prompting');
|
|
316
|
+
program.option('--agent-tool <tool>', 'AI tool invoking the command, e.g. claude-code or codex (requires --agent-interaction)');
|
|
317
|
+
program.option('--locale <locale>', 'Use a temporary locale for this command (zh-CN or en-US)');
|
|
318
|
+
program.option('--params-json <json>', 'pass command parameters as a JSON object');
|
|
319
|
+
program.option('-C, --cd <path>', 'run the command in the given directory first, like npm -C');
|
|
320
|
+
program.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
321
|
+
const options = program.opts();
|
|
322
|
+
if (options.locale !== undefined &&
|
|
323
|
+
!SUPPORTED_LOCALES.includes(options.locale)) {
|
|
324
|
+
throw new Error(`Unsupported locale: ${options.locale}`);
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const config = await loadConfig({});
|
|
328
|
+
activeLocale = resolveLocale({ override: options.locale, accountLocale: config.locale });
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
activeLocale = resolveLocale({ override: options.locale, accountLocale: null });
|
|
332
|
+
}
|
|
333
|
+
if (options.agentTool !== undefined && resolveToolName(options.agentTool) === undefined) {
|
|
334
|
+
throw new Error(`Unknown agent tool: ${options.agentTool}. Supported tools: ${SUPPORTED_AGENT_TOOL_NAMES}`);
|
|
335
|
+
}
|
|
336
|
+
if (options.agentTool !== undefined && options.agentInteraction !== true) {
|
|
337
|
+
throw new Error('--agent-tool requires --agent-interaction');
|
|
338
|
+
}
|
|
339
|
+
if (options.paramsJson !== undefined &&
|
|
340
|
+
!AGENT_INTERACTION_COMMANDS.has(actionCommand.name())) {
|
|
341
|
+
throw new Error(`--params-json is not supported for ${actionCommand.name()}`);
|
|
342
|
+
}
|
|
343
|
+
if (options.agentInteraction === true &&
|
|
344
|
+
!AGENT_INTERACTION_COMMANDS.has(actionCommand.name())) {
|
|
345
|
+
throw new Error(`--agent-interaction is not supported for ${actionCommand.name()}`);
|
|
346
|
+
}
|
|
347
|
+
const cd = options.cd;
|
|
348
|
+
if (typeof cd === 'string' && cd.length > 0) {
|
|
349
|
+
try {
|
|
350
|
+
process.chdir(cd);
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
throw new Error(`Cannot change directory to ${cd}: ${error.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
program
|
|
358
|
+
.command('init')
|
|
359
|
+
.argument('[path]', 'target skill directory (defaults to --cd or the current directory)')
|
|
360
|
+
.option('--name <name>', 'skill short name (defaults to the target directory basename)')
|
|
361
|
+
.option('--namespace <namespace>', 'release.json namespace: personal (default) or an organization')
|
|
362
|
+
.option('--license <spdx>', 'release.json license (default MIT)')
|
|
363
|
+
.option('--description <text>', 'SKILL.md description (asked interactively when omitted)')
|
|
364
|
+
.option('--display-name <text>', 'release.json display name (defaults to a title-cased short name)')
|
|
365
|
+
.option('--keywords <list>', 'comma-separated release.json keywords')
|
|
366
|
+
.addHelpText('after', example('$ esl init ./markdown-master\n $ esl init --name my-skill'))
|
|
367
|
+
.action(async (skillPath, options) => {
|
|
368
|
+
const rawParamsJson = program.opts().paramsJson;
|
|
369
|
+
const params = rawParamsJson
|
|
370
|
+
? parseCommandParams(rawParamsJson, 'init', ['name', 'namespace', 'license', 'description', 'display-name', 'keywords'])
|
|
371
|
+
: {};
|
|
372
|
+
assertNoDuplicateCommandParams(params, {
|
|
373
|
+
name: options.name,
|
|
374
|
+
namespace: options.namespace,
|
|
375
|
+
license: options.license,
|
|
376
|
+
description: options.description,
|
|
377
|
+
'display-name': options.displayName,
|
|
378
|
+
keywords: options.keywords
|
|
379
|
+
}, 'init');
|
|
380
|
+
const name = readOptionalStringParam(params, 'name', 'init') ?? options.name;
|
|
381
|
+
const namespace = readOptionalStringParam(params, 'namespace', 'init') ?? options.namespace;
|
|
382
|
+
const license = readOptionalStringParam(params, 'license', 'init') ?? options.license;
|
|
383
|
+
const description = readOptionalStringParam(params, 'description', 'init') ?? options.description;
|
|
384
|
+
const displayName = readOptionalStringParam(params, 'display-name', 'init') ?? options.displayName;
|
|
385
|
+
const keywords = readOptionalStringArrayParam(params, 'keywords', 'init') ??
|
|
386
|
+
options.keywords
|
|
387
|
+
?.split(',')
|
|
388
|
+
.map((keyword) => keyword.trim())
|
|
389
|
+
.filter((keyword) => keyword.length > 0);
|
|
390
|
+
const targetDir = await executeInit({
|
|
391
|
+
directory: skillPath,
|
|
392
|
+
name,
|
|
393
|
+
namespace,
|
|
394
|
+
license,
|
|
395
|
+
description,
|
|
396
|
+
displayName,
|
|
397
|
+
keywords,
|
|
398
|
+
noInput: program.opts().input === false,
|
|
399
|
+
agentInteraction: program.opts().agentInteraction === true,
|
|
400
|
+
agentTool: program.opts().agentTool === undefined
|
|
401
|
+
? undefined
|
|
402
|
+
: resolveToolName(program.opts().agentTool)
|
|
403
|
+
});
|
|
404
|
+
console.log(`Skill initialized at ${targetDir}`);
|
|
405
|
+
});
|
|
406
|
+
program
|
|
407
|
+
.command('login')
|
|
408
|
+
.option('--username <username>', 'ESL username (defaults to the saved or prompted username)')
|
|
409
|
+
.option('--server <url>', 'ESL Server URL (defaults to the saved server or ESL_SERVER)')
|
|
410
|
+
.option('--password-file <path>', 'Read the ESL password from a file')
|
|
411
|
+
.option('--token-file <path>', 'Read a Skill User Token from a file')
|
|
412
|
+
.addHelpText('after', example('$ esl login --server http://localhost:3000 --username alice'))
|
|
413
|
+
.action(async (options) => {
|
|
414
|
+
const login = await executeLogin({
|
|
415
|
+
...options,
|
|
416
|
+
locale: program.opts().locale,
|
|
417
|
+
noInput: program.opts().input === false,
|
|
418
|
+
readInput: process.stdin.isTTY ? undefined : () => readStdinText(),
|
|
419
|
+
readServer: process.stdin.isTTY ? undefined : () => readStdinText(),
|
|
420
|
+
readUsername: process.stdin.isTTY ? undefined : () => readStdinText()
|
|
421
|
+
});
|
|
422
|
+
// 全局身份登录(ADR-0032):一条凭据走遍个人空间与所有组织
|
|
423
|
+
console.log(`Logged in as ${login.username}`);
|
|
424
|
+
});
|
|
425
|
+
program
|
|
426
|
+
.command('logout')
|
|
427
|
+
.description('Clear the locally stored ESL credentials')
|
|
428
|
+
.addHelpText('after', example('$ esl logout'))
|
|
429
|
+
.action(async () => {
|
|
430
|
+
const result = await executeLogout();
|
|
431
|
+
console.log(formatLogout(result));
|
|
432
|
+
});
|
|
433
|
+
const configCmd = program.command('config').description('Manage ESL client configuration');
|
|
434
|
+
configCmd.addHelpText('after', example('$ esl config set-server http://localhost:3000'));
|
|
435
|
+
configCmd
|
|
436
|
+
.command('set-server')
|
|
437
|
+
.description('Set the ESL Server URL used by all commands')
|
|
438
|
+
.argument('<url>', 'ESL Server URL')
|
|
439
|
+
.addHelpText('after', example('$ esl config set-server http://localhost:3000'))
|
|
440
|
+
.action(async (url) => {
|
|
441
|
+
const { server } = await executeSetServer(url);
|
|
442
|
+
console.log(`Server set to ${server}`);
|
|
443
|
+
});
|
|
444
|
+
program
|
|
445
|
+
.command('whoami')
|
|
446
|
+
.description('Show the current login and login status')
|
|
447
|
+
.addHelpText('after', example('$ esl whoami'))
|
|
448
|
+
.action(async () => {
|
|
449
|
+
const result = await executeWhoami();
|
|
450
|
+
console.log(formatWhoami(result));
|
|
451
|
+
});
|
|
452
|
+
const myAccount = program
|
|
453
|
+
.command('account')
|
|
454
|
+
.description('Manage your own ESL account');
|
|
455
|
+
myAccount.addHelpText('after', example('$ esl account change-password'));
|
|
456
|
+
myAccount
|
|
457
|
+
.command('change-password')
|
|
458
|
+
.description('Change your own ESL password')
|
|
459
|
+
.option('--current-password-file <path>', 'Read the current ESL password from a file')
|
|
460
|
+
.option('--password-file <path>', 'Read the new ESL password from a file')
|
|
461
|
+
.option('--server <url>', 'ESL Server URL')
|
|
462
|
+
.addHelpText('after', example('$ esl account change-password --server http://localhost:3000'))
|
|
463
|
+
.action(async (options) => {
|
|
464
|
+
await executeChangeOwnPassword({
|
|
465
|
+
...options,
|
|
466
|
+
noInput: program.opts().input === false,
|
|
467
|
+
readInput: process.stdin.isTTY ? undefined : () => readStdinText(),
|
|
468
|
+
readPassword: readHidden
|
|
469
|
+
});
|
|
470
|
+
console.log('Password changed');
|
|
471
|
+
});
|
|
472
|
+
program
|
|
473
|
+
.command('search')
|
|
474
|
+
.argument('[query]', 'search query (omit to browse all visible published skills)')
|
|
475
|
+
.option('--server <url>', 'ESL Server URL')
|
|
476
|
+
.option('--namespace <namespace>', 'filter by namespace (org or user)')
|
|
477
|
+
.option('--keyword <keyword>', 'hard filter by skill keyword')
|
|
478
|
+
.addOption(new Option('--visibility <visibility>', 'filter by visibility').choices(['public', 'private']))
|
|
479
|
+
.option('--limit <count>', 'maximum number of results (default 50)')
|
|
480
|
+
.option('--json', 'Output as JSON')
|
|
481
|
+
.addHelpText('after', example('$ esl search code-review\n$ esl search'))
|
|
482
|
+
.action(async (query, options) => {
|
|
483
|
+
let limit;
|
|
484
|
+
if (options.limit !== undefined) {
|
|
485
|
+
limit = Number(options.limit);
|
|
486
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
487
|
+
throw new Error('--limit must be a positive integer');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const filters = {
|
|
491
|
+
namespace: options.namespace,
|
|
492
|
+
keyword: options.keyword,
|
|
493
|
+
visibility: options.visibility,
|
|
494
|
+
limit,
|
|
495
|
+
server: options.server
|
|
496
|
+
};
|
|
497
|
+
// TTY(stdin+stdout 且未 --no-input/--json)才进入两步选择会话。
|
|
498
|
+
if (program.opts().input === false || options.json || !isInteractive()) {
|
|
499
|
+
const results = await executeSearch(query, { ...options, ...filters });
|
|
500
|
+
if (options.json) {
|
|
501
|
+
console.log(JSON.stringify(results, null, 2));
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (results.length === 0) {
|
|
505
|
+
console.log('No skills found.');
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
for (const result of results) {
|
|
509
|
+
console.log(`${result.name}\t${result.displayName ?? result.skillName ?? ''}\t${result.latestStableVersion ?? ''}\t${result.visibility ?? ''}\t${result.description}`);
|
|
510
|
+
}
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
await runSearchSession(program, { ...filters, query });
|
|
514
|
+
});
|
|
515
|
+
program
|
|
516
|
+
.command('info')
|
|
517
|
+
.argument('<skill-name>')
|
|
518
|
+
.option('--server <url>', 'ESL Server URL')
|
|
519
|
+
.option('--json', 'Output as JSON')
|
|
520
|
+
.addHelpText('after', example('$ esl info @cnfox/code-review'))
|
|
521
|
+
.action(async (skillName, options) => {
|
|
522
|
+
const info = await executeInfo(skillName, options);
|
|
523
|
+
if (options.json) {
|
|
524
|
+
console.log(JSON.stringify(info, null, 2));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
console.log(formatSkillInfo(info));
|
|
528
|
+
});
|
|
529
|
+
program
|
|
530
|
+
.command('upload')
|
|
531
|
+
.description('Commit, push and (on first use) register a local skill source')
|
|
532
|
+
.argument('[path]', 'skill directory (defaults to --cd or the current directory)')
|
|
533
|
+
.option('--license <spdx>', 'SPDX license for a missing release.json (default MIT)')
|
|
534
|
+
.option('-m, --message <text>', 'description of this upload, used as the source commit message')
|
|
535
|
+
.option('--confirm-identity <skill-name>', 'confirm the first-upload skill identity for non-interactive use')
|
|
536
|
+
.option('--server <url>', 'ESL Server URL')
|
|
537
|
+
.addHelpText('after', example('$ esl upload ./my-skill --confirm-identity @acme/my-skill'))
|
|
538
|
+
.action(async (skillPath, options) => {
|
|
539
|
+
const uploaded = await executeUpload({
|
|
540
|
+
...options,
|
|
541
|
+
directory: skillPath,
|
|
542
|
+
noInput: program.opts().input === false
|
|
543
|
+
});
|
|
544
|
+
if ('alreadyUpToDate' in uploaded && uploaded.alreadyUpToDate) {
|
|
545
|
+
console.log(`Skill source is already up to date: ${uploaded.name}`);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
console.log(uploaded.skillId
|
|
549
|
+
? `Skill source uploaded: ${uploaded.name} (${uploaded.skillId})`
|
|
550
|
+
: `Skill source synced: ${uploaded.name}`);
|
|
551
|
+
});
|
|
552
|
+
program
|
|
553
|
+
.command('reset-source')
|
|
554
|
+
.description('Detach a skill source directory from its server source (remove the esl remote and back up release.json)')
|
|
555
|
+
.argument('[path]', 'skill directory (defaults to --cd or the current directory)')
|
|
556
|
+
.option('--server <url>', 'ESL Server URL')
|
|
557
|
+
.option('-f, --force', 'reset without confirmation')
|
|
558
|
+
.addHelpText('after', example('$ esl reset-source ./my-skill --force'))
|
|
559
|
+
.action(async (skillPath, options) => {
|
|
560
|
+
const result = await executeResetSource({
|
|
561
|
+
directory: skillPath,
|
|
562
|
+
server: options.server,
|
|
563
|
+
force: options.force,
|
|
564
|
+
noInput: program.opts().input === false
|
|
565
|
+
});
|
|
566
|
+
console.log(`Source link reset in ${result.directory}; the directory is now a plain local skill source`);
|
|
567
|
+
console.log('To register it as a fresh server source, run "esl upload" from the directory');
|
|
568
|
+
});
|
|
569
|
+
program
|
|
570
|
+
.command('status')
|
|
571
|
+
.description('Show the state of the local skill source vs the server')
|
|
572
|
+
.argument('[path]', 'skill directory (defaults to --cd or the current directory)')
|
|
573
|
+
.addHelpText('after', example('$ esl status'))
|
|
574
|
+
.action(async (skillPath) => {
|
|
575
|
+
const status = await executeStatus({ directory: skillPath });
|
|
576
|
+
if (!status.serverHosted) {
|
|
577
|
+
console.log('Not yet a server-hosted skill source; run "esl upload ." to register it');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const lines = [
|
|
581
|
+
status.clean ? 'Working tree: clean' : 'Working tree: has uncommitted changes',
|
|
582
|
+
`Local ahead of server: ${status.ahead} commit(s) not pushed`,
|
|
583
|
+
`Local behind server: ${status.behind} commit(s)`
|
|
584
|
+
];
|
|
585
|
+
if (status.lastCommit) {
|
|
586
|
+
lines.push(`Last commit: ${status.lastCommit}`);
|
|
587
|
+
}
|
|
588
|
+
console.log(lines.join('\n'));
|
|
589
|
+
});
|
|
590
|
+
program
|
|
591
|
+
.command('rename')
|
|
592
|
+
.description('Rename a server-hosted skill')
|
|
593
|
+
.argument('<skill-name>')
|
|
594
|
+
.argument('<new-name>')
|
|
595
|
+
.option('--server <url>', 'ESL Server URL')
|
|
596
|
+
.addHelpText('after', example('$ esl rename @platform-ai/reviewer reviewer-pro'))
|
|
597
|
+
.action(async (identity, newName, options) => {
|
|
598
|
+
const renamed = await executeRename(identity, { ...options, newName });
|
|
599
|
+
console.log(`Skill renamed: ${renamed.name ?? newName}`);
|
|
600
|
+
});
|
|
601
|
+
program
|
|
602
|
+
.command('share')
|
|
603
|
+
.description('Share a skill with your organization, a team, or a member')
|
|
604
|
+
.argument('<skill-name>')
|
|
605
|
+
.option('--all', 'share with the whole organization (read; add --write for edit)')
|
|
606
|
+
.option('--team <name>', 'share with a team (read; add --write or --manage)')
|
|
607
|
+
.option('--user <username>', 'share with a member (read; add --write for edit; add --manage for co-management)')
|
|
608
|
+
.option('--write', 'grant edit (write) permission where applicable')
|
|
609
|
+
.option('--manage', 'grant manage permission (share, publish, and grant others)')
|
|
610
|
+
.option('--reset', 'reset to private (only you keep access)')
|
|
611
|
+
.option('--server <url>', 'ESL Server URL')
|
|
612
|
+
.addHelpText('after', example('$ esl share @acme/code-review --all'))
|
|
613
|
+
.action(async (identity, options) => {
|
|
614
|
+
await executeShare(identity, options);
|
|
615
|
+
console.log('Skill sharing updated');
|
|
616
|
+
});
|
|
617
|
+
program
|
|
618
|
+
.command('repair-tag')
|
|
619
|
+
.description('Repair a missing release tag')
|
|
620
|
+
.argument('<skill-name>')
|
|
621
|
+
.argument('<version>')
|
|
622
|
+
.option('--server <url>', 'ESL Server URL')
|
|
623
|
+
.addHelpText('after', example('$ esl repair-tag @platform-ai/reviewer 1.0.0'))
|
|
624
|
+
.action(async (identity, version, options) => {
|
|
625
|
+
const repaired = await executeRepairTag(identity, { ...options, version });
|
|
626
|
+
console.log(`Release tag repaired: ${repaired.tag ?? `v${version}`}`);
|
|
627
|
+
});
|
|
628
|
+
program
|
|
629
|
+
.command('publish')
|
|
630
|
+
.argument('[path]', 'skill directory (defaults to --cd or the current directory)')
|
|
631
|
+
.option('--server <url>', 'ESL Server URL')
|
|
632
|
+
.option('--visibility <visibility>', 'public or private')
|
|
633
|
+
.option('--license <spdx>', 'SPDX license for a missing release.json (default MIT)')
|
|
634
|
+
.option('-m, --message <text>', 'release notes; defaults to the commits since the last release tag')
|
|
635
|
+
.option('-f, --force', 'publish without confirmation')
|
|
636
|
+
.option('--dry-run', 'validate and preview the release without touching the server')
|
|
637
|
+
.addHelpText('after', example('$ esl publish ./my-skill -m "fix: dead-link regex"'))
|
|
638
|
+
.action(async (skillPath, options) => {
|
|
639
|
+
if (skillPath && SEMVER_ARGUMENT_PATTERN.test(skillPath)) {
|
|
640
|
+
console.error(`esl publish no longer takes a version argument (got ${skillPath}). Run \`esl version <release>\` to set the version, then run esl publish.`);
|
|
641
|
+
process.exitCode = 1;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
await executePublish({
|
|
645
|
+
...options,
|
|
646
|
+
directory: skillPath,
|
|
647
|
+
noInput: program.opts().input === false
|
|
648
|
+
});
|
|
649
|
+
if (options.dryRun) {
|
|
650
|
+
console.log('Dry run complete — no release was created.');
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
console.log('Skill published');
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
program
|
|
657
|
+
.command('deprecate')
|
|
658
|
+
.description('Mark a published version as deprecated, or clear the mark')
|
|
659
|
+
.argument('<skill-name>', 'scoped skill name, e.g. @acme/code-review')
|
|
660
|
+
.argument('<version>', 'published version to deprecate')
|
|
661
|
+
.option('-m, --message <text>', 'warning shown to anyone installing this version; empty clears the mark', '')
|
|
662
|
+
.option('--server <url>', 'ESL Server URL')
|
|
663
|
+
.addHelpText('after', example('$ esl deprecate @acme/code-review 1.2.0 -m "Use 1.3.0 instead"'))
|
|
664
|
+
.action(async (name, version, options) => {
|
|
665
|
+
await executeDeprecate(name, version, options);
|
|
666
|
+
console.log(options.message.trim()
|
|
667
|
+
? `Marked ${name}@${version} as deprecated`
|
|
668
|
+
: `Cleared the deprecation mark on ${name}@${version}`);
|
|
669
|
+
});
|
|
670
|
+
program
|
|
671
|
+
.command('release-delete')
|
|
672
|
+
.description('Delete a single published version (the version number is burned)')
|
|
673
|
+
.argument('<skill-name>', 'scoped skill name, e.g. @acme/code-review')
|
|
674
|
+
.argument('<version>', 'published version to delete')
|
|
675
|
+
.requiredOption('--confirm <version>', 'echo the version to confirm the deletion')
|
|
676
|
+
.option('--force', 'override the dependency-pinning guard (platform administrator only)')
|
|
677
|
+
.option('--server <url>', 'ESL Server URL')
|
|
678
|
+
.addHelpText('after', example('$ esl release-delete @acme/code-review 1.0.0 --confirm 1.0.0'))
|
|
679
|
+
.action(async (name, version, options) => {
|
|
680
|
+
const result = await executeReleaseDelete(name, version, options);
|
|
681
|
+
const dependents = result.dependents?.length ? ` (was required by ${result.dependents.join(', ')})` : '';
|
|
682
|
+
console.log(`Deleted ${name}@${version}${dependents}`);
|
|
683
|
+
});
|
|
684
|
+
program
|
|
685
|
+
.command('notes')
|
|
686
|
+
.description('Update the release notes of a published version')
|
|
687
|
+
.argument('<skill-name>')
|
|
688
|
+
.argument('<version>')
|
|
689
|
+
.requiredOption('-m, --message <text>', 'new release notes')
|
|
690
|
+
.option('--server <url>', 'ESL Server URL')
|
|
691
|
+
.addHelpText('after', example('$ esl notes @platform-ai/reviewer 1.1.0 -m "Revised notes"'))
|
|
692
|
+
.action(async (identity, version, options) => {
|
|
693
|
+
const updated = await executeNotes(identity, version, options);
|
|
694
|
+
console.log(`Release notes updated: ${updated.skillName} ${updated.version}`);
|
|
695
|
+
});
|
|
696
|
+
program
|
|
697
|
+
.command('install')
|
|
698
|
+
.argument('[name-or-path]', 'skill name (@namespace/skill) or local path')
|
|
699
|
+
.option('--version <version>', 'version to install')
|
|
700
|
+
.option('-g, --global', 'Install to global skills directory')
|
|
701
|
+
.option('--tools <tools>', 'AI tools to link, comma-separated or all')
|
|
702
|
+
.option('--no-tools', 'Install the skill source without creating tool links')
|
|
703
|
+
.option('-f, --force', 'Replace ESL-owned stale links')
|
|
704
|
+
.option('--ignore-compatibility', 'Install incompatible published packages')
|
|
705
|
+
.option('--no-adapt', 'Skip automatic tool links after install')
|
|
706
|
+
.option('--server <url>', 'ESL Server URL')
|
|
707
|
+
.addHelpText('after', example('$ esl install @cnfox/code-review --tools claude-code,codex'))
|
|
708
|
+
.action(async (nameOrPath, options) => {
|
|
709
|
+
if (!nameOrPath) {
|
|
710
|
+
console.log('Restoring skills from the ESL install manifest...');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
await installSkill(program, nameOrPath, options);
|
|
714
|
+
});
|
|
715
|
+
program
|
|
716
|
+
.command('link')
|
|
717
|
+
.description('Link a local skill directory into the store (symlink, like npm link)')
|
|
718
|
+
.argument('[path]', 'local skill directory (defaults to --cd or the current directory)')
|
|
719
|
+
.option('-g, --global', 'Link to global skills directory')
|
|
720
|
+
.option('--tools <tools>', 'AI tools to link, comma-separated or all')
|
|
721
|
+
.option('--no-tools', 'Link the skill source without creating tool links')
|
|
722
|
+
.option('--identity <identity>', 'Namespace or full identity for a bare release.json name')
|
|
723
|
+
.option('-f, --force', 'Replace existing directory or stale link at the target')
|
|
724
|
+
.addHelpText('after', example('$ esl link ./my-skill -g\\n $ esl link ../draft-skill'))
|
|
725
|
+
.action(async (skillPath, options) => {
|
|
726
|
+
const sourcePath = skillPath ?? '.';
|
|
727
|
+
const skipToolLinks = options.tools === false;
|
|
728
|
+
let tools = skipToolLinks ? [] : parseToolsOption(options.tools);
|
|
729
|
+
if (!skipToolLinks && tools.length === 0) {
|
|
730
|
+
const configured = await resolveDefaultInstallTools(process.cwd(), {
|
|
731
|
+
...options,
|
|
732
|
+
tools: undefined,
|
|
733
|
+
global: options.global
|
|
734
|
+
});
|
|
735
|
+
if (configured.length === 0) {
|
|
736
|
+
if (program.opts().input === false || !isInteractive()) {
|
|
737
|
+
throw new Error('No tools configured; pass --tools or run interactively');
|
|
738
|
+
}
|
|
739
|
+
tools = await promptToolSelection();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const targetDir = await executeLink(sourcePath, {
|
|
743
|
+
...options,
|
|
744
|
+
tools,
|
|
745
|
+
noTools: skipToolLinks
|
|
746
|
+
});
|
|
747
|
+
console.log('Linked skill at ' + targetDir);
|
|
748
|
+
});
|
|
749
|
+
program
|
|
750
|
+
.command('adapt')
|
|
751
|
+
.description('Ensure Tool Links for installed skills')
|
|
752
|
+
.argument('[path]', 'project directory (defaults to --cd or the current directory)')
|
|
753
|
+
.option('-g, --global', 'Ensure global Tool Links instead of project links')
|
|
754
|
+
.addHelpText('after', example('$ esl adapt'))
|
|
755
|
+
.action(async (skillPath, options) => {
|
|
756
|
+
const results = await executeAdapt({ ...options, directory: skillPath });
|
|
757
|
+
for (const line of formatAdaptResults(results)) {
|
|
758
|
+
console.log(line);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
program
|
|
762
|
+
.command('list')
|
|
763
|
+
.alias('ls')
|
|
764
|
+
.description('List installed skills')
|
|
765
|
+
.option('-g, --global', 'List global skills instead of project skills')
|
|
766
|
+
.option('--json', 'Output as JSON')
|
|
767
|
+
.addHelpText('after', example('$ esl list'))
|
|
768
|
+
.action(async (options) => {
|
|
769
|
+
const skills = await executeList(options);
|
|
770
|
+
if (options.json) {
|
|
771
|
+
console.log(JSON.stringify(skills, null, 2));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (skills.length === 0) {
|
|
775
|
+
console.log(options.global ? 'No global skills installed.' : 'No skills installed in this project.');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const label = options.global ? 'Global' : 'Project';
|
|
779
|
+
console.log(`${label} skills (${skills.length} installed):`);
|
|
780
|
+
for (const skill of skills) {
|
|
781
|
+
const name = skill.name.padEnd(30);
|
|
782
|
+
console.log(` ${name} v${skill.version} (${skill.source})`);
|
|
783
|
+
}
|
|
784
|
+
});
|
|
785
|
+
const toolsCommand = program
|
|
786
|
+
.command('tools')
|
|
787
|
+
.description('Manage AI tool skill links')
|
|
788
|
+
.addHelpText('after', example('$ esl tools list --tool claude-code,codex --managed'));
|
|
789
|
+
toolsCommand
|
|
790
|
+
.command('list')
|
|
791
|
+
.description('List skills linked into AI tools')
|
|
792
|
+
.option('--tool <tools>', 'filter by tool, comma-separated')
|
|
793
|
+
.option('--skill <skills>', 'filter by skill identity, comma-separated')
|
|
794
|
+
.option('-g, --global', 'list global links instead of project links')
|
|
795
|
+
.option('--project', 'list project links (the default)')
|
|
796
|
+
.option('--managed', 'only ESL-managed links')
|
|
797
|
+
.option('--unmanaged', 'only links ESL does not manage')
|
|
798
|
+
.option('--status <statuses>', 'filter by status: linked,broken,conflict,source-only,unmanaged')
|
|
799
|
+
.option('--json', 'output as JSON')
|
|
800
|
+
.addHelpText('after', example('$ esl tools list --tool claude-code,codex --managed'))
|
|
801
|
+
.action(async (options) => {
|
|
802
|
+
const entries = await executeToolsList(options);
|
|
803
|
+
if (options.json) {
|
|
804
|
+
console.log(JSON.stringify(entries, null, 2));
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
for (const line of formatToolsList(entries)) {
|
|
808
|
+
console.log(line);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
toolsCommand
|
|
812
|
+
.command('remove')
|
|
813
|
+
.description('Remove ESL-managed links for a skill')
|
|
814
|
+
.argument('<skill-name>', 'skill identity, e.g. @acme/review')
|
|
815
|
+
.option('--tools <tools>', 'AI tools to unlink, comma-separated or all')
|
|
816
|
+
.option('-g, --global', 'remove global links instead of project links')
|
|
817
|
+
.addHelpText('after', example('$ esl tools remove @acme/review --tools claude-code,cursor'))
|
|
818
|
+
.action(async (skillName, options) => {
|
|
819
|
+
let tools = parseToolsOption(options.tools);
|
|
820
|
+
if (tools.length === 0) {
|
|
821
|
+
if (program.opts().input === false || !isInteractive()) {
|
|
822
|
+
throw new Error('No tools selected; pass --tools or run interactively');
|
|
823
|
+
}
|
|
824
|
+
tools = await promptToolSelection();
|
|
825
|
+
}
|
|
826
|
+
const results = await executeToolsRemove(skillName, { ...options, tools });
|
|
827
|
+
if (results.length === 0) {
|
|
828
|
+
console.log(`No ESL-managed links found for ${skillName}`);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
for (const result of results) {
|
|
832
|
+
console.log(`${result.tool}: ${result.status} ${result.targetDir}`);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
program
|
|
836
|
+
.command('source')
|
|
837
|
+
.description('Clone skill source for development')
|
|
838
|
+
.argument('<skill-name>')
|
|
839
|
+
.argument('[target]', 'target directory')
|
|
840
|
+
.option('--server <url>', 'ESL Server URL')
|
|
841
|
+
.addHelpText('after', example('$ esl source @cnfox/code-review'))
|
|
842
|
+
.action(async (skillName, target, options) => {
|
|
843
|
+
const targetDir = await executeSource(skillName, { ...options, target });
|
|
844
|
+
console.log(`Skill cloned to ${targetDir}`);
|
|
845
|
+
});
|
|
846
|
+
program
|
|
847
|
+
.command('use')
|
|
848
|
+
.description('Output a skill prompt without installing (pipe to an agent)')
|
|
849
|
+
.argument('<name-or-path>', 'skill name (@namespace/skill) or local path')
|
|
850
|
+
.option('--version <version>', 'version to use')
|
|
851
|
+
.option('--server <url>', 'ESL Server URL')
|
|
852
|
+
.addHelpText('after', example('$ esl use @cnfox/code-review'))
|
|
853
|
+
.action(async (nameOrPath, options) => {
|
|
854
|
+
const content = await executeUse(nameOrPath, options);
|
|
855
|
+
process.stdout.write(content);
|
|
856
|
+
});
|
|
857
|
+
program
|
|
858
|
+
.command('update')
|
|
859
|
+
.description('Update installed skills to latest versions')
|
|
860
|
+
.argument('[skill-name]', 'specific skill to update')
|
|
861
|
+
.option('-g, --global', 'Update global skills')
|
|
862
|
+
.option('--tools <tools>', 'Ensure links for these AI tools after updating')
|
|
863
|
+
.option('-f, --force', 'Replace ESL-owned stale links')
|
|
864
|
+
.option('--server <url>', 'ESL Server URL')
|
|
865
|
+
.addHelpText('after', example('$ esl update'))
|
|
866
|
+
.action(async (skillName, options) => {
|
|
867
|
+
const results = await executeUpdate({
|
|
868
|
+
...options,
|
|
869
|
+
skillName,
|
|
870
|
+
tools: parseToolsOption(options.tools)
|
|
871
|
+
});
|
|
872
|
+
if (results.length === 0) {
|
|
873
|
+
console.log('All skills are up to date');
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
for (const result of results) {
|
|
877
|
+
if (result.skipped === 'link') {
|
|
878
|
+
console.log(`${result.name}: linked (skipped)`);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
console.log(`${result.name}: ${result.from} -> ${result.to}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
program
|
|
886
|
+
.command('uninstall')
|
|
887
|
+
.description('Remove an installed skill')
|
|
888
|
+
.argument('<skill-name>')
|
|
889
|
+
.option('-g, --global', 'Uninstall from global skills directory')
|
|
890
|
+
.option('-f, --force', 'uninstall without confirmation')
|
|
891
|
+
.addHelpText('after', example('$ esl uninstall @cnfox/code-review'))
|
|
892
|
+
.action(async (skillName, options) => {
|
|
893
|
+
const result = await executeUninstall(skillName, options);
|
|
894
|
+
if (result.sourceRemoved) {
|
|
895
|
+
console.log(`Skill ${skillName} uninstalled`);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
console.log(`Skill ${skillName} uninstalled; linked source was preserved at ${result.sourcePath}`);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
program
|
|
902
|
+
.command('unlink')
|
|
903
|
+
.description('Unlink a local skill source and restore the previous store copy when staged')
|
|
904
|
+
.argument('[skill-name-or-path]', 'skill identity (@scope/name), or a skill directory (defaults to --cd or the current directory)')
|
|
905
|
+
.option('-g, --global', 'Unlink from global skills directory')
|
|
906
|
+
.addHelpText('after', example('$ esl unlink @local/my-skill\n $ esl unlink -g\n $ esl unlink ./my-skill') +
|
|
907
|
+
'\n\nProject-level path form must be run from the project root; omit-in-directory is for --global.')
|
|
908
|
+
.action(async (target, options) => {
|
|
909
|
+
const result = await executeUnlink(target, options);
|
|
910
|
+
if (result.restored) {
|
|
911
|
+
console.log(`Skill ${result.identity} unlinked; previous store copy restored at ${result.targetDir}`);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
console.log(`Skill ${result.identity} unlinked`);
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
program
|
|
918
|
+
.command('validate')
|
|
919
|
+
.argument('[path]', 'skill directory')
|
|
920
|
+
.addHelpText('after', example('$ esl validate ./my-skill'))
|
|
921
|
+
.action(async (directory) => {
|
|
922
|
+
const result = await executeValidate(directory);
|
|
923
|
+
if (result.valid) {
|
|
924
|
+
console.log('Skill package is valid');
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
for (const error of result.errors) {
|
|
928
|
+
console.error(error);
|
|
929
|
+
}
|
|
930
|
+
process.exitCode = 1;
|
|
931
|
+
});
|
|
932
|
+
program
|
|
933
|
+
.command('version')
|
|
934
|
+
.argument('[release]', 'major, minor, patch, or an explicit SemVer (e.g. 1.2.3)')
|
|
935
|
+
.addHelpText('after', example('$ esl version minor\n$ esl version 1.2.3'))
|
|
936
|
+
.action(async (release) => {
|
|
937
|
+
const rawParamsJson = program.opts().paramsJson;
|
|
938
|
+
const params = rawParamsJson
|
|
939
|
+
? parseCommandParams(rawParamsJson, 'version', ['release'])
|
|
940
|
+
: {};
|
|
941
|
+
assertNoDuplicateCommandParams(params, { release }, 'version');
|
|
942
|
+
release ??= readOptionalStringParam(params, 'release', 'version');
|
|
943
|
+
if (!release) {
|
|
944
|
+
if (program.opts().input === false) {
|
|
945
|
+
throw new Error('Missing release; pass major, minor, patch, or an explicit SemVer');
|
|
946
|
+
}
|
|
947
|
+
if (program.opts().agentInteraction === true) {
|
|
948
|
+
const inspection = await inspectVersion(process.cwd());
|
|
949
|
+
throw new AgentInteractionRequiredError(createAgentInteractionRequest({
|
|
950
|
+
command: 'version',
|
|
951
|
+
fields: versionAgentFields(inspection),
|
|
952
|
+
agentTool: program.opts().agentTool === undefined
|
|
953
|
+
? undefined
|
|
954
|
+
: resolveToolName(program.opts().agentTool)
|
|
955
|
+
}));
|
|
956
|
+
}
|
|
957
|
+
if (!isInteractive()) {
|
|
958
|
+
throw new Error('Missing release; pass major, minor, patch, or an explicit SemVer');
|
|
959
|
+
}
|
|
960
|
+
const inspection = await inspectVersion(process.cwd());
|
|
961
|
+
const selected = await promptVersionSelection(inspection);
|
|
962
|
+
release = selected === 'custom' ? await promptCustomVersion() : selected;
|
|
963
|
+
}
|
|
964
|
+
const version = await executeVersion(release);
|
|
965
|
+
console.log(version);
|
|
966
|
+
});
|
|
967
|
+
return program;
|
|
968
|
+
}
|
|
969
|
+
export function formatErrorMessage(error, locale = 'en-US') {
|
|
970
|
+
const apiError = error;
|
|
971
|
+
const message = typeof apiError.code === 'string'
|
|
972
|
+
? translateApiError({
|
|
973
|
+
locale,
|
|
974
|
+
code: apiError.code,
|
|
975
|
+
params: apiError.params,
|
|
976
|
+
fallback: apiError.message ?? 'The request failed'
|
|
977
|
+
})
|
|
978
|
+
: error instanceof Error
|
|
979
|
+
? error.message
|
|
980
|
+
: String(error);
|
|
981
|
+
const localizedMessage = apiError.status === 403
|
|
982
|
+
? `${message}\nThis skill may be maintained by another account or organization; log in with the maintaining organization ("esl login") and retry.`
|
|
983
|
+
: message;
|
|
984
|
+
return `Error: ${localizedMessage}\nRun with --debug for more detail.`;
|
|
985
|
+
}
|
|
986
|
+
function isPromptCancellation(error) {
|
|
987
|
+
return error instanceof Error && error.name === 'ExitPromptError';
|
|
988
|
+
}
|
|
989
|
+
export async function run(argv) {
|
|
990
|
+
const debug = argv.includes('-d') || argv.includes('--debug');
|
|
991
|
+
const onSigint = () => {
|
|
992
|
+
console.error('\nInterrupted');
|
|
993
|
+
process.exit(130);
|
|
994
|
+
};
|
|
995
|
+
process.on('SIGINT', onSigint);
|
|
996
|
+
try {
|
|
997
|
+
await retryPendingGlobalSync({});
|
|
998
|
+
await createProgram().parseAsync(argv);
|
|
999
|
+
}
|
|
1000
|
+
catch (error) {
|
|
1001
|
+
if (error instanceof AgentInteractionRequiredError) {
|
|
1002
|
+
process.stdout.write(`${JSON.stringify(toAskUserQuestionPayload(error.request))}\n`);
|
|
1003
|
+
process.exitCode = 2;
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
if (isPromptCancellation(error)) {
|
|
1007
|
+
process.exitCode = 130;
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
console.error(debug ? error : formatErrorMessage(error, activeLocale));
|
|
1011
|
+
process.exitCode = 1;
|
|
1012
|
+
}
|
|
1013
|
+
finally {
|
|
1014
|
+
process.off('SIGINT', onSigint);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
export function isDirectCliEntry(moduleUrl, argvPath) {
|
|
1018
|
+
if (!argvPath) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
return canonicalPath(fileURLToPath(moduleUrl)) === canonicalPath(argvPath);
|
|
1022
|
+
}
|
|
1023
|
+
function canonicalPath(filePath) {
|
|
1024
|
+
try {
|
|
1025
|
+
return fs.realpathSync.native(filePath);
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
return path.resolve(filePath);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (isDirectCliEntry(import.meta.url, process.argv[1])) {
|
|
1032
|
+
await run(process.argv);
|
|
1033
|
+
}
|