@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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { loadConfig, initializeLocalStore, saveConfig, saveCredentials } from '../vendor/core/index.js';
|
|
2
|
+
import { resolveLocale, translateApiError } from '../vendor/i18n/index.js';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { isInteractive, readHidden, readText } from '../prompt.js';
|
|
5
|
+
import { resolveServer } from './config.js';
|
|
6
|
+
import { fetchWithTimeout } from './network-options.js';
|
|
7
|
+
// 全局身份登录(ADR-0032):username + password 一条凭据,无组织输入;
|
|
8
|
+
// 所属组织列表由服务端按 Gitea 成员关系派生。
|
|
9
|
+
export async function executeLogin(options) {
|
|
10
|
+
const fetchImpl = options.customFetch ?? fetch;
|
|
11
|
+
await initializeLocalStore({ homeDir: options.homeDir });
|
|
12
|
+
const currentConfig = await loadConfig({ homeDir: options.homeDir });
|
|
13
|
+
const activeLocale = resolveLocale({
|
|
14
|
+
override: options.locale,
|
|
15
|
+
accountLocale: currentConfig.locale
|
|
16
|
+
});
|
|
17
|
+
const server = await resolveServerForLogin(options);
|
|
18
|
+
const username = await resolveUsername(options);
|
|
19
|
+
const { token, organizations, locale } = await resolveLoginToken(options, server, username, fetchImpl, activeLocale);
|
|
20
|
+
await saveCredentials({ token, loginAt: new Date().toISOString() }, { homeDir: options.homeDir });
|
|
21
|
+
// 显式清掉旧版标记:重新登录后 organizations 已是新形状,提示不该跟着旧配置
|
|
22
|
+
// 一直留在 whoami 输出里。
|
|
23
|
+
await saveConfig({ server, username, organizations, locale, legacyIdentity: false }, { homeDir: options.homeDir });
|
|
24
|
+
return { token, username, organizations, locale };
|
|
25
|
+
}
|
|
26
|
+
// 首次使用未配置 server 时交互式询问并记住;非交互环境直接报错。
|
|
27
|
+
async function resolveServerForLogin(options) {
|
|
28
|
+
try {
|
|
29
|
+
return await resolveServer({ server: options.server, homeDir: options.homeDir });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (options.noInput) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
if (options.readServer) {
|
|
36
|
+
const url = (await options.readServer()).trim();
|
|
37
|
+
if (!url) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
return url;
|
|
41
|
+
}
|
|
42
|
+
if (!isInteractive()) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
const url = (await readText('Server URL (e.g. http://localhost:3000): ')).trim();
|
|
46
|
+
if (!url) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
return url;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function resolveUsername(options) {
|
|
53
|
+
if (options.username) {
|
|
54
|
+
return options.username;
|
|
55
|
+
}
|
|
56
|
+
if (options.noInput) {
|
|
57
|
+
throw new Error('A username is required; pass --username or run interactively');
|
|
58
|
+
}
|
|
59
|
+
if (options.readUsername) {
|
|
60
|
+
return (await options.readUsername('Username: ')).trim();
|
|
61
|
+
}
|
|
62
|
+
if (!isInteractive()) {
|
|
63
|
+
throw new Error('A username is required; pass --username or run interactively');
|
|
64
|
+
}
|
|
65
|
+
return (await readText('Username: ')).trim();
|
|
66
|
+
}
|
|
67
|
+
async function resolveLoginToken(options, server, username, fetchImpl, locale) {
|
|
68
|
+
if (options.tokenFile) {
|
|
69
|
+
const token = (await fs.readFile(options.tokenFile, 'utf8')).trim();
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new Error(`Token file ${options.tokenFile} is empty`);
|
|
72
|
+
}
|
|
73
|
+
// token-file 登录不联网,拿不到成员关系;组织列表留空,whoami/status
|
|
74
|
+
// 以本地已有信息展示。
|
|
75
|
+
return { token, organizations: [], locale: null };
|
|
76
|
+
}
|
|
77
|
+
const password = options.passwordFile
|
|
78
|
+
? (await fs.readFile(options.passwordFile, 'utf8')).trim()
|
|
79
|
+
: await promptForPassword(options);
|
|
80
|
+
if (!password) {
|
|
81
|
+
throw new Error('Password is required for login');
|
|
82
|
+
}
|
|
83
|
+
return exchangePasswordForToken(server, username, password, fetchImpl, locale);
|
|
84
|
+
}
|
|
85
|
+
async function promptForPassword(options) {
|
|
86
|
+
if (options.noInput) {
|
|
87
|
+
throw new Error('Login requires a password or token; pass --password-file or --token-file when using --no-input');
|
|
88
|
+
}
|
|
89
|
+
if (options.readInput) {
|
|
90
|
+
return (await options.readInput()).trim();
|
|
91
|
+
}
|
|
92
|
+
if (!isInteractive()) {
|
|
93
|
+
throw new Error('Login requires a password; run interactively or pass --password-file/--token-file');
|
|
94
|
+
}
|
|
95
|
+
return (await readHidden('Password: ')).trim();
|
|
96
|
+
}
|
|
97
|
+
async function exchangePasswordForToken(server, username, password, fetchImpl, locale) {
|
|
98
|
+
const base = server.replace(/\/$/, '');
|
|
99
|
+
const res = await fetchWithTimeout(fetchImpl, `${base}/api/auth/login`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': 'application/json'
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify({ username, password })
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const err = await res.text();
|
|
108
|
+
try {
|
|
109
|
+
const body = JSON.parse(err);
|
|
110
|
+
if (typeof body.code === 'string') {
|
|
111
|
+
throw new Error(translateApiError({
|
|
112
|
+
locale,
|
|
113
|
+
code: body.code,
|
|
114
|
+
params: body.params,
|
|
115
|
+
fallback: body.message ?? 'Failed to authenticate with ESL Server'
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
if (!(error instanceof SyntaxError)) {
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Failed to authenticate with ESL Server: ${err}`);
|
|
125
|
+
}
|
|
126
|
+
const data = (await res.json());
|
|
127
|
+
return {
|
|
128
|
+
token: data.token,
|
|
129
|
+
organizations: Array.isArray(data.organizations) ? data.organizations : [],
|
|
130
|
+
locale: data.locale ?? null
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type LocalStoreOptions } from '@esl/core';
|
|
2
|
+
export interface LogoutResult {
|
|
3
|
+
hadCredentials: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare function executeLogout(options?: LocalStoreOptions): Promise<LogoutResult>;
|
|
6
|
+
export declare function formatLogout(result: LogoutResult): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { clearCredentials, loadCredentials } from '../vendor/core/index.js';
|
|
2
|
+
export async function executeLogout(options = {}) {
|
|
3
|
+
let hadCredentials = false;
|
|
4
|
+
try {
|
|
5
|
+
const credentials = await loadCredentials({ homeDir: options.homeDir });
|
|
6
|
+
hadCredentials = Boolean(credentials.token);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
hadCredentials = false;
|
|
10
|
+
}
|
|
11
|
+
await clearCredentials({ homeDir: options.homeDir });
|
|
12
|
+
return { hadCredentials };
|
|
13
|
+
}
|
|
14
|
+
export function formatLogout(result) {
|
|
15
|
+
return result.hadCredentials
|
|
16
|
+
? 'Logged out. Local credentials cleared.'
|
|
17
|
+
: 'Not logged in; no credentials to clear.';
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type LocalStoreOptions } from '@esl/core';
|
|
2
|
+
export interface NetworkCommandOptions extends LocalStoreOptions {
|
|
3
|
+
server?: string;
|
|
4
|
+
customFetch?: typeof fetch;
|
|
5
|
+
builtinDir?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ResolvedNetworkConfig {
|
|
8
|
+
server: string;
|
|
9
|
+
token: string | null;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveNetworkConfig(options: NetworkCommandOptions): Promise<ResolvedNetworkConfig>;
|
|
12
|
+
export declare function requireConfigured(value: string | null | undefined, name: string): string;
|
|
13
|
+
export declare function withAuthGuidanceIfForbidden(status: number, message: string): string;
|
|
14
|
+
export declare function resolveTimeoutMs(): number;
|
|
15
|
+
export declare function resolveLoginTtlMs(): number;
|
|
16
|
+
export declare function isLoginFresh(loginAt: string | null | undefined): boolean;
|
|
17
|
+
export declare function requireFreshToken(options?: LocalStoreOptions): Promise<string>;
|
|
18
|
+
export declare function resolveOptionalFreshToken(options?: LocalStoreOptions): Promise<string | null>;
|
|
19
|
+
export declare function fetchWithTimeout(fetchImpl: typeof fetch, url: string, init?: RequestInit, timeoutMs?: number): Promise<Response>;
|
|
20
|
+
export declare function apiUrl(server: string, path: string): string;
|
|
21
|
+
export declare function parseHttpOrigin(url: string): string | null;
|
|
22
|
+
export declare function sourceRemoteOriginDrifted(remoteUrl: string, server: string): boolean;
|
|
23
|
+
export declare function gitAuthHeaderConfig(token: string): string;
|
|
24
|
+
export declare function installTargetDir(skillName: string, options: LocalStoreOptions): string;
|
|
25
|
+
export declare function projectSkillsDir(projectRoot: string, skillName: string): string;
|
|
26
|
+
export declare function publishedInstallTargetDir(skillName: string, options: LocalStoreOptions): string;
|
|
27
|
+
export declare function publishedProjectSkillsDir(projectRoot: string, skillName: string): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { loadConfig, loadCredentials, resolveLocalStorePaths, resolveProjectStorePaths, skillSourceDirectoryName } from '../vendor/core/index.js';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function resolveNetworkConfig(options) {
|
|
4
|
+
const config = await loadConfig({ homeDir: options.homeDir });
|
|
5
|
+
const credentials = await loadCredentials({ homeDir: options.homeDir });
|
|
6
|
+
return {
|
|
7
|
+
server: options.server ?? requireConfigured(config.server, 'server'),
|
|
8
|
+
token: credentials.token
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function requireConfigured(value, name) {
|
|
12
|
+
if (!value) {
|
|
13
|
+
throw new Error(`Missing ${name}; run esl login or pass --${name}`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
// ADR-0021 后续:403 通常意味着技能由其他账号/组织维护(各组织账号相互独立)。
|
|
18
|
+
// 在原始错误后附加可行动指引,替代裸 403 信息。
|
|
19
|
+
function withAuthGuidance(message) {
|
|
20
|
+
return `${message}\nThis skill may be maintained by another account or organization; log in with the maintaining organization ("esl login") and retry.`;
|
|
21
|
+
}
|
|
22
|
+
// 403 时附加跨账号指引,其余状态码原样返回。
|
|
23
|
+
export function withAuthGuidanceIfForbidden(status, message) {
|
|
24
|
+
return status === 403 ? withAuthGuidance(message) : message;
|
|
25
|
+
}
|
|
26
|
+
export function resolveTimeoutMs() {
|
|
27
|
+
const env = process.env.ESL_HTTP_TIMEOUT;
|
|
28
|
+
if (env) {
|
|
29
|
+
const parsed = Number(env);
|
|
30
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return 30_000;
|
|
35
|
+
}
|
|
36
|
+
export function resolveLoginTtlMs() {
|
|
37
|
+
const env = process.env.ESL_LOGIN_TTL_HOURS;
|
|
38
|
+
if (env) {
|
|
39
|
+
const parsed = Number(env);
|
|
40
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
41
|
+
return parsed * 3_600_000;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return 720 * 3_600_000;
|
|
45
|
+
}
|
|
46
|
+
// 登录是否仍在 TTL 有效期内(本地时间戳判断,不向服务器验证)。
|
|
47
|
+
export function isLoginFresh(loginAt) {
|
|
48
|
+
if (!loginAt) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const loginAtMs = Date.parse(loginAt);
|
|
52
|
+
return !Number.isNaN(loginAtMs) && Date.now() - loginAtMs <= resolveLoginTtlMs();
|
|
53
|
+
}
|
|
54
|
+
export async function requireFreshToken(options = {}) {
|
|
55
|
+
const credentials = await loadCredentials({ homeDir: options.homeDir });
|
|
56
|
+
const token = requireConfigured(credentials.token, 'token');
|
|
57
|
+
if (!isLoginFresh(credentials.loginAt)) {
|
|
58
|
+
throw new Error('Login expired; run esl login to re-authenticate');
|
|
59
|
+
}
|
|
60
|
+
return token;
|
|
61
|
+
}
|
|
62
|
+
// 匿名也可用的请求所用尽力而为的凭据:登录仍在有效期则返回 token,缺失或过期返回 null(调用方按匿名处理)。
|
|
63
|
+
export async function resolveOptionalFreshToken(options = {}) {
|
|
64
|
+
let token;
|
|
65
|
+
let loginAt;
|
|
66
|
+
try {
|
|
67
|
+
const credentials = await loadCredentials({ homeDir: options.homeDir });
|
|
68
|
+
token = credentials.token;
|
|
69
|
+
loginAt = credentials.loginAt;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!token || !isLoginFresh(loginAt)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return token;
|
|
78
|
+
}
|
|
79
|
+
export async function fetchWithTimeout(fetchImpl, url, init = {}, timeoutMs = resolveTimeoutMs()) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
82
|
+
try {
|
|
83
|
+
return await fetchImpl(url, { ...init, signal: controller.signal });
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (controller.signal.aborted && error?.name === 'AbortError') {
|
|
87
|
+
throw new Error(`Request timed out after ${timeoutMs} ms`);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function apiUrl(server, path) {
|
|
96
|
+
const base = server.replace(/\/$/, '');
|
|
97
|
+
return `${base}${path}`;
|
|
98
|
+
}
|
|
99
|
+
// 解析 http(s) URL 的 origin(scheme://host[:port]);非 http(s) 或无法解析时返回 null。
|
|
100
|
+
export function parseHttpOrigin(url) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = new URL(url);
|
|
103
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return parsed.origin;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ADR-0023:Source Remote origin 与当前配置 ESL Server origin 是否漂移
|
|
113
|
+
// (Server Origin 迁移检测)。任一侧不是 http(s) 时视为无漂移,维持既有行为。
|
|
114
|
+
export function sourceRemoteOriginDrifted(remoteUrl, server) {
|
|
115
|
+
const remoteOrigin = parseHttpOrigin(remoteUrl);
|
|
116
|
+
const serverOrigin = parseHttpOrigin(server);
|
|
117
|
+
return remoteOrigin !== null && serverOrigin !== null && remoteOrigin !== serverOrigin;
|
|
118
|
+
}
|
|
119
|
+
export function gitAuthHeaderConfig(token) {
|
|
120
|
+
return `http.extraHeader=Authorization: Bearer ${token}`;
|
|
121
|
+
}
|
|
122
|
+
export function installTargetDir(skillName, options) {
|
|
123
|
+
return path.join(resolveLocalStorePaths(options).skillsDir, skillSourceDirectoryName(skillName));
|
|
124
|
+
}
|
|
125
|
+
export function projectSkillsDir(projectRoot, skillName) {
|
|
126
|
+
return path.join(resolveProjectStorePaths(projectRoot).skillsDir, skillSourceDirectoryName(skillName));
|
|
127
|
+
}
|
|
128
|
+
export function publishedInstallTargetDir(skillName, options) {
|
|
129
|
+
return installTargetDir(skillName, options);
|
|
130
|
+
}
|
|
131
|
+
export function publishedProjectSkillsDir(projectRoot, skillName) {
|
|
132
|
+
return projectSkillsDir(projectRoot, skillName);
|
|
133
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NetworkCommandOptions } from './network-options.js';
|
|
2
|
+
export interface NotesOptions extends NetworkCommandOptions {
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
export interface UpdatedNotes {
|
|
6
|
+
skillName: string;
|
|
7
|
+
version: string;
|
|
8
|
+
notes?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function executeNotes(identity: string, version: string, options: NotesOptions): Promise<UpdatedNotes>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parseSkillName, isBuiltinIdentity } from '../vendor/core/index.js';
|
|
2
|
+
import { requireOkResponse } from '../api-error.js';
|
|
3
|
+
import { apiUrl, fetchWithTimeout, requireFreshToken, resolveNetworkConfig } from './network-options.js';
|
|
4
|
+
export async function executeNotes(identity, version, options) {
|
|
5
|
+
if (isBuiltinIdentity(identity)) {
|
|
6
|
+
throw new Error(`Unknown built-in skill: ${identity}; built-in skills have no releases`);
|
|
7
|
+
}
|
|
8
|
+
const token = await requireFreshToken(options);
|
|
9
|
+
const server = options.server ?? (await resolveNetworkConfig(options)).server;
|
|
10
|
+
const { scope, skillName } = parseSkillName(identity);
|
|
11
|
+
const fetchImpl = options.customFetch ?? fetch;
|
|
12
|
+
const response = await fetchWithTimeout(fetchImpl, apiUrl(server, `/api/skills/${encodeURIComponent(scope)}/${encodeURIComponent(skillName)}/releases/${encodeURIComponent(version)}/notes`), {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: {
|
|
15
|
+
Authorization: `token ${token}`,
|
|
16
|
+
'Content-Type': 'application/json'
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ message: options.message })
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
await requireOkResponse(response, 'Failed to update release notes');
|
|
22
|
+
}
|
|
23
|
+
return response.json();
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
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 PublishOptions extends NetworkCommandOptions {
|
|
5
|
+
directory?: string;
|
|
6
|
+
/** @deprecated Version is now read from release.json; pass --version to point users at `esl version` */
|
|
7
|
+
version?: string;
|
|
8
|
+
visibility?: string;
|
|
9
|
+
force?: boolean;
|
|
10
|
+
noInput?: boolean;
|
|
11
|
+
license?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
confirmInput?: () => Promise<boolean>;
|
|
14
|
+
noteInput?: (collected: string) => Promise<string>;
|
|
15
|
+
execFileAsync?: typeof defaultExecFileAsync;
|
|
16
|
+
dryRun?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function executePublish(options?: PublishOptions): Promise<unknown>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,271 @@
|
|
|
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 semver from 'semver';
|
|
6
|
+
import { fileExists, highestStableVersion, isBuiltinIdentity, validateSkillSourceDirectory } from '../vendor/core/index.js';
|
|
7
|
+
import { requireOkResponse } from '../api-error.js';
|
|
8
|
+
import { confirm, isInteractive, readText } from '../prompt.js';
|
|
9
|
+
import { apiUrl, fetchWithTimeout, gitAuthHeaderConfig, requireFreshToken, resolveNetworkConfig } from './network-options.js';
|
|
10
|
+
import { ensureReleaseManifest } from './release-manifest.js';
|
|
11
|
+
const defaultExecFileAsync = promisify(execFile);
|
|
12
|
+
export async function executePublish(options = {}) {
|
|
13
|
+
const directory = options.directory ?? process.cwd();
|
|
14
|
+
const skillJsonPath = path.join(directory, 'skill.json');
|
|
15
|
+
if (await fileExists(skillJsonPath)) {
|
|
16
|
+
const raw = await fs.readFile(skillJsonPath, 'utf8');
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (parsed.name && isBuiltinIdentity(parsed.name)) {
|
|
19
|
+
throw new Error('Built-in skills cannot be published; they are bundled with the ESL CLI');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!(await fileExists(`${directory}/release.json`))) {
|
|
23
|
+
await ensureReleaseManifest(options, directory);
|
|
24
|
+
throw new Error('Created release.json in the source directory; commit it, then run `esl version <SemVer>` to set the version before publishing.');
|
|
25
|
+
}
|
|
26
|
+
if (options.version !== undefined) {
|
|
27
|
+
throw new Error('`esl publish` no longer takes a version argument. The version is read from release.json. Use `esl version` (major | minor | patch | <SemVer>) to set the version first.');
|
|
28
|
+
}
|
|
29
|
+
return executeSourceRelease(options, directory);
|
|
30
|
+
}
|
|
31
|
+
async function executeSourceRelease(options, directory) {
|
|
32
|
+
const validation = await validateSkillSourceDirectory(directory);
|
|
33
|
+
if (!validation.success) {
|
|
34
|
+
throw new Error(`Invalid skill source: ${validation.errors.join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
const version = validation.data.releaseManifest.version;
|
|
37
|
+
const status = await git(options, directory, ['status', '--porcelain']);
|
|
38
|
+
if (status.trim()) {
|
|
39
|
+
throw new Error('Cannot publish: working tree is not clean');
|
|
40
|
+
}
|
|
41
|
+
let remoteUrl = '';
|
|
42
|
+
try {
|
|
43
|
+
remoteUrl = await git(options, directory, ['remote', 'get-url', 'esl']);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error('Cannot publish: the skill source has no esl remote; run esl upload first');
|
|
47
|
+
}
|
|
48
|
+
const identity = inferIdentityFromRemote(remoteUrl);
|
|
49
|
+
if (identity.startsWith('@local/')) {
|
|
50
|
+
throw new Error('@local/* skills use the local namespace and must be renamed to a stable namespace before publishing');
|
|
51
|
+
}
|
|
52
|
+
// A dry run stays entirely local: it validates the source and previews what
|
|
53
|
+
// would be released, without syncing the source or creating a Skill Release.
|
|
54
|
+
const head = options.dryRun
|
|
55
|
+
? (await git(options, directory, ['rev-parse', 'HEAD'])).trim()
|
|
56
|
+
: await syncSource(options, directory, await requireFreshToken(options));
|
|
57
|
+
await verifyReleaseTag(options, directory, version, head);
|
|
58
|
+
const notes = await resolveReleaseNotes(options, directory);
|
|
59
|
+
if (options.dryRun) {
|
|
60
|
+
return {
|
|
61
|
+
dryRun: true,
|
|
62
|
+
name: identity,
|
|
63
|
+
version,
|
|
64
|
+
sourceCommit: head,
|
|
65
|
+
files: Object.keys(await collectSourceFiles(directory)).sort(),
|
|
66
|
+
notes
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
await assertNotOlderThanPublished(options, identity, version);
|
|
70
|
+
await confirmPublish(options, identity, version);
|
|
71
|
+
const fetchImpl = options.customFetch ?? fetch;
|
|
72
|
+
const { server } = await resolveNetworkConfig(options);
|
|
73
|
+
const authToken = await requireFreshToken(options);
|
|
74
|
+
const response = await fetchWithTimeout(fetchImpl, apiUrl(server, `/api/skills/${encodeURIComponent(identity)}/releases`), {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: {
|
|
77
|
+
Authorization: `token ${authToken}`,
|
|
78
|
+
'Content-Type': 'application/json'
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
version,
|
|
82
|
+
sourceCommit: head,
|
|
83
|
+
releaseManifest: validation.data.releaseManifest,
|
|
84
|
+
files: await collectSourceFiles(directory),
|
|
85
|
+
notes
|
|
86
|
+
})
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
await requireOkResponse(response, 'Failed to publish Skill Release');
|
|
90
|
+
}
|
|
91
|
+
return response.json();
|
|
92
|
+
}
|
|
93
|
+
async function collectSourceFiles(directory) {
|
|
94
|
+
const files = {};
|
|
95
|
+
async function visit(current) {
|
|
96
|
+
for (const entry of await fs.readdir(current, { withFileTypes: true })) {
|
|
97
|
+
if (entry.name === '.git')
|
|
98
|
+
continue;
|
|
99
|
+
const fullPath = path.join(current, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
await visit(fullPath);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
files[path.relative(directory, fullPath).replaceAll(path.sep, '/')] = await fs.readFile(fullPath, 'utf8');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
await visit(directory);
|
|
109
|
+
return files;
|
|
110
|
+
}
|
|
111
|
+
async function git(options, directory, args) {
|
|
112
|
+
const execFileAsync = options.execFileAsync ?? defaultExecFileAsync;
|
|
113
|
+
const result = await execFileAsync('git', args, { cwd: directory });
|
|
114
|
+
return result.stdout;
|
|
115
|
+
}
|
|
116
|
+
async function resolveReleaseNotes(options, directory) {
|
|
117
|
+
if (options.message) {
|
|
118
|
+
return options.message;
|
|
119
|
+
}
|
|
120
|
+
const collected = await collectChangesSinceLastTag(options, directory);
|
|
121
|
+
if (options.noInput || options.noteInput) {
|
|
122
|
+
if (options.noteInput) {
|
|
123
|
+
return (await options.noteInput(collected)).trim() || collected;
|
|
124
|
+
}
|
|
125
|
+
return collected;
|
|
126
|
+
}
|
|
127
|
+
if (!isInteractive()) {
|
|
128
|
+
return collected;
|
|
129
|
+
}
|
|
130
|
+
if (collected) {
|
|
131
|
+
console.log(`\nChanges since the last release:\n${collected}\n`);
|
|
132
|
+
}
|
|
133
|
+
const answer = (await readText(collected ? 'Release note (Enter to use the above): ' : 'Release note (optional): ')).trim();
|
|
134
|
+
return answer || collected;
|
|
135
|
+
}
|
|
136
|
+
async function collectChangesSinceLastTag(options, directory) {
|
|
137
|
+
let lastTag = '';
|
|
138
|
+
try {
|
|
139
|
+
lastTag = (await git(options, directory, ['describe', '--tags', '--abbrev=0'])).trim();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
lastTag = '';
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
|
|
146
|
+
const subjects = (await git(options, directory, ['log', range, '--format=%s'])).trim();
|
|
147
|
+
if (!subjects) {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
return subjects.split('\n').map((subject) => `- ${subject.trim()}`).join('\n');
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Guard against publishing a version older than what is already released, which
|
|
158
|
+
* would silently burn a version number on an immutable release. Releasing an
|
|
159
|
+
* older line on purpose (a backport) stays possible through --force, which skips
|
|
160
|
+
* this check along with the confirmation prompt.
|
|
161
|
+
*/
|
|
162
|
+
async function assertNotOlderThanPublished(options, identity, version) {
|
|
163
|
+
if (options.force) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const fetchImpl = options.customFetch ?? fetch;
|
|
167
|
+
const { server } = await resolveNetworkConfig(options);
|
|
168
|
+
const authToken = await requireFreshToken(options);
|
|
169
|
+
let published = [];
|
|
170
|
+
try {
|
|
171
|
+
const response = await fetchWithTimeout(fetchImpl, apiUrl(server, `/api/skills/${encodeURIComponent(identity)}`), { headers: { Authorization: `token ${authToken}` } });
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
return; // best effort: an unreachable listing must not block a release
|
|
174
|
+
}
|
|
175
|
+
const info = (await response.json());
|
|
176
|
+
published = info.versions ?? [];
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const highest = highestStableVersion(published);
|
|
182
|
+
if (highest && semver.lt(version, highest)) {
|
|
183
|
+
throw new Error(`Cannot publish ${version}: ${highest} is already published and versions must move forward. Bump with \`esl version\` instead, or pass --force to release the older version on purpose.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Bring the server source up to date with the local branch before releasing, and
|
|
188
|
+
* return the commit that will be released. An already-synced source is left
|
|
189
|
+
* untouched (no push); a source that is behind is rebased and then pushed.
|
|
190
|
+
*/
|
|
191
|
+
async function syncSource(options, directory, authToken) {
|
|
192
|
+
const execFileAsync = options.execFileAsync ?? defaultExecFileAsync;
|
|
193
|
+
const authHeader = gitAuthHeaderConfig(authToken);
|
|
194
|
+
try {
|
|
195
|
+
await execFileAsync('git', ['-c', authHeader, 'fetch', 'esl'], { cwd: directory });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
199
|
+
throw new Error(`Cannot publish: failed to read the server source. Check that you are logged in and have access to this skill. (${detail})`);
|
|
200
|
+
}
|
|
201
|
+
let remoteHead = '';
|
|
202
|
+
try {
|
|
203
|
+
remoteHead = (await git(options, directory, ['rev-parse', 'esl/main'])).trim();
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
remoteHead = ''; // the server source has no commits yet
|
|
207
|
+
}
|
|
208
|
+
if ((await git(options, directory, ['rev-parse', 'HEAD'])).trim() === remoteHead) {
|
|
209
|
+
return remoteHead;
|
|
210
|
+
}
|
|
211
|
+
const behind = parseInt((await git(options, directory, ['rev-list', '--count', 'HEAD..esl/main'])).trim() || '0', 10);
|
|
212
|
+
if (behind > 0) {
|
|
213
|
+
try {
|
|
214
|
+
await execFileAsync('git', ['rebase', 'esl/main'], { cwd: directory });
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new Error('Cannot publish: the server source has newer commits and rebasing onto them hit a conflict. Resolve the conflicted files, finish the rebase, then run esl publish again.');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await execFileAsync('git', ['-c', authHeader, 'push', 'esl', 'HEAD:main'], { cwd: directory });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
225
|
+
throw new Error(`Cannot publish: failed to push the source to the server. (${detail})`);
|
|
226
|
+
}
|
|
227
|
+
return (await git(options, directory, ['rev-parse', 'HEAD'])).trim();
|
|
228
|
+
}
|
|
229
|
+
async function verifyReleaseTag(options, directory, version, head) {
|
|
230
|
+
const tag = `v${version}`;
|
|
231
|
+
try {
|
|
232
|
+
const tagCommit = (await git(options, directory, ['rev-list', '-n', '1', tag])).trim();
|
|
233
|
+
if (tagCommit !== head) {
|
|
234
|
+
throw new Error(`Tag ${tag} points to a different commit than HEAD. Run \`esl version\` on the commit you want to release.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
239
|
+
if (msg.includes('unknown revision') || msg.includes('bad revision') || msg.includes('fatal')) {
|
|
240
|
+
throw new Error(`No release tag ${tag} found on HEAD. Run \`esl version <release>\` to bump the version and create the tag before publishing.`);
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function inferIdentityFromRemote(remoteUrl) {
|
|
246
|
+
const match = remoteUrl.trim().match(/\/git\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
247
|
+
if (!match) {
|
|
248
|
+
throw new Error('Cannot determine Skill Identity from the esl remote URL');
|
|
249
|
+
}
|
|
250
|
+
return `@${match[1]}/${match[2]}`;
|
|
251
|
+
}
|
|
252
|
+
async function confirmPublish(options, name, version) {
|
|
253
|
+
if (options.force) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (options.noInput) {
|
|
257
|
+
throw new Error('Publishing requires confirmation; pass --force to skip it');
|
|
258
|
+
}
|
|
259
|
+
const question = `Publish ${name} v${version} to the ESL Server? `;
|
|
260
|
+
const confirmFn = options.confirmInput ??
|
|
261
|
+
(async () => {
|
|
262
|
+
if (!isInteractive()) {
|
|
263
|
+
throw new Error('Publishing requires confirmation in an interactive terminal; pass --force to skip');
|
|
264
|
+
}
|
|
265
|
+
return confirm(question);
|
|
266
|
+
});
|
|
267
|
+
const confirmed = await confirmFn();
|
|
268
|
+
if (!confirmed) {
|
|
269
|
+
throw new Error('Publish cancelled');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type NetworkCommandOptions } from './network-options.js';
|
|
2
|
+
export interface ReleaseDeleteOptions extends NetworkCommandOptions {
|
|
3
|
+
/** Must equal the version being deleted — the server's explicit-confirmation contract. */
|
|
4
|
+
confirm?: string;
|
|
5
|
+
/** Overrides the dependency-pinning guard; requires a platform administrator on the server. */
|
|
6
|
+
force?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface DeletedRelease {
|
|
9
|
+
deleted: boolean;
|
|
10
|
+
name: string;
|
|
11
|
+
version: string;
|
|
12
|
+
dependents: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Delete a single Skill Release (CONTEXT:单版本删除). The version number stays burned,
|
|
16
|
+
* so this is a deliberate, hard-to-undo action: the caller has to echo the version.
|
|
17
|
+
*
|
|
18
|
+
* A release other skills pin through their Release Dependency Lock is refused unless
|
|
19
|
+
* `force` is passed, and the server only accepts that from a platform administrator.
|
|
20
|
+
*/
|
|
21
|
+
export declare function executeReleaseDelete(identity: string, version: string, options?: ReleaseDeleteOptions): Promise<DeletedRelease>;
|