@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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/dist/api-error.d.ts +13 -0
  3. package/dist/api-error.js +31 -0
  4. package/dist/bin/esl.d.ts +35 -0
  5. package/dist/bin/esl.js +1033 -0
  6. package/dist/builtin/builtin-packages.json +11 -0
  7. package/dist/builtin/esl-operator/SKILL.md +65 -0
  8. package/dist/builtin/esl-operator/references/agent-interaction.md +110 -0
  9. package/dist/builtin/esl-operator/references/author.md +98 -0
  10. package/dist/builtin/esl-operator/references/consumer.md +109 -0
  11. package/dist/builtin/esl-operator/references/setup.md +41 -0
  12. package/dist/builtin/esl-operator/skill.json +7 -0
  13. package/dist/builtin-dir.d.ts +1 -0
  14. package/dist/builtin-dir.js +11 -0
  15. package/dist/commands/adapt.d.ts +8 -0
  16. package/dist/commands/adapt.js +27 -0
  17. package/dist/commands/admin.d.ts +11 -0
  18. package/dist/commands/admin.js +96 -0
  19. package/dist/commands/clone.d.ts +10 -0
  20. package/dist/commands/clone.js +21 -0
  21. package/dist/commands/config.d.ts +8 -0
  22. package/dist/commands/config.js +19 -0
  23. package/dist/commands/delete.d.ts +13 -0
  24. package/dist/commands/delete.js +51 -0
  25. package/dist/commands/deprecate.d.ts +16 -0
  26. package/dist/commands/deprecate.js +29 -0
  27. package/dist/commands/import.d.ts +14 -0
  28. package/dist/commands/import.js +21 -0
  29. package/dist/commands/info.d.ts +28 -0
  30. package/dist/commands/info.js +69 -0
  31. package/dist/commands/init.d.ts +28 -0
  32. package/dist/commands/init.js +252 -0
  33. package/dist/commands/install.d.ts +17 -0
  34. package/dist/commands/install.js +435 -0
  35. package/dist/commands/link.d.ts +11 -0
  36. package/dist/commands/link.js +170 -0
  37. package/dist/commands/list.d.ts +6 -0
  38. package/dist/commands/list.js +7 -0
  39. package/dist/commands/login.d.ts +20 -0
  40. package/dist/commands/login.js +132 -0
  41. package/dist/commands/logout.d.ts +6 -0
  42. package/dist/commands/logout.js +18 -0
  43. package/dist/commands/network-options.d.ts +27 -0
  44. package/dist/commands/network-options.js +133 -0
  45. package/dist/commands/notes.d.ts +10 -0
  46. package/dist/commands/notes.js +24 -0
  47. package/dist/commands/publish.d.ts +19 -0
  48. package/dist/commands/publish.js +271 -0
  49. package/dist/commands/release-delete.d.ts +21 -0
  50. package/dist/commands/release-delete.js +34 -0
  51. package/dist/commands/release-manifest.d.ts +5 -0
  52. package/dist/commands/release-manifest.js +23 -0
  53. package/dist/commands/rename.d.ts +5 -0
  54. package/dist/commands/rename.js +25 -0
  55. package/dist/commands/repair-tag.d.ts +5 -0
  56. package/dist/commands/repair-tag.js +14 -0
  57. package/dist/commands/reset-source.d.ts +16 -0
  58. package/dist/commands/reset-source.js +71 -0
  59. package/dist/commands/resolve-unlink-identity.d.ts +14 -0
  60. package/dist/commands/resolve-unlink-identity.js +47 -0
  61. package/dist/commands/search.d.ts +19 -0
  62. package/dist/commands/search.js +33 -0
  63. package/dist/commands/share.d.ts +17 -0
  64. package/dist/commands/share.js +56 -0
  65. package/dist/commands/source.d.ts +10 -0
  66. package/dist/commands/source.js +34 -0
  67. package/dist/commands/status.d.ts +16 -0
  68. package/dist/commands/status.js +77 -0
  69. package/dist/commands/sync-builtin.d.ts +10 -0
  70. package/dist/commands/sync-builtin.js +68 -0
  71. package/dist/commands/tools.d.ts +19 -0
  72. package/dist/commands/tools.js +73 -0
  73. package/dist/commands/uninstall.d.ts +19 -0
  74. package/dist/commands/uninstall.js +75 -0
  75. package/dist/commands/unlink.d.ts +14 -0
  76. package/dist/commands/unlink.js +108 -0
  77. package/dist/commands/update.d.ts +18 -0
  78. package/dist/commands/update.js +176 -0
  79. package/dist/commands/upload.d.ts +27 -0
  80. package/dist/commands/upload.js +446 -0
  81. package/dist/commands/use.d.ts +9 -0
  82. package/dist/commands/use.js +51 -0
  83. package/dist/commands/validate.d.ts +4 -0
  84. package/dist/commands/validate.js +8 -0
  85. package/dist/commands/version.d.ts +16 -0
  86. package/dist/commands/version.js +147 -0
  87. package/dist/commands/whoami.d.ts +14 -0
  88. package/dist/commands/whoami.js +54 -0
  89. package/dist/index.d.ts +24 -0
  90. package/dist/index.js +24 -0
  91. package/dist/output.d.ts +1 -0
  92. package/dist/output.js +3 -0
  93. package/dist/postinstall.d.ts +1 -0
  94. package/dist/postinstall.js +8 -0
  95. package/dist/prompt.d.ts +9 -0
  96. package/dist/prompt.js +24 -0
  97. package/dist/vendor/core/adapt/adapt-engine.d.ts +24 -0
  98. package/dist/vendor/core/adapt/adapt-engine.js +99 -0
  99. package/dist/vendor/core/adapt/adapted-skill-copy.d.ts +2 -0
  100. package/dist/vendor/core/adapt/adapted-skill-copy.js +17 -0
  101. package/dist/vendor/core/adapt/claude-adapter.d.ts +8 -0
  102. package/dist/vendor/core/adapt/claude-adapter.js +19 -0
  103. package/dist/vendor/core/adapt/codex-adapter.d.ts +8 -0
  104. package/dist/vendor/core/adapt/codex-adapter.js +19 -0
  105. package/dist/vendor/core/adapt/index.d.ts +2 -0
  106. package/dist/vendor/core/adapt/index.js +2 -0
  107. package/dist/vendor/core/adapt/tool-adapter.d.ts +12 -0
  108. package/dist/vendor/core/adapt/tool-adapter.js +1 -0
  109. package/dist/vendor/core/adapt/trae-adapter.d.ts +8 -0
  110. package/dist/vendor/core/adapt/trae-adapter.js +19 -0
  111. package/dist/vendor/core/adapt/trae-cn-adapter.d.ts +8 -0
  112. package/dist/vendor/core/adapt/trae-cn-adapter.js +19 -0
  113. package/dist/vendor/core/agent-interaction.d.ts +58 -0
  114. package/dist/vendor/core/agent-interaction.js +58 -0
  115. package/dist/vendor/core/command-params.d.ts +5 -0
  116. package/dist/vendor/core/command-params.js +46 -0
  117. package/dist/vendor/core/index.d.ts +22 -0
  118. package/dist/vendor/core/index.js +22 -0
  119. package/dist/vendor/core/link/skill-links.d.ts +38 -0
  120. package/dist/vendor/core/link/skill-links.js +245 -0
  121. package/dist/vendor/core/link/tool-links.d.ts +83 -0
  122. package/dist/vendor/core/link/tool-links.js +376 -0
  123. package/dist/vendor/core/org/account-policy.d.ts +8 -0
  124. package/dist/vendor/core/org/account-policy.js +57 -0
  125. package/dist/vendor/core/org/org-name.d.ts +2 -0
  126. package/dist/vendor/core/org/org-name.js +22 -0
  127. package/dist/vendor/core/org/standing-teams.d.ts +10 -0
  128. package/dist/vendor/core/org/standing-teams.js +27 -0
  129. package/dist/vendor/core/schema/compatibility.d.ts +17 -0
  130. package/dist/vendor/core/schema/compatibility.js +45 -0
  131. package/dist/vendor/core/schema/release-manifest.d.ts +88 -0
  132. package/dist/vendor/core/schema/release-manifest.js +161 -0
  133. package/dist/vendor/core/schema/skill-json.d.ts +61 -0
  134. package/dist/vendor/core/schema/skill-json.js +47 -0
  135. package/dist/vendor/core/schema/validation-result.d.ts +7 -0
  136. package/dist/vendor/core/schema/validation-result.js +1 -0
  137. package/dist/vendor/core/skill/builtin-package.d.ts +32 -0
  138. package/dist/vendor/core/skill/builtin-package.js +143 -0
  139. package/dist/vendor/core/skill/directory-validator.d.ts +9 -0
  140. package/dist/vendor/core/skill/directory-validator.js +72 -0
  141. package/dist/vendor/core/skill/import-skill.d.ts +10 -0
  142. package/dist/vendor/core/skill/import-skill.js +68 -0
  143. package/dist/vendor/core/skill/published-package.d.ts +1 -0
  144. package/dist/vendor/core/skill/published-package.js +9 -0
  145. package/dist/vendor/core/skill/skill-md.d.ts +15 -0
  146. package/dist/vendor/core/skill/skill-md.js +32 -0
  147. package/dist/vendor/core/store/file-copy.d.ts +3 -0
  148. package/dist/vendor/core/store/file-copy.js +102 -0
  149. package/dist/vendor/core/store/local-store.d.ts +43 -0
  150. package/dist/vendor/core/store/local-store.js +104 -0
  151. package/dist/vendor/core/store/skill-store.d.ts +24 -0
  152. package/dist/vendor/core/store/skill-store.js +41 -0
  153. package/dist/vendor/core/store/skills-json.d.ts +34 -0
  154. package/dist/vendor/core/store/skills-json.js +146 -0
  155. package/dist/vendor/core/version/select-version.d.ts +16 -0
  156. package/dist/vendor/core/version/select-version.js +34 -0
  157. package/dist/vendor/core/version/version.d.ts +2 -0
  158. package/dist/vendor/core/version/version.js +25 -0
  159. package/dist/vendor/i18n/api-errors.d.ts +121 -0
  160. package/dist/vendor/i18n/api-errors.js +240 -0
  161. package/dist/vendor/i18n/i18n-runtime.d.ts +4 -0
  162. package/dist/vendor/i18n/i18n-runtime.js +16 -0
  163. package/dist/vendor/i18n/index.d.ts +5 -0
  164. package/dist/vendor/i18n/index.js +5 -0
  165. package/dist/vendor/i18n/locales.d.ts +9 -0
  166. package/dist/vendor/i18n/locales.js +33 -0
  167. package/dist/vendor/i18n/resources.d.ts +11 -0
  168. package/dist/vendor/i18n/resources.js +925 -0
  169. package/dist/vendor/i18n/translate-api-error.d.ts +8 -0
  170. package/dist/vendor/i18n/translate-api-error.js +12 -0
  171. package/dist/version.d.ts +1 -0
  172. package/dist/version.js +24 -0
  173. package/package.json +46 -0
