@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,376 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { loadInstallManifest, skillDirectoryName, skillSourceRelativeDir } from '../store/skill-store.js';
|
|
5
|
+
export const SUPPORTED_TOOLS = [
|
|
6
|
+
'claude',
|
|
7
|
+
'codex',
|
|
8
|
+
'cursor',
|
|
9
|
+
'trae-intl',
|
|
10
|
+
'trae-cn',
|
|
11
|
+
'workbuddy',
|
|
12
|
+
'opencode',
|
|
13
|
+
'openclaw',
|
|
14
|
+
'hermes'
|
|
15
|
+
];
|
|
16
|
+
const TOOL_LINK_MANIFEST_FILE = '.esl-tools-manifest.json';
|
|
17
|
+
export function isSupportedTool(value) {
|
|
18
|
+
return SUPPORTED_TOOLS.includes(value);
|
|
19
|
+
}
|
|
20
|
+
const TOOL_NAME_ALIASES = {
|
|
21
|
+
'claude-code': 'claude'
|
|
22
|
+
};
|
|
23
|
+
export function resolveToolName(value) {
|
|
24
|
+
const normalized = value.trim().toLowerCase();
|
|
25
|
+
return isSupportedTool(normalized) ? normalized : TOOL_NAME_ALIASES[normalized];
|
|
26
|
+
}
|
|
27
|
+
export function parseToolSelection(value) {
|
|
28
|
+
const raw = value.trim().toLowerCase();
|
|
29
|
+
if (raw === 'all') {
|
|
30
|
+
return [...SUPPORTED_TOOLS];
|
|
31
|
+
}
|
|
32
|
+
const tools = value
|
|
33
|
+
.split(',')
|
|
34
|
+
.map((tool) => tool.trim().toLowerCase())
|
|
35
|
+
.filter((tool) => tool.length > 0);
|
|
36
|
+
const resolved = tools.map((tool) => resolveToolName(tool));
|
|
37
|
+
for (const [index, tool] of resolved.entries()) {
|
|
38
|
+
if (tool === undefined) {
|
|
39
|
+
throw new Error(`Unknown tool: ${tools[index]}. Supported tools: ${SUPPORTED_TOOLS.join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
export function toolDirectory(tool, level, options = {}) {
|
|
45
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
46
|
+
const homeDir = options.homeDir ?? os.homedir();
|
|
47
|
+
switch (tool) {
|
|
48
|
+
case 'claude':
|
|
49
|
+
return level === 'project'
|
|
50
|
+
? path.join(projectRoot, '.claude', 'skills')
|
|
51
|
+
: path.join(homeDir, '.claude', 'skills');
|
|
52
|
+
case 'codex':
|
|
53
|
+
return level === 'project'
|
|
54
|
+
? path.join(projectRoot, '.codex', 'skills')
|
|
55
|
+
: path.join(homeDir, '.codex', 'skills');
|
|
56
|
+
case 'cursor':
|
|
57
|
+
return level === 'project'
|
|
58
|
+
? path.join(projectRoot, '.cursor', 'skills')
|
|
59
|
+
: path.join(homeDir, '.cursor', 'skills');
|
|
60
|
+
case 'trae-intl':
|
|
61
|
+
return level === 'project'
|
|
62
|
+
? path.join(projectRoot, '.trae', 'skills')
|
|
63
|
+
: path.join(homeDir, '.trae', 'skills');
|
|
64
|
+
case 'trae-cn':
|
|
65
|
+
return level === 'project'
|
|
66
|
+
? path.join(projectRoot, '.trae', 'skills')
|
|
67
|
+
: path.join(homeDir, '.trae-cn', 'skills');
|
|
68
|
+
case 'workbuddy':
|
|
69
|
+
return level === 'project'
|
|
70
|
+
? path.join(projectRoot, '.workbuddy', 'skills')
|
|
71
|
+
: path.join(homeDir, '.workbuddy', 'skills');
|
|
72
|
+
case 'opencode':
|
|
73
|
+
return level === 'project'
|
|
74
|
+
? path.join(projectRoot, '.opencode', 'skills')
|
|
75
|
+
: path.join(homeDir, '.config', 'opencode', 'skills');
|
|
76
|
+
case 'openclaw':
|
|
77
|
+
return level === 'project'
|
|
78
|
+
? path.join(projectRoot, 'skills')
|
|
79
|
+
: path.join(homeDir, '.openclaw', 'skills');
|
|
80
|
+
case 'hermes':
|
|
81
|
+
return level === 'project'
|
|
82
|
+
? path.join(projectRoot, '.hermes', 'skills')
|
|
83
|
+
: path.join(homeDir, '.hermes', 'skills');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export function toolLinkManifestPath(storeRoot) {
|
|
87
|
+
return path.join(storeRoot, TOOL_LINK_MANIFEST_FILE);
|
|
88
|
+
}
|
|
89
|
+
export function defaultToolLinkManifest() {
|
|
90
|
+
return { version: 1, links: [] };
|
|
91
|
+
}
|
|
92
|
+
export async function loadToolLinkManifest(storeRoot) {
|
|
93
|
+
try {
|
|
94
|
+
const raw = await fs.readFile(toolLinkManifestPath(storeRoot), 'utf8');
|
|
95
|
+
const parsed = JSON.parse(raw);
|
|
96
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.links)) {
|
|
97
|
+
throw new Error('Invalid ESL tools manifest');
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (error.code === 'ENOENT') {
|
|
103
|
+
return defaultToolLinkManifest();
|
|
104
|
+
}
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function saveToolLinkManifest(storeRoot, manifest) {
|
|
109
|
+
await fs.mkdir(storeRoot, { recursive: true });
|
|
110
|
+
await fs.writeFile(toolLinkManifestPath(storeRoot), `${JSON.stringify({ version: 1, links: manifest.links }, null, 2)}\n`, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
async function lstatPath(target) {
|
|
113
|
+
try {
|
|
114
|
+
return await fs.lstat(target);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function isLink(target) {
|
|
121
|
+
const stat = await lstatPath(target);
|
|
122
|
+
return stat?.isSymbolicLink() ?? false;
|
|
123
|
+
}
|
|
124
|
+
async function readLinkTarget(target) {
|
|
125
|
+
try {
|
|
126
|
+
return path.resolve(await fs.readlink(target));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function directoryExists(target) {
|
|
133
|
+
try {
|
|
134
|
+
return (await fs.stat(target)).isDirectory();
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function createLink(sourceDir, targetDir) {
|
|
141
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
142
|
+
if (process.platform === 'win32') {
|
|
143
|
+
await fs.symlink(sourceDir, targetDir, 'junction');
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
await fs.symlink(sourceDir, targetDir, 'dir');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function recordKey(record) {
|
|
150
|
+
return `${record.identity}\n${record.tool}\n${record.level}`;
|
|
151
|
+
}
|
|
152
|
+
export async function createToolLink(options) {
|
|
153
|
+
const { identity, tool, level, storeRoot, force = false } = options;
|
|
154
|
+
const sourceDir = path.resolve(storeRoot, skillSourceRelativeDir(identity));
|
|
155
|
+
const targetDir = path.resolve(toolDirectory(tool, level, options), skillDirectoryName(identity));
|
|
156
|
+
const manifest = await loadToolLinkManifest(storeRoot);
|
|
157
|
+
const existingRecord = manifest.links.find((record) => recordKey(record) === recordKey({ identity, tool, level }));
|
|
158
|
+
const targetStat = await lstatPath(targetDir);
|
|
159
|
+
if (targetStat) {
|
|
160
|
+
if (targetStat.isSymbolicLink()) {
|
|
161
|
+
const currentTarget = await readLinkTarget(targetDir);
|
|
162
|
+
if (currentTarget === sourceDir) {
|
|
163
|
+
if (existingRecord) {
|
|
164
|
+
return { identity, tool, targetDir, status: 'existing' };
|
|
165
|
+
}
|
|
166
|
+
const managedByAnotherTool = manifest.links.some((record) => record.level === level &&
|
|
167
|
+
targetKey(record.targetDir) === targetKey(targetDir) &&
|
|
168
|
+
path.resolve(storeRoot, record.sourceDir) === sourceDir);
|
|
169
|
+
if (managedByAnotherTool) {
|
|
170
|
+
manifest.links.push({
|
|
171
|
+
identity,
|
|
172
|
+
tool,
|
|
173
|
+
level,
|
|
174
|
+
sourceDir: path.relative(storeRoot, sourceDir),
|
|
175
|
+
targetDir,
|
|
176
|
+
createdAt: new Date().toISOString()
|
|
177
|
+
});
|
|
178
|
+
await saveToolLinkManifest(storeRoot, manifest);
|
|
179
|
+
return { identity, tool, targetDir, status: 'existing' };
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
identity,
|
|
183
|
+
tool,
|
|
184
|
+
targetDir,
|
|
185
|
+
status: 'conflict',
|
|
186
|
+
error: 'target link is unmanaged; ESL did not create it'
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (force && existingRecord && path.resolve(existingRecord.targetDir) === targetDir) {
|
|
190
|
+
await fs.rm(targetDir, { force: true });
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
return { identity, tool, targetDir, status: 'conflict' };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
return { identity, tool, targetDir, status: 'conflict' };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!await directoryExists(sourceDir)) {
|
|
201
|
+
return { identity, tool, targetDir, status: 'conflict' };
|
|
202
|
+
}
|
|
203
|
+
await createLink(sourceDir, targetDir);
|
|
204
|
+
const record = {
|
|
205
|
+
identity,
|
|
206
|
+
tool,
|
|
207
|
+
level,
|
|
208
|
+
sourceDir: path.relative(storeRoot, sourceDir),
|
|
209
|
+
targetDir,
|
|
210
|
+
createdAt: new Date().toISOString()
|
|
211
|
+
};
|
|
212
|
+
const nextLinks = manifest.links.filter((candidate) => recordKey(candidate) !== recordKey(record));
|
|
213
|
+
nextLinks.push(record);
|
|
214
|
+
await saveToolLinkManifest(storeRoot, { version: 1, links: nextLinks });
|
|
215
|
+
return { identity, tool, targetDir, status: 'created' };
|
|
216
|
+
}
|
|
217
|
+
export async function syncToolLinks(options) {
|
|
218
|
+
const manifest = await loadInstallManifest(options.storeRoot);
|
|
219
|
+
const results = [];
|
|
220
|
+
const identities = options.identities ?? Object.keys(manifest.skills);
|
|
221
|
+
for (const identity of identities) {
|
|
222
|
+
if (!manifest.skills[identity]) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
for (const tool of options.tools) {
|
|
226
|
+
try {
|
|
227
|
+
results.push(await createToolLink({
|
|
228
|
+
identity,
|
|
229
|
+
tool,
|
|
230
|
+
level: options.level,
|
|
231
|
+
storeRoot: options.storeRoot,
|
|
232
|
+
projectRoot: options.projectRoot,
|
|
233
|
+
homeDir: options.homeDir,
|
|
234
|
+
force: options.force
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
results.push({
|
|
239
|
+
identity,
|
|
240
|
+
tool,
|
|
241
|
+
targetDir: path.resolve(toolDirectory(tool, options.level, options), skillDirectoryName(identity)),
|
|
242
|
+
status: 'failed',
|
|
243
|
+
error: error.message
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
250
|
+
export async function removeToolLinks(options) {
|
|
251
|
+
const manifest = await loadToolLinkManifest(options.storeRoot);
|
|
252
|
+
const results = [];
|
|
253
|
+
const requestedTools = new Set(options.tools);
|
|
254
|
+
const remaining = [];
|
|
255
|
+
const requestedRecords = manifest.links.filter((record) => record.identity === options.identity &&
|
|
256
|
+
record.level === options.level &&
|
|
257
|
+
requestedTools.has(record.tool));
|
|
258
|
+
for (const record of manifest.links) {
|
|
259
|
+
if (!requestedRecords.includes(record)) {
|
|
260
|
+
remaining.push(record);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const targetDir = path.resolve(record.targetDir);
|
|
264
|
+
const expectedSourceDir = path.resolve(options.storeRoot, record.sourceDir);
|
|
265
|
+
const sharedReference = manifest.links.some((candidate) => !requestedRecords.includes(candidate) &&
|
|
266
|
+
candidate.level === record.level &&
|
|
267
|
+
targetKey(candidate.targetDir) === targetKey(record.targetDir));
|
|
268
|
+
if (await isLink(targetDir)) {
|
|
269
|
+
const currentTarget = await readLinkTarget(targetDir);
|
|
270
|
+
if (currentTarget === expectedSourceDir) {
|
|
271
|
+
if (!sharedReference) {
|
|
272
|
+
await fs.rm(targetDir, { force: true });
|
|
273
|
+
}
|
|
274
|
+
results.push({ identity: record.identity, tool: record.tool, targetDir, status: 'removed' });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
results.push({ identity: record.identity, tool: record.tool, targetDir, status: 'conflict' });
|
|
278
|
+
remaining.push(record);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (await directoryExists(targetDir)) {
|
|
282
|
+
results.push({ identity: record.identity, tool: record.tool, targetDir, status: 'conflict' });
|
|
283
|
+
remaining.push(record);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
results.push({ identity: record.identity, tool: record.tool, targetDir, status: 'missing' });
|
|
287
|
+
}
|
|
288
|
+
await saveToolLinkManifest(options.storeRoot, { version: 1, links: remaining });
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
291
|
+
export async function listToolLinks(options) {
|
|
292
|
+
const [installManifest, toolManifest] = await Promise.all([
|
|
293
|
+
loadInstallManifest(options.storeRoot),
|
|
294
|
+
loadToolLinkManifest(options.storeRoot)
|
|
295
|
+
]);
|
|
296
|
+
const entries = [];
|
|
297
|
+
const manifestRecords = toolManifest.links.filter((record) => record.level === options.level);
|
|
298
|
+
const recordTargets = new Set(manifestRecords.map((record) => targetKey(record.targetDir)));
|
|
299
|
+
for (const record of manifestRecords) {
|
|
300
|
+
const targetDir = path.resolve(record.targetDir);
|
|
301
|
+
const expectedSourceDir = path.resolve(options.storeRoot, record.sourceDir);
|
|
302
|
+
const stat = await lstatPath(targetDir);
|
|
303
|
+
let status;
|
|
304
|
+
if (!stat) {
|
|
305
|
+
status = 'broken';
|
|
306
|
+
}
|
|
307
|
+
else if (!stat.isSymbolicLink()) {
|
|
308
|
+
status = 'conflict';
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
const linkTarget = await readLinkTarget(targetDir);
|
|
312
|
+
if (linkTarget === expectedSourceDir && await directoryExists(expectedSourceDir)) {
|
|
313
|
+
status = 'linked';
|
|
314
|
+
}
|
|
315
|
+
else if (linkTarget && !await directoryExists(linkTarget)) {
|
|
316
|
+
status = 'broken';
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
status = 'conflict';
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
entries.push({
|
|
323
|
+
tool: record.tool,
|
|
324
|
+
identity: record.identity,
|
|
325
|
+
level: options.level,
|
|
326
|
+
status,
|
|
327
|
+
managed: true,
|
|
328
|
+
sourceDir: expectedSourceDir,
|
|
329
|
+
targetDir
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
for (const tool of SUPPORTED_TOOLS) {
|
|
333
|
+
const directory = toolDirectory(tool, options.level, options);
|
|
334
|
+
let dirents;
|
|
335
|
+
try {
|
|
336
|
+
dirents = await fs.readdir(directory, { withFileTypes: true });
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
for (const dirent of dirents) {
|
|
342
|
+
const targetDir = path.join(directory, dirent.name);
|
|
343
|
+
if (recordTargets.has(targetKey(targetDir))) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const stat = await lstatPath(targetDir);
|
|
347
|
+
entries.push({
|
|
348
|
+
tool,
|
|
349
|
+
identity: dirent.name,
|
|
350
|
+
level: options.level,
|
|
351
|
+
status: stat?.isDirectory() || stat?.isSymbolicLink() ? 'unmanaged' : 'conflict',
|
|
352
|
+
managed: false,
|
|
353
|
+
sourceDir: '',
|
|
354
|
+
targetDir
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const recordedIdentities = new Set(manifestRecords.map((record) => record.identity));
|
|
359
|
+
for (const identity of Object.keys(installManifest.skills)) {
|
|
360
|
+
if (!recordedIdentities.has(identity)) {
|
|
361
|
+
entries.push({
|
|
362
|
+
tool: 'source',
|
|
363
|
+
identity,
|
|
364
|
+
level: options.level,
|
|
365
|
+
status: 'source-only',
|
|
366
|
+
managed: true,
|
|
367
|
+
sourceDir: path.resolve(options.storeRoot, installManifest.skills[identity].sourceDir)
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return entries;
|
|
372
|
+
}
|
|
373
|
+
function targetKey(targetDir) {
|
|
374
|
+
const resolved = path.resolve(targetDir);
|
|
375
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
376
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ValidationResult } from '../schema/validation-result.js';
|
|
2
|
+
export declare const RESERVED_SCOPE_NAMES: ReadonlySet<string>;
|
|
3
|
+
export declare const DEFAULT_PASSWORD_MIN_LENGTH = 8;
|
|
4
|
+
export declare function validatePassword(password: string, minimumLength?: number): ValidationResult<string>;
|
|
5
|
+
export declare function validateMemberUsername(username: string): ValidationResult<string>;
|
|
6
|
+
export declare function normalizeSkillUserEmail(email: string): string;
|
|
7
|
+
export declare function validateSkillUserEmail(email: string): ValidationResult<string>;
|
|
8
|
+
export declare function giteaUserEmail(giteaUsername: string): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const ACCOUNT_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
2
|
+
const SKILL_USER_EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3
|
+
// 扁平命名池的保留名(ADR-0032):用户名与组织名共用同一约束,
|
|
4
|
+
// 保证保留 scope 与系统路径永远可用。
|
|
5
|
+
export const RESERVED_SCOPE_NAMES = new Set([
|
|
6
|
+
'local',
|
|
7
|
+
'builtin',
|
|
8
|
+
'admin',
|
|
9
|
+
'api',
|
|
10
|
+
'git',
|
|
11
|
+
'system'
|
|
12
|
+
]);
|
|
13
|
+
export const DEFAULT_PASSWORD_MIN_LENGTH = 8;
|
|
14
|
+
export function validatePassword(password, minimumLength = DEFAULT_PASSWORD_MIN_LENGTH) {
|
|
15
|
+
if (password.length < minimumLength) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
errors: [`password must be at least ${minimumLength} characters`]
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return { success: true, data: password };
|
|
22
|
+
}
|
|
23
|
+
export function validateMemberUsername(username) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if (username.length < 2 || username.length > 39) {
|
|
26
|
+
errors.push('username must be 2-39 characters');
|
|
27
|
+
}
|
|
28
|
+
if (!ACCOUNT_NAME_PATTERN.test(username)) {
|
|
29
|
+
errors.push('username may only contain lowercase letters, digits, and hyphens');
|
|
30
|
+
}
|
|
31
|
+
if (username.startsWith('-') || username.endsWith('-')) {
|
|
32
|
+
errors.push('username must not start or end with a hyphen');
|
|
33
|
+
}
|
|
34
|
+
if (RESERVED_SCOPE_NAMES.has(username)) {
|
|
35
|
+
errors.push('username is reserved');
|
|
36
|
+
}
|
|
37
|
+
return errors.length > 0 ? { success: false, errors } : { success: true, data: username };
|
|
38
|
+
}
|
|
39
|
+
export function normalizeSkillUserEmail(email) {
|
|
40
|
+
return email.trim().toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
export function validateSkillUserEmail(email) {
|
|
43
|
+
const normalized = normalizeSkillUserEmail(email);
|
|
44
|
+
const errors = [];
|
|
45
|
+
if (!SKILL_USER_EMAIL_PATTERN.test(normalized)) {
|
|
46
|
+
errors.push('email must be a valid address');
|
|
47
|
+
}
|
|
48
|
+
if (normalized.endsWith('@local.esl')) {
|
|
49
|
+
errors.push('email domain is reserved');
|
|
50
|
+
}
|
|
51
|
+
return errors.length > 0 ? { success: false, errors } : { success: true, data: normalized };
|
|
52
|
+
}
|
|
53
|
+
// Git Backend 用户账号的 email 约定(用户创建流程统一使用该格式):以该 email
|
|
54
|
+
// 作为 commit author 时,Git Backend 可通过 email 把提交匹配到对应账号。
|
|
55
|
+
export function giteaUserEmail(giteaUsername) {
|
|
56
|
+
return `${giteaUsername}@local.esl`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RESERVED_SCOPE_NAMES } from './account-policy.js';
|
|
2
|
+
const ORG_NAME_PATTERN = /^[a-z0-9-]+$/;
|
|
3
|
+
export function validateOrgName(name) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
if (name.length < 2 || name.length > 39) {
|
|
6
|
+
errors.push('organization name must be 2-39 characters');
|
|
7
|
+
return { success: false, errors };
|
|
8
|
+
}
|
|
9
|
+
if (!ORG_NAME_PATTERN.test(name)) {
|
|
10
|
+
errors.push('organization name may only contain lowercase letters, digits, and hyphens');
|
|
11
|
+
}
|
|
12
|
+
if (name.startsWith('-') || name.endsWith('-')) {
|
|
13
|
+
errors.push('organization name must not start or end with a hyphen');
|
|
14
|
+
}
|
|
15
|
+
if (RESERVED_SCOPE_NAMES.has(name)) {
|
|
16
|
+
errors.push('organization name is reserved');
|
|
17
|
+
}
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
return { success: false, errors };
|
|
20
|
+
}
|
|
21
|
+
return { success: true, data: name };
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const STANDING_TEAM_NAMES: readonly string[];
|
|
2
|
+
export declare function isStandingTeam(teamName: string): boolean;
|
|
3
|
+
export declare const SHARE_TIER_TEAM_NAMES: {
|
|
4
|
+
readonly read: "all-readers";
|
|
5
|
+
readonly write: "all-writers";
|
|
6
|
+
readonly manage: "all-managers";
|
|
7
|
+
};
|
|
8
|
+
export type OrgIdentity = 'ordinary' | 'managing' | 'owner';
|
|
9
|
+
export declare const AUTO_JOIN_TEAM_NAMES: readonly string[];
|
|
10
|
+
export declare const MANAGING_TEAM_NAME = "org-managers";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 常设团队(ADR-0032):只读 / 读写 / 技能管理三个团队随成员进出组织自动增删,
|
|
2
|
+
// 不可删除、不可改名;管理员团队就是 Git Backend 原生的 Owners。
|
|
3
|
+
// 这里是团队标识名的单一来源——服务端与前端都不再各写一份。
|
|
4
|
+
export const STANDING_TEAM_NAMES = [
|
|
5
|
+
'all-readers',
|
|
6
|
+
'all-writers',
|
|
7
|
+
'all-managers',
|
|
8
|
+
'org-managers'
|
|
9
|
+
];
|
|
10
|
+
export function isStandingTeam(teamName) {
|
|
11
|
+
return STANDING_TEAM_NAMES.includes(teamName);
|
|
12
|
+
}
|
|
13
|
+
// 授权档位 → 常设团队标识名(share_all_* 动作的落点)
|
|
14
|
+
export const SHARE_TIER_TEAM_NAMES = {
|
|
15
|
+
read: 'all-readers',
|
|
16
|
+
write: 'all-writers',
|
|
17
|
+
manage: 'all-managers'
|
|
18
|
+
};
|
|
19
|
+
// 成员加入组织时**自动**加入的三个技能授权团队。管理成员身份团队 org-managers
|
|
20
|
+
// 不在其中,必须由所有者成员通过身份变更授予。
|
|
21
|
+
export const AUTO_JOIN_TEAM_NAMES = [
|
|
22
|
+
'all-readers',
|
|
23
|
+
'all-writers',
|
|
24
|
+
'all-managers'
|
|
25
|
+
];
|
|
26
|
+
// 承载管理成员身份的专用常设团队。技能共享级别 manage 的落点仍是 all-managers。
|
|
27
|
+
export const MANAGING_TEAM_NAME = 'org-managers';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
declare const execFileAsync: typeof execFile.__promisify__;
|
|
3
|
+
export interface CompatibilityRequirements {
|
|
4
|
+
tools?: string[];
|
|
5
|
+
languages?: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface CompatibilityResult {
|
|
8
|
+
compatible: boolean;
|
|
9
|
+
missingTools: string[];
|
|
10
|
+
unsupportedLanguages: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function evaluateCompatibility(requirements: CompatibilityRequirements, options?: {
|
|
13
|
+
execFileAsync?: typeof execFileAsync;
|
|
14
|
+
platform?: string;
|
|
15
|
+
nodeVersion?: string;
|
|
16
|
+
}): Promise<CompatibilityResult>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export async function evaluateCompatibility(requirements, options = {}) {
|
|
5
|
+
const runner = options.execFileAsync ?? execFileAsync;
|
|
6
|
+
const missingTools = [];
|
|
7
|
+
for (const tool of requirements.tools ?? []) {
|
|
8
|
+
try {
|
|
9
|
+
await runner(process.platform === 'win32' ? 'where' : 'which', [tool]);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
missingTools.push(tool);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const platform = options.platform ?? process.platform;
|
|
16
|
+
const supportedLanguages = new Set(['node', 'nodejs', 'javascript', 'typescript']);
|
|
17
|
+
if (platform === 'win32')
|
|
18
|
+
supportedLanguages.add('powershell');
|
|
19
|
+
const unsupportedLanguages = [];
|
|
20
|
+
for (const language of requirements.languages ?? []) {
|
|
21
|
+
const normalized = language.toLowerCase();
|
|
22
|
+
if (supportedLanguages.has(normalized))
|
|
23
|
+
continue;
|
|
24
|
+
if (normalized === 'python') {
|
|
25
|
+
let found = false;
|
|
26
|
+
for (const candidate of ['python', 'python3']) {
|
|
27
|
+
try {
|
|
28
|
+
await runner(process.platform === 'win32' ? 'where' : 'which', [candidate]);
|
|
29
|
+
found = true;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (found)
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
unsupportedLanguages.push(language);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
compatible: missingTools.length === 0 && unsupportedLanguages.length === 0,
|
|
42
|
+
missingTools,
|
|
43
|
+
unsupportedLanguages
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ValidationResult } from './validation-result.js';
|
|
3
|
+
import { type SkillMdMetadata } from '../skill/skill-md.js';
|
|
4
|
+
export declare const SkillIdentitySchema: z.ZodString;
|
|
5
|
+
export declare const DISPLAY_NAME_MAX_LENGTH = 128;
|
|
6
|
+
export declare const ReleaseManifestSchema: z.ZodEffects<z.ZodObject<{
|
|
7
|
+
schemaVersion: z.ZodUnion<[z.ZodLiteral<3>, z.ZodLiteral<4>]>;
|
|
8
|
+
name: z.ZodString;
|
|
9
|
+
version: z.ZodString;
|
|
10
|
+
license: z.ZodUnion<[z.ZodString, z.ZodString]>;
|
|
11
|
+
displayName: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
|
|
12
|
+
keywords: z.ZodArray<z.ZodString, "many">;
|
|
13
|
+
compatibility: z.ZodObject<{
|
|
14
|
+
tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
15
|
+
languages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
16
|
+
}, "strip", z.ZodTypeAny, {
|
|
17
|
+
tools?: string[] | undefined;
|
|
18
|
+
languages?: string[] | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
tools?: string[] | undefined;
|
|
21
|
+
languages?: string[] | undefined;
|
|
22
|
+
}>;
|
|
23
|
+
dependencies: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
24
|
+
}, "strict", z.ZodTypeAny, {
|
|
25
|
+
name: string;
|
|
26
|
+
version: string;
|
|
27
|
+
license: string;
|
|
28
|
+
keywords: string[];
|
|
29
|
+
compatibility: {
|
|
30
|
+
tools?: string[] | undefined;
|
|
31
|
+
languages?: string[] | undefined;
|
|
32
|
+
};
|
|
33
|
+
dependencies: Record<string, string>;
|
|
34
|
+
schemaVersion: 3 | 4;
|
|
35
|
+
displayName?: string | undefined;
|
|
36
|
+
}, {
|
|
37
|
+
name: string;
|
|
38
|
+
version: string;
|
|
39
|
+
license: string;
|
|
40
|
+
keywords: string[];
|
|
41
|
+
compatibility: {
|
|
42
|
+
tools?: string[] | undefined;
|
|
43
|
+
languages?: string[] | undefined;
|
|
44
|
+
};
|
|
45
|
+
dependencies: Record<string, string>;
|
|
46
|
+
schemaVersion: 3 | 4;
|
|
47
|
+
displayName?: string | undefined;
|
|
48
|
+
}>, {
|
|
49
|
+
name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
license: string;
|
|
52
|
+
keywords: string[];
|
|
53
|
+
compatibility: {
|
|
54
|
+
tools?: string[] | undefined;
|
|
55
|
+
languages?: string[] | undefined;
|
|
56
|
+
};
|
|
57
|
+
dependencies: Record<string, string>;
|
|
58
|
+
schemaVersion: 3 | 4;
|
|
59
|
+
displayName?: string | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
license: string;
|
|
64
|
+
keywords: string[];
|
|
65
|
+
compatibility: {
|
|
66
|
+
tools?: string[] | undefined;
|
|
67
|
+
languages?: string[] | undefined;
|
|
68
|
+
};
|
|
69
|
+
dependencies: Record<string, string>;
|
|
70
|
+
schemaVersion: 3 | 4;
|
|
71
|
+
displayName?: string | undefined;
|
|
72
|
+
}>;
|
|
73
|
+
export type ReleaseManifest = z.infer<typeof ReleaseManifestSchema>;
|
|
74
|
+
export interface SkillIdentity {
|
|
75
|
+
/** 持有命名空间的组织名;null 表示上传者个人命名空间(@用户名)。 */
|
|
76
|
+
scope: string | null;
|
|
77
|
+
shortName: string;
|
|
78
|
+
}
|
|
79
|
+
export declare function parseSkillIdentity(name: string): SkillIdentity | null;
|
|
80
|
+
export declare function titleCaseDisplayName(name: string): string;
|
|
81
|
+
export declare function createMinimalReleaseManifest(name: string, license: string, displayName?: string): ReleaseManifest;
|
|
82
|
+
export declare function validateReleaseManifest(data: unknown): ValidationResult<ReleaseManifest>;
|
|
83
|
+
export interface SkillSourceDirectory {
|
|
84
|
+
directory: string;
|
|
85
|
+
skillMd: SkillMdMetadata;
|
|
86
|
+
releaseManifest: ReleaseManifest;
|
|
87
|
+
}
|
|
88
|
+
export declare function validateSkillSourceDirectory(directory: string): Promise<ValidationResult<SkillSourceDirectory>>;
|