@@ -0,0 +1,446 @@
1
+ import { execFile } from 'node:child_process';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { fileExists, giteaUserEmail, isBuiltinIdentity, loadConfig, validateSkillSourceDirectory } from '../vendor/core/index.js';
6
+ import { apiUrl, fetchWithTimeout, gitAuthHeaderConfig, parseHttpOrigin, requireFreshToken, resolveNetworkConfig } from './network-options.js';
7
+ import { ensureReleaseManifest } from './release-manifest.js';
8
+ import { executeInfo } from './info.js';
9
+ import { confirm, isInteractive, readText } from '../prompt.js';
10
+ import { notify } from '../output.js';
11
+ import { requireOkResponse } from '../api-error.js';
12
+ const defaultExecFileAsync = promisify(execFile);
13
+ const DEFAULT_COMMIT_MESSAGE = 'chore: commit skill source for esl upload';
14
+ export async function executeUpload(options = {}) {
15
+ const directory = options.directory ?? process.cwd();
16
+ const execFileAsync = options.execFileAsync ?? defaultExecFileAsync;
17
+ const skillJsonPath = path.join(directory, 'skill.json');
18
+ if (await fileExists(skillJsonPath)) {
19
+ const raw = await fs.readFile(skillJsonPath, 'utf8');
20
+ const parsed = JSON.parse(raw);
21
+ if (parsed.name && isBuiltinIdentity(parsed.name)) {
22
+ throw new Error('Built-in skills cannot be uploaded; they are bundled with the ESL CLI');
23
+ }
24
+ }
25
+ if (!(await fileExists(`${directory}/release.json`))) {
26
+ await ensureReleaseManifest(options, directory);
27
+ }
28
+ const sourceValidation = await validateSkillSourceDirectory(directory);
29
+ if (!sourceValidation.success) {
30
+ throw new Error(`Invalid skill source: ${sourceValidation.errors.join(', ')}`);
31
+ }
32
+ const identity = sourceValidation.data.releaseManifest.name;
33
+ if (!(await hasEslRemote(execFileAsync, directory))) {
34
+ await confirmFirstUpload(options, identity);
35
+ }
36
+ const message = await resolveUploadMessage(options);
37
+ await prepareSourceGit(execFileAsync, directory, message, await resolveFallbackGitIdentity(options));
38
+ // ADR-0032:release.json v3 的 name 是归属的唯一权威来源,原样上送——
39
+ // 裸名由服务端解析为上传者个人命名空间。
40
+ return uploadSource(options, directory, identity, sourceValidation.data.skillMd.name, sourceValidation.data.skillMd.description, sourceValidation.data.releaseManifest.displayName);
41
+ }
42
+ async function confirmFirstUpload(options, identity) {
43
+ if (options.confirmIdentity !== undefined) {
44
+ if (options.confirmIdentity !== identity) {
45
+ throw new Error(`Upload identity confirmation mismatch: expected ${identity}, got ${options.confirmIdentity}`);
46
+ }
47
+ return;
48
+ }
49
+ const prompt = options.confirmPrompt ?? (isInteractive() ? confirm : undefined);
50
+ if (!prompt) {
51
+ throw new Error(`First upload fixes the skill identity; pass --confirm-identity ${identity} to confirm it in non-interactive mode`);
52
+ }
53
+ const approved = await prompt(`About to create a server-hosted skill source as ${identity}. This identity is fixed after upload. Continue? `);
54
+ if (!approved) {
55
+ throw new Error('Upload cancelled');
56
+ }
57
+ }
58
+ const DEFAULT_GITIGNORE = [
59
+ '# ESL skill upload ignores generated and local-only artifacts',
60
+ '.pytest_cache/',
61
+ '__pycache__/',
62
+ '*.pyc',
63
+ 'node_modules/',
64
+ '.DS_Store',
65
+ 'dist/',
66
+ 'build/',
67
+ '.venv/',
68
+ ''
69
+ ].join('\n');
70
+ async function resolveUploadMessage(options) {
71
+ if (options.message) {
72
+ return options.message;
73
+ }
74
+ if (options.noInput || !isInteractive()) {
75
+ return '';
76
+ }
77
+ return (await readText('Describe this upload (optional, press Enter to skip): ')).trim();
78
+ }
79
+ // 未显式配置 git 身份时的仓库级兜底作者:优先用当前登录的全局账号
80
+ // (Git Backend 的 email 约定),使 Git Backend 能把提交匹配到登录账号;
81
+ // 未登录或本地存储缺失时退回通用占位身份。
82
+ async function resolveFallbackGitIdentity(options) {
83
+ try {
84
+ const config = await loadConfig({ homeDir: options.homeDir });
85
+ if (config.username) {
86
+ return { name: config.username, email: giteaUserEmail(config.username) };
87
+ }
88
+ }
89
+ catch {
90
+ // Fall through to the generic placeholder identity.
91
+ }
92
+ return { name: 'esl upload', email: 'esl@local' };
93
+ }
94
+ async function prepareSourceGit(execFileAsync, directory, message, fallbackIdentity) {
95
+ // Initialize the repository when the skill directory is not already one.
96
+ try {
97
+ const inside = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: directory });
98
+ if (inside.stdout.trim() !== 'true') {
99
+ await execFileAsync('git', ['init'], { cwd: directory });
100
+ }
101
+ }
102
+ catch {
103
+ await execFileAsync('git', ['init'], { cwd: directory });
104
+ }
105
+ // Write a base .gitignore when none exists so generated junk stays out of the source.
106
+ const gitignorePath = path.join(directory, '.gitignore');
107
+ if (!(await fileExists(gitignorePath))) {
108
+ await fs.writeFile(gitignorePath, DEFAULT_GITIGNORE);
109
+ }
110
+ // Use a repository-local identity fallback so the auto-commit never blocks
111
+ // on a missing git user.name / user.email (repo-local only, never global).
112
+ // 兜底作者优先取当前登录身份,使提交在 Git Backend 里可关联到该账号。
113
+ if (!(await readGitConfig(execFileAsync, directory, 'user.name'))) {
114
+ await execFileAsync('git', ['config', 'user.name', fallbackIdentity.name], { cwd: directory });
115
+ }
116
+ if (!(await readGitConfig(execFileAsync, directory, 'user.email'))) {
117
+ await execFileAsync('git', ['config', 'user.email', fallbackIdentity.email], { cwd: directory });
118
+ }
119
+ // If a previous upload hit a merge conflict and left a rebase in progress,
120
+ // the user has resolved the marked files: continue that rebase instead of
121
+ // creating a new commit on top of the conflicted state.
122
+ if (await isRebaseInProgress(directory)) {
123
+ await execFileAsync('git', ['add', '-A'], { cwd: directory });
124
+ try {
125
+ await execFileAsync('git', ['rebase', '--continue'], { cwd: directory });
126
+ }
127
+ catch (error) {
128
+ throw new Error(`Still have unresolved source conflicts; resolve the marked files then re-run "esl upload": ${error.message}`);
129
+ }
130
+ }
131
+ else {
132
+ // Commit uncommitted changes so HEAD matches the source being uploaded,
133
+ // using the user-provided upload message as the commit message when given.
134
+ const status = await execFileAsync('git', ['status', '--porcelain'], { cwd: directory });
135
+ if (status.stdout.trim()) {
136
+ await execFileAsync('git', ['add', '-A'], { cwd: directory });
137
+ await execFileAsync('git', ['commit', '-m', message || DEFAULT_COMMIT_MESSAGE], { cwd: directory });
138
+ }
139
+ }
140
+ }
141
+ async function isRebaseInProgress(directory) {
142
+ return ((await fileExists(path.join(directory, '.git', 'rebase-merge'))) ||
143
+ (await fileExists(path.join(directory, '.git', 'rebase-apply'))));
144
+ }
145
+ async function readGitConfig(execFileAsync, directory, key) {
146
+ try {
147
+ const res = await execFileAsync('git', ['config', key], { cwd: directory });
148
+ return res.stdout.trim();
149
+ }
150
+ catch {
151
+ return '';
152
+ }
153
+ }
154
+ async function uploadSource(options, directory, identity, shortName, description, displayName) {
155
+ const authToken = await requireFreshToken(options);
156
+ const server = options.server ?? (await resolveNetworkConfig(options)).server;
157
+ const execFileAsync = options.execFileAsync ?? defaultExecFileAsync;
158
+ const authHeader = gitAuthHeaderConfig(authToken);
159
+ let uploaded;
160
+ let remoteUrl;
161
+ if (await hasEslRemote(execFileAsync, directory)) {
162
+ // Already server-hosted: skip the registration call and sync straight
163
+ // against the existing source.
164
+ remoteUrl = (await execFileAsync('git', ['remote', 'get-url', 'esl'], { cwd: directory })).stdout.trim();
165
+ // ADR-0023:Source Remote 重指——origin 漂移(Server Origin 迁移)时先按
166
+ // 身份向当前 server 验证,再采用服务器 cloneUrl 修复 remote;验证不过则
167
+ // 报可行动错误,绝不重新注册或接管。
168
+ remoteUrl = await rehomeEslRemoteIfNeeded(options, execFileAsync, directory, remoteUrl, server);
169
+ const remoteShortName = repoPathFromGitUrl(remoteUrl).split('/').pop() ?? '';
170
+ if (remoteShortName && remoteShortName !== shortName) {
171
+ throw new Error(`Local skill name "${shortName}" does not match the source repository "${remoteShortName}"; ` +
172
+ 'renames must go through "esl rename", not by editing SKILL.md');
173
+ }
174
+ await assertHostedNamespace(options, identity, skillNameFromRemote(remoteUrl));
175
+ uploaded = { name: skillNameFromRemote(remoteUrl), skillId: '', cloneUrl: remoteUrl };
176
+ // 技能描述(CONTEXT:Skill Description)与显示名随每次 Source Update 登记到服务器:
177
+ // 都是纯元数据更新,尽力而为——失败不阻断源码同步,仅在输出中提示。
178
+ const [hostScope, hostShortName] = uploaded.name.split('/');
179
+ const descriptionImpl = options.customFetch ?? fetch;
180
+ try {
181
+ const descriptionResponse = await fetchWithTimeout(descriptionImpl, apiUrl(server, `/api/skills/${encodeURIComponent(hostScope)}/${encodeURIComponent(hostShortName)}/description`), {
182
+ method: 'PUT',
183
+ headers: {
184
+ Authorization: `token ${authToken}`,
185
+ 'Content-Type': 'application/json'
186
+ },
187
+ body: JSON.stringify({ description })
188
+ });
189
+ if (!descriptionResponse.ok) {
190
+ console.warn(`Note: the server rejected the description update (${descriptionResponse.status}); the source sync is unaffected.`);
191
+ }
192
+ }
193
+ catch {
194
+ console.warn('Note: the description update could not reach the server; the source sync is unaffected.');
195
+ }
196
+ // 显示名(ADR-0048):随每次 Source Update 把当前源码 displayName 登记到服务器。
197
+ // 同样尽力而为——失败不阻断源码同步。已发布后对外标题由最近 Release 快照覆盖。
198
+ try {
199
+ const displayNameResponse = await fetchWithTimeout(descriptionImpl, apiUrl(server, `/api/skills/${encodeURIComponent(hostScope)}/${encodeURIComponent(hostShortName)}/display-name`), {
200
+ method: 'PUT',
201
+ headers: {
202
+ Authorization: `token ${authToken}`,
203
+ 'Content-Type': 'application/json'
204
+ },
205
+ body: JSON.stringify({ displayName: displayName ?? null })
206
+ });
207
+ if (!displayNameResponse.ok) {
208
+ console.warn(`Note: the server rejected the display name update (${displayNameResponse.status}); the source sync is unaffected.`);
209
+ }
210
+ }
211
+ catch {
212
+ console.warn('Note: the display name update could not reach the server; the source sync is unaffected.');
213
+ }
214
+ }
215
+ else {
216
+ const fetchImpl = options.customFetch ?? fetch;
217
+ const response = await fetchWithTimeout(fetchImpl, apiUrl(server, '/api/skills/upload'), {
218
+ method: 'POST',
219
+ headers: {
220
+ Authorization: `token ${authToken}`,
221
+ 'Content-Type': 'application/json'
222
+ },
223
+ body: JSON.stringify({ name: identity, description, displayName })
224
+ });
225
+ if (!response.ok) {
226
+ await requireOkResponse(response, 'Failed to upload skill source');
227
+ }
228
+ uploaded = (await response.json());
229
+ if (!uploaded.cloneUrl || !uploaded.skillId || !uploaded.name) {
230
+ throw new Error('Failed to upload skill source: API response is incomplete');
231
+ }
232
+ remoteUrl = uploaded.cloneUrl;
233
+ await ensureEslRemote(execFileAsync, directory, uploaded.cloneUrl);
234
+ }
235
+ try {
236
+ if ((await syncSource(options, execFileAsync, directory, authHeader, remoteUrl)) === 'up-to-date') {
237
+ return { ...uploaded, alreadyUpToDate: true };
238
+ }
239
+ }
240
+ catch (error) {
241
+ // syncSource 已按 ADR-0021 包裹指导,透传即可,避免指导段落重复。
242
+ if (error instanceof HostedSourceAccessError)
243
+ throw error;
244
+ throw hostedSourceAccessError(error.message);
245
+ }
246
+ try {
247
+ await execFileAsync('git', ['-c', authHeader, 'push', 'esl', 'HEAD:main'], { cwd: directory });
248
+ }
249
+ catch (error) {
250
+ // ADR-0021:不可访问的已托管源绝不被接管。错误给出两条恢复出路——
251
+ // 切回维护账号重跑 upload,或显式两步手动重建——绝不触碰 remote。
252
+ throw hostedSourceAccessError(error.message, { includePushHint: true });
253
+ }
254
+ return uploaded;
255
+ }
256
+ async function assertHostedNamespace(options, declaredIdentity, hostedIdentity) {
257
+ let expectedNamespace;
258
+ if (!declaredIdentity.startsWith('@')) {
259
+ const config = await loadConfig({ homeDir: options.homeDir }).catch(() => null);
260
+ if (!config?.username) {
261
+ throw new Error(`Cannot verify the bare release.json name "${declaredIdentity}" against the hosted source; log in again so the personal namespace can be resolved`);
262
+ }
263
+ expectedNamespace = config.username;
264
+ }
265
+ else {
266
+ expectedNamespace = namespaceFromIdentity(declaredIdentity);
267
+ }
268
+ const hostedNamespace = namespaceFromIdentity(hostedIdentity);
269
+ if (expectedNamespace !== hostedNamespace) {
270
+ throw new Error(`release.json declares namespace "${expectedNamespace}", but this source is hosted under namespace "${hostedNamespace}". ` +
271
+ 'Restore the established namespace, or create a new source; cross-namespace migration is not supported.');
272
+ }
273
+ }
274
+ function namespaceFromIdentity(identity) {
275
+ const namespace = identity.match(/^@([^/]+)\//)?.[1];
276
+ if (!namespace) {
277
+ throw new Error(`Cannot determine the namespace from the skill identity "${identity}"`);
278
+ }
279
+ return namespace;
280
+ }
281
+ // 带身份标记的「已托管源不可访问」错误:调用方据此识别错误已被包裹,避免对已
282
+ // 带指导的错误再次包裹导致指导段落重复。
283
+ class HostedSourceAccessError extends Error {
284
+ }
285
+ function hostedSourceAccessError(detail, options = {}) {
286
+ const guidance = 'Failed to sync the skill source with the server: the current login cannot access the server source at the esl remote. ' +
287
+ 'The source may be maintained by another account or organization, or it may no longer exist. ' +
288
+ 'Log in with the maintaining account and re-run "esl upload"; ' +
289
+ 'or, if the server source is confirmed deleted, run "git remote remove esl" and re-run "esl upload" to register a fresh source.';
290
+ const pushHint = options.includePushHint
291
+ ? ' To finish an interrupted push on an already-registered source, run "git push esl HEAD:main".'
292
+ : '';
293
+ return new HostedSourceAccessError(`${guidance}${pushHint} (${detail})`);
294
+ }
295
+ // ADR-0027:fetch 失败时的只读诊断。向 Registry API 查询 remote 揭示的
296
+ // Skill Identity;只有身份在当前登录下可见时才细分文案——
297
+ // - 服务器 cloneUrl 与 remote 仓库路径不一致 → remote 指向陈旧路径;
298
+ // - 路径一致 → 源存在、当前登录具备 Registry 读权限但缺 Git 源访问权限,
299
+ // 指引切回维护账号。
300
+ // 探测失败(404/403/网络)时退回 ADR-0021 的统一文案,不猜测「已删除」。
301
+ async function diagnoseHostedSourceAccess(options, remoteUrl, detail) {
302
+ if (!remoteUrl.startsWith('http'))
303
+ return hostedSourceAccessError(detail);
304
+ const identity = skillNameFromRemote(remoteUrl);
305
+ let info;
306
+ try {
307
+ info = await executeInfo(identity, options);
308
+ }
309
+ catch {
310
+ return hostedSourceAccessError(detail);
311
+ }
312
+ const serverClonePath = info.cloneUrl ? repoPathFromGitUrl(info.cloneUrl) : '';
313
+ const remotePath = repoPathFromGitUrl(remoteUrl);
314
+ if (!serverClonePath || serverClonePath === remotePath) {
315
+ return new HostedSourceAccessError(`Failed to sync the skill source with the server: the skill identity ${identity} exists on the server and is visible ` +
316
+ 'with the current login, but the current login cannot access its Git source. ' +
317
+ 'The credential may be stale or the login may lack repository access (the source may be maintained by another ' +
318
+ 'account, or its access was revoked). Log in with the maintaining account and re-run "esl upload". ' +
319
+ `(${detail})`);
320
+ }
321
+ return new HostedSourceAccessError(`The esl remote points at repository path "${remotePath}" but the server's current clone URL for ${identity} is "${serverClonePath}". ` +
322
+ 'The remote path is stale (for example after a rename). Verify the source location ("esl source"), then run ' +
323
+ '"git remote remove esl" and re-run "esl upload" to re-register or re-sync against the current repository. ' +
324
+ `(${detail})`);
325
+ }
326
+ function skillNameFromRemote(remoteUrl) {
327
+ const match = remoteUrl.match(/\/git\/(.+?)(?:\.git)?\/?$/);
328
+ return match ? `@${match[1]}` : 'source';
329
+ }
330
+ // 从服务器 cloneUrl / Source Remote URL 中取出 /git/ 之后的仓库路径。
331
+ function repoPathFromGitUrl(url) {
332
+ return url.match(/\/git\/(.+?)(?:\.git)?\/?$/)?.[1] ?? '';
333
+ }
334
+ // ADR-0023:Source Remote 重指。检测到 Source Remote origin 与当前配置 server
335
+ // 的 origin 漂移(Server Origin 迁移)时,以旧 remote 路径解析出的 Skill
336
+ // Identity 向当前 server 验证(含 Skill Rename 重定向),验证通过则采用服务器
337
+ // 返回的 cloneUrl(ADR-0004:客户端不自行推导后端路径)静默重指;验证不过则
338
+ // 报可行动错误,不触碰所有权(不创建 Skill ID、不删除 remote)。
339
+ async function rehomeEslRemoteIfNeeded(options, execFileAsync, directory, remoteUrl, server) {
340
+ const oldOrigin = parseHttpOrigin(remoteUrl);
341
+ const newOrigin = parseHttpOrigin(server);
342
+ if (!oldOrigin || !newOrigin || oldOrigin === newOrigin) {
343
+ return remoteUrl;
344
+ }
345
+ const identity = skillNameFromRemote(remoteUrl);
346
+ if (!identity.startsWith('@')) {
347
+ throw sourceRemoteUnverifiedError(oldOrigin, newOrigin, null);
348
+ }
349
+ let cloneUrl;
350
+ try {
351
+ const info = await executeInfo(identity, options);
352
+ cloneUrl = info.cloneUrl;
353
+ }
354
+ catch {
355
+ cloneUrl = undefined;
356
+ }
357
+ if (!cloneUrl) {
358
+ throw sourceRemoteUnverifiedError(oldOrigin, newOrigin, identity);
359
+ }
360
+ if (parseHttpOrigin(cloneUrl) === oldOrigin) {
361
+ // 服务器返回的 cloneUrl 与现 remote 同源:remote 已指向权威地址,无需重指。
362
+ return remoteUrl;
363
+ }
364
+ await execFileAsync('git', ['remote', 'set-url', 'esl', cloneUrl], { cwd: directory });
365
+ notify(`Server origin migration detected: re-homed the esl remote from ${oldOrigin} to ${newOrigin}.`);
366
+ return cloneUrl;
367
+ }
368
+ // ADR-0023:地址迁移验证失败的可行动错误——与 ADR-0021 的「账号/权限」语义区分。
369
+ function sourceRemoteUnverifiedError(oldOrigin, newOrigin, identity) {
370
+ const identityClause = identity
371
+ ? `the skill identity ${identity} could not be verified on ${newOrigin}`
372
+ : 'the skill identity could not be determined from the remote URL';
373
+ return new Error(`The esl remote points at ${oldOrigin} but the configured ESL server is ${newOrigin}; ` +
374
+ 'this looks like a server origin migration, but ' +
375
+ `${identityClause} (it may not exist there, or the current login cannot read it - ` +
376
+ 'the source may be maintained by another account or organization). ' +
377
+ 'Log in with the maintaining account and re-run "esl upload"; ' +
378
+ 'or, if the source is confirmed absent from this server, run "git remote remove esl" and re-run "esl upload" to register a fresh source.');
379
+ }
380
+ async function hasEslRemote(execFileAsync, directory) {
381
+ try {
382
+ const res = await execFileAsync('git', ['remote', 'get-url', 'esl'], { cwd: directory });
383
+ return res.stdout.trim().length > 0;
384
+ }
385
+ catch {
386
+ return false;
387
+ }
388
+ }
389
+ async function syncSource(options, execFileAsync, directory, authHeader, remoteUrl) {
390
+ // Refresh the server-side ref so we can compare and rebase onto it.
391
+ // ADR-0021:已托管源上任何 fetch 失败一律按「凭据不足或源不可达」处理,
392
+ // 不视为可接管的孤儿。
393
+ try {
394
+ await execFileAsync('git', ['-c', authHeader, 'fetch', 'esl'], { cwd: directory });
395
+ }
396
+ catch (error) {
397
+ // ADR-0027:Git Backend 对「无权限」与「不存在」返回相同 404;报错前用
398
+ // Registry API 只读探测一次身份,探测成功才细分文案,失败退回统一文案。
399
+ // 行为不变:不自动接管、不重指。
400
+ throw await diagnoseHostedSourceAccess(options, remoteUrl, error.message);
401
+ }
402
+ let remoteHead;
403
+ try {
404
+ remoteHead = (await execFileAsync('git', ['rev-parse', '--verify', 'esl/main'], { cwd: directory })).stdout.trim();
405
+ }
406
+ catch {
407
+ return 'needs-push'; // the server source has no commits yet (first push)
408
+ }
409
+ const localHead = (await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: directory })).stdout.trim();
410
+ if (localHead === remoteHead) {
411
+ return 'up-to-date';
412
+ }
413
+ const behind = parseInt((await execFileAsync('git', ['rev-list', '--count', 'HEAD..esl/main'], { cwd: directory })).stdout.trim() || '0', 10);
414
+ if (behind > 0) {
415
+ // Rebase local commits onto the server source. On a content conflict the
416
+ // rebase is left in progress so the user can resolve the marked files and
417
+ // re-run upload to finish the merge and push.
418
+ try {
419
+ await execFileAsync('git', ['rebase', 'esl/main'], { cwd: directory });
420
+ }
421
+ catch {
422
+ throw new Error(`Source update conflict: the server source advanced by ${behind} commit(s) and your local edits overlap. ` +
423
+ 'Conflicted files are marked in your editor; resolve them, then re-run "esl upload" to finish the merge and push.');
424
+ }
425
+ }
426
+ return 'needs-push';
427
+ }
428
+ async function ensureEslRemote(execFileAsync, directory, cloneUrl) {
429
+ try {
430
+ await execFileAsync('git', ['remote', 'add', 'esl', cloneUrl], { cwd: directory });
431
+ return;
432
+ }
433
+ catch (error) {
434
+ const message = error.message ?? '';
435
+ if (!message.includes('remote esl already exists') && !message.includes('remote `esl` already exists')) {
436
+ throw error;
437
+ }
438
+ }
439
+ // An interrupted Source Upload may be resumed when the existing Source Remote
440
+ // already points at the same server source; a different URL must be resolved first.
441
+ const { stdout } = await execFileAsync('git', ['remote', 'get-url', 'esl'], { cwd: directory });
442
+ const currentUrl = stdout.trim();
443
+ if (currentUrl !== cloneUrl) {
444
+ throw new Error(`Skill source already has an esl remote pointing at ${currentUrl}; remove it with "git remote remove esl" or point it at ${cloneUrl}`);
445
+ }
446
+ }
@@ -0,0 +1,9 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { type NetworkCommandOptions } from './network-options.js';
3
+ declare const defaultExecFileAsync: typeof execFile.__promisify__;
4
+ export interface UseOptions extends NetworkCommandOptions {
5
+ version?: string;
6
+ execFileAsync?: typeof defaultExecFileAsync;
7
+ }
8
+ export declare function executeUse(nameOrPath: string, options?: UseOptions): Promise<string>;
9
+ export {};
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { highestStableVersion, isBuiltinIdentity, loadBuiltinPackageOrThrow, removeDirectory } from '../vendor/core/index.js';
7
+ import { gitAuthHeaderConfig, requireConfigured, requireFreshToken } from './network-options.js';
8
+ import { executeInfo } from './info.js';
9
+ import { resolveBuiltinDir } from '../builtin-dir.js';
10
+ const defaultExecFileAsync = promisify(execFile);
11
+ function isLocalPath(nameOrPath) {
12
+ return nameOrPath.startsWith('.') || nameOrPath.startsWith('/') || nameOrPath.startsWith('\\') || path.isAbsolute(nameOrPath);
13
+ }
14
+ async function readSkillMdFromLocal(sourcePath) {
15
+ const resolved = path.resolve(sourcePath);
16
+ const skillMdPath = path.join(resolved, 'SKILL.md');
17
+ return fs.readFile(skillMdPath, 'utf8');
18
+ }
19
+ async function readSkillMdFromServer(name, options) {
20
+ const execFileAsync = options.execFileAsync ?? defaultExecFileAsync;
21
+ const authToken = await requireFreshToken(options);
22
+ const info = await executeInfo(name, options);
23
+ const remoteUrl = requireConfigured(info.cloneUrl, 'cloneUrl');
24
+ const authHeader = gitAuthHeaderConfig(authToken);
25
+ const version = options.version ?? highestStableVersion(info.versions ?? []);
26
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'esl-use-'));
27
+ try {
28
+ const cloneDir = path.join(tmpDir, 'repo');
29
+ await execFileAsync('git', ['-c', authHeader, 'clone', '--depth', '1', remoteUrl, cloneDir]);
30
+ if (version) {
31
+ await execFileAsync('git', ['checkout', version], { cwd: cloneDir });
32
+ }
33
+ return fs.readFile(path.join(cloneDir, 'SKILL.md'), 'utf8');
34
+ }
35
+ finally {
36
+ await removeDirectory(tmpDir);
37
+ }
38
+ }
39
+ async function readSkillMdFromBuiltin(name, options) {
40
+ const builtinDir = options.builtinDir ?? resolveBuiltinDir();
41
+ const builtin = await loadBuiltinPackageOrThrow(builtinDir, name);
42
+ return fs.readFile(path.join(builtin.directory, 'SKILL.md'), 'utf8');
43
+ }
44
+ export async function executeUse(nameOrPath, options = {}) {
45
+ if (isBuiltinIdentity(nameOrPath)) {
46
+ return readSkillMdFromBuiltin(nameOrPath, options);
47
+ }
48
+ return isLocalPath(nameOrPath)
49
+ ? readSkillMdFromLocal(nameOrPath)
50
+ : readSkillMdFromServer(nameOrPath, options);
51
+ }
@@ -0,0 +1,4 @@
1
+ export declare function executeValidate(directory?: string): Promise<{
2
+ valid: boolean;
3
+ errors: string[];
4
+ }>;
@@ -0,0 +1,8 @@
1
+ import { validateSkillSourceDirectory } from '../vendor/core/index.js';
2
+ export async function executeValidate(directory = process.cwd()) {
3
+ const result = await validateSkillSourceDirectory(directory);
4
+ if (result.success) {
5
+ return { valid: true, errors: [] };
6
+ }
7
+ return { valid: false, errors: result.errors };
8
+ }
@@ -0,0 +1,16 @@
1
+ import { execFile } from 'node:child_process';
2
+ declare const execFileAsync: typeof execFile.__promisify__;
3
+ type BumpType = 'major' | 'minor' | 'patch';
4
+ type VersionInput = BumpType | string;
5
+ export declare function isValidExplicitVersion(input: string): boolean;
6
+ export declare function nextVersion(current: string, bump: BumpType): string;
7
+ export interface VersionInspection {
8
+ currentVersion: string | null;
9
+ isPreVersion: boolean;
10
+ }
11
+ export declare function inspectVersion(directory: string): Promise<VersionInspection>;
12
+ export declare function executeVersion(input: VersionInput, options?: {
13
+ cwd?: string;
14
+ execFile?: typeof execFileAsync;
15
+ }): Promise<string>;
16
+ export {};