transskill 0.2.9 → 0.3.1
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/dist/index.js +121 -0
- package/dist/index.js.map +1 -1
- package/dist/marketplace/install.d.ts +26 -0
- package/dist/marketplace/install.d.ts.map +1 -0
- package/dist/marketplace/install.js +217 -0
- package/dist/marketplace/install.js.map +1 -0
- package/dist/marketplace/publish-all.d.ts +15 -0
- package/dist/marketplace/publish-all.d.ts.map +1 -0
- package/dist/marketplace/publish-all.js +169 -0
- package/dist/marketplace/publish-all.js.map +1 -0
- package/dist/marketplace/publish.d.ts +21 -0
- package/dist/marketplace/publish.d.ts.map +1 -0
- package/dist/marketplace/publish.js +481 -0
- package/dist/marketplace/publish.js.map +1 -0
- package/dist/marketplace/search.d.ts +20 -0
- package/dist/marketplace/search.d.ts.map +1 -0
- package/dist/marketplace/search.js +116 -0
- package/dist/marketplace/search.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish-all command — batch publish all skills from a directory to the registry.
|
|
3
|
+
*
|
|
4
|
+
* Scans a directory (e.g. anthropic-skills/skills/) for skill subdirectories,
|
|
5
|
+
* adds default metadata if missing, and publishes each one.
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, existsSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { intro, outro, spinner, log, confirm } from '@clack/prompts';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import matter from 'gray-matter';
|
|
12
|
+
const REGISTRY_OWNER = 'ljk-777';
|
|
13
|
+
const REGISTRY_REPO = 'transskill-registry';
|
|
14
|
+
/**
|
|
15
|
+
* Scan a directory for skill subdirectories.
|
|
16
|
+
*/
|
|
17
|
+
function scanSkills(baseDir) {
|
|
18
|
+
if (!existsSync(baseDir)) {
|
|
19
|
+
log.error(`Directory not found: ${baseDir}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const entries = readdirSync(baseDir);
|
|
23
|
+
const skills = [];
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const fullPath = join(baseDir, entry);
|
|
26
|
+
if (!statSync(fullPath).isDirectory())
|
|
27
|
+
continue;
|
|
28
|
+
const skillMdPath = join(fullPath, 'SKILL.md');
|
|
29
|
+
if (!existsSync(skillMdPath))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(skillMdPath, 'utf-8');
|
|
33
|
+
const parsed = matter(raw);
|
|
34
|
+
const fm = parsed.data;
|
|
35
|
+
// Determine skill name
|
|
36
|
+
const name = typeof fm.name === 'string' ? fm.name : entry;
|
|
37
|
+
skills.push({
|
|
38
|
+
name,
|
|
39
|
+
dirPath: fullPath,
|
|
40
|
+
skillMdPath,
|
|
41
|
+
frontmatter: fm,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
log.warn(` Skipped ${entry}/SKILL.md (parse error)`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return skills;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Ensure required frontmatter fields exist.
|
|
52
|
+
*/
|
|
53
|
+
function ensureFrontmatter(skill, defaultAuthor) {
|
|
54
|
+
const fm = skill.frontmatter;
|
|
55
|
+
let modified = false;
|
|
56
|
+
if (!fm.description || typeof fm.description !== 'string') {
|
|
57
|
+
log.warn(` ${skill.name}: missing description — skipping`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(fm.tags) || fm.tags.length === 0) {
|
|
61
|
+
// Auto-generate tags from path name
|
|
62
|
+
const autoTags = [skill.name.split('-')[0] || skill.name];
|
|
63
|
+
fm.tags = autoTags;
|
|
64
|
+
modified = true;
|
|
65
|
+
}
|
|
66
|
+
if (!fm.author || typeof fm.author !== 'string') {
|
|
67
|
+
fm.author = defaultAuthor;
|
|
68
|
+
modified = true;
|
|
69
|
+
}
|
|
70
|
+
if (!fm.version || typeof fm.version !== 'string') {
|
|
71
|
+
fm.version = '1.0.0';
|
|
72
|
+
modified = true;
|
|
73
|
+
}
|
|
74
|
+
// Write back if modified
|
|
75
|
+
if (modified) {
|
|
76
|
+
const raw = readFileSync(skill.skillMdPath, 'utf-8');
|
|
77
|
+
const parsed = matter(raw);
|
|
78
|
+
const newContent = matter.stringify(parsed.content, skill.frontmatter);
|
|
79
|
+
writeFileSync(skill.skillMdPath, newContent, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Publish all skills in a directory.
|
|
85
|
+
*/
|
|
86
|
+
export async function publishAllSkills(dir, options) {
|
|
87
|
+
const baseDir = dir.startsWith('/') ? dir : join(process.cwd(), dir);
|
|
88
|
+
const defaultAuthor = options.author || 'anthropic';
|
|
89
|
+
intro(chalk.green('📤 TransSkill Batch Publish'));
|
|
90
|
+
// Step 1: Scan for skills
|
|
91
|
+
const spin = spinner();
|
|
92
|
+
spin.start(`Scanning ${baseDir}…`);
|
|
93
|
+
const skills = scanSkills(baseDir);
|
|
94
|
+
spin.stop(`Found ${skills.length} skill(s)`);
|
|
95
|
+
if (skills.length === 0) {
|
|
96
|
+
log.info('No skills found (directories must contain SKILL.md)');
|
|
97
|
+
outro('Done');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// List skills
|
|
101
|
+
console.log('');
|
|
102
|
+
for (const s of skills) {
|
|
103
|
+
const hasTags = Array.isArray(s.frontmatter.tags) && s.frontmatter.tags.length > 0;
|
|
104
|
+
const hasAuthor = typeof s.frontmatter.author === 'string';
|
|
105
|
+
const isComplete = hasTags && hasAuthor;
|
|
106
|
+
console.log(` ${isComplete ? chalk.green('✓') : chalk.yellow('~')} ${chalk.bold(s.name)}` +
|
|
107
|
+
`${hasTags ? '' : chalk.gray(' [no tags]')}` +
|
|
108
|
+
`${hasAuthor ? '' : chalk.gray(' [no author]')}`);
|
|
109
|
+
}
|
|
110
|
+
console.log('');
|
|
111
|
+
// Confirm
|
|
112
|
+
if (!options.dryRun && !options.force) {
|
|
113
|
+
const proceed = await confirm({
|
|
114
|
+
message: `Publish ${skills.length} skill(s) to ${chalk.cyan(`${REGISTRY_OWNER}/${REGISTRY_REPO}`)}?`,
|
|
115
|
+
active: 'Yes, publish all',
|
|
116
|
+
inactive: 'No, cancel',
|
|
117
|
+
initialValue: false,
|
|
118
|
+
});
|
|
119
|
+
if (!proceed) {
|
|
120
|
+
outro('Cancelled');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Step 2: Process each skill
|
|
125
|
+
let succeeded = 0;
|
|
126
|
+
let skipped = 0;
|
|
127
|
+
let failed = 0;
|
|
128
|
+
for (const skill of skills) {
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(chalk.cyan(`── ${skill.name} ──`));
|
|
131
|
+
// Ensure frontmatter
|
|
132
|
+
if (!ensureFrontmatter(skill, defaultAuthor)) {
|
|
133
|
+
skipped++;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (options.dryRun) {
|
|
137
|
+
console.log(` ${chalk.gray('→ would publish')}`);
|
|
138
|
+
succeeded++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Publish via the existing publish flow
|
|
142
|
+
try {
|
|
143
|
+
// Dynamic import to avoid circular dependency
|
|
144
|
+
const { publishSkill } = await import('./publish.js');
|
|
145
|
+
try {
|
|
146
|
+
await publishSkill(skill.dirPath, {
|
|
147
|
+
force: true,
|
|
148
|
+
dryRun: false,
|
|
149
|
+
});
|
|
150
|
+
succeeded++;
|
|
151
|
+
}
|
|
152
|
+
catch (pErr) {
|
|
153
|
+
// PublishError means messages already printed, just count as failure
|
|
154
|
+
failed++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
159
|
+
log.error(` Failed: ${message}`);
|
|
160
|
+
failed++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Summary
|
|
164
|
+
console.log('');
|
|
165
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
166
|
+
console.log(` ${chalk.green(`✓ ${succeeded} published`)}${skipped > 0 ? chalk.yellow(`, ${skipped} skipped`) : ''}${failed > 0 ? chalk.red(`, ${failed} failed`) : ''}`);
|
|
167
|
+
outro('Batch publish complete');
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=publish-all.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish-all.js","sourceRoot":"","sources":["../../src/marketplace/publish-all.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish command — audit, fork, and PR a skill to the registry.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Validate skill directory (SKILL.md + frontmatter)
|
|
6
|
+
* 2. Parse + audit (mandatory, score >= 90 unless --force)
|
|
7
|
+
* 3. GitHub API: fork → branch → upload SKILL.md → update registry.json → PR
|
|
8
|
+
*/
|
|
9
|
+
/** Error thrown to abort publish (messages already printed via log/outro). */
|
|
10
|
+
export declare class PublishError extends Error {
|
|
11
|
+
constructor(msg?: string);
|
|
12
|
+
}
|
|
13
|
+
export interface PublishOptions {
|
|
14
|
+
force?: boolean;
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Publish a skill directory to the registry.
|
|
19
|
+
*/
|
|
20
|
+
export declare function publishSkill(skillPath: string, options: PublishOptions): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=publish.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/marketplace/publish.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAyBH,8EAA8E;AAC9E,qBAAa,YAAa,SAAQ,KAAK;gBACzB,GAAG,CAAC,EAAE,MAAM;CAIzB;AAsMD,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA0V5F"}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Publish command — audit, fork, and PR a skill to the registry.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Validate skill directory (SKILL.md + frontmatter)
|
|
6
|
+
* 2. Parse + audit (mandatory, score >= 90 unless --force)
|
|
7
|
+
* 3. GitHub API: fork → branch → upload SKILL.md → update registry.json → PR
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { intro, outro, spinner, log } from '@clack/prompts';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import matter from 'gray-matter';
|
|
14
|
+
import { AuditEngine } from '../audit/index.js';
|
|
15
|
+
import { getParser } from '../parser/parser-registry.js';
|
|
16
|
+
// ──────────────────────────────────────────────
|
|
17
|
+
// Constants
|
|
18
|
+
// ──────────────────────────────────────────────
|
|
19
|
+
const REGISTRY_OWNER = 'ljk-777';
|
|
20
|
+
const REGISTRY_REPO = 'transskill-registry';
|
|
21
|
+
const REGISTRY_FULL = `${REGISTRY_OWNER}/${REGISTRY_REPO}`;
|
|
22
|
+
const GITHUB_API = 'https://api.github.com';
|
|
23
|
+
// ──────────────────────────────────────────────
|
|
24
|
+
// Error type
|
|
25
|
+
// ──────────────────────────────────────────────
|
|
26
|
+
/** Error thrown to abort publish (messages already printed via log/outro). */
|
|
27
|
+
export class PublishError extends Error {
|
|
28
|
+
constructor(msg) {
|
|
29
|
+
super(msg || 'Publish aborted');
|
|
30
|
+
this.name = 'PublishError';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// ──────────────────────────────────────────────
|
|
34
|
+
// GitHub API helpers
|
|
35
|
+
// ──────────────────────────────────────────────
|
|
36
|
+
function getToken() {
|
|
37
|
+
const token = process.env.GITHUB_TOKEN;
|
|
38
|
+
if (!token) {
|
|
39
|
+
console.error('');
|
|
40
|
+
log.error('GITHUB_TOKEN environment variable not set.');
|
|
41
|
+
log.info('Create a token at https://github.com/settings/tokens');
|
|
42
|
+
log.info('Then set: export GITHUB_TOKEN=ghp_xxx');
|
|
43
|
+
console.error('');
|
|
44
|
+
throw new PublishError();
|
|
45
|
+
}
|
|
46
|
+
return token;
|
|
47
|
+
}
|
|
48
|
+
function ghHeaders(token) {
|
|
49
|
+
return {
|
|
50
|
+
Authorization: `Bearer ${token}`,
|
|
51
|
+
Accept: 'application/vnd.github+json',
|
|
52
|
+
'User-Agent': 'transskill-cli',
|
|
53
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function ghFetch(url, token, options = {}) {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
...options,
|
|
59
|
+
headers: {
|
|
60
|
+
...ghHeaders(token),
|
|
61
|
+
...options.headers,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return res;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the authenticated user's GitHub login.
|
|
68
|
+
*/
|
|
69
|
+
async function getGitHubUser(token) {
|
|
70
|
+
const res = await ghFetch(`${GITHUB_API}/user`, token);
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
throw new Error(`Failed to authenticate: ${res.status} ${res.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
return data.login;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the default branch of the registry repo.
|
|
79
|
+
*/
|
|
80
|
+
async function getDefaultBranch(token) {
|
|
81
|
+
const res = await ghFetch(`${GITHUB_API}/repos/${REGISTRY_FULL}`, token);
|
|
82
|
+
if (!res.ok)
|
|
83
|
+
throw new Error(`Failed to get repo info: ${res.status}`);
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
const branch = data.default_branch;
|
|
86
|
+
// Get the SHA of the latest commit on the default branch
|
|
87
|
+
const refRes = await ghFetch(`${GITHUB_API}/repos/${REGISTRY_FULL}/git/ref/heads/${branch}`, token);
|
|
88
|
+
if (!refRes.ok)
|
|
89
|
+
throw new Error(`Failed to get branch ref: ${refRes.status}`);
|
|
90
|
+
const refData = await refRes.json();
|
|
91
|
+
return { name: branch, sha: refData.object.sha };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a fork of the registry repo (no-op if already exists).
|
|
95
|
+
*/
|
|
96
|
+
async function ensureFork(token, user) {
|
|
97
|
+
// Check if fork exists
|
|
98
|
+
const checkRes = await ghFetch(`${GITHUB_API}/repos/${user}/${REGISTRY_REPO}`, token);
|
|
99
|
+
if (checkRes.ok) {
|
|
100
|
+
// Fork exists
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
log.info('Creating fork of transskill-registry…');
|
|
104
|
+
const res = await ghFetch(`${GITHUB_API}/repos/${REGISTRY_FULL}/forks`, token, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body: JSON.stringify({}),
|
|
107
|
+
});
|
|
108
|
+
if (res.status === 202) {
|
|
109
|
+
log.info('Fork creation in progress (this may take a few seconds)…');
|
|
110
|
+
// Wait for fork to be ready
|
|
111
|
+
let ready = false;
|
|
112
|
+
let attempts = 0;
|
|
113
|
+
while (!ready && attempts < 30) {
|
|
114
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
115
|
+
const check = await ghFetch(`${GITHUB_API}/repos/${user}/${REGISTRY_REPO}`, token);
|
|
116
|
+
if (check.ok)
|
|
117
|
+
ready = true;
|
|
118
|
+
attempts++;
|
|
119
|
+
}
|
|
120
|
+
if (!ready)
|
|
121
|
+
throw new Error('Timed out waiting for fork to be created');
|
|
122
|
+
}
|
|
123
|
+
else if (!res.ok) {
|
|
124
|
+
const body = await res.json().catch(() => ({}));
|
|
125
|
+
throw new Error(`Failed to fork repo: ${res.status} ${body.message || res.statusText}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create or update a file in the fork via GitHub API.
|
|
130
|
+
*/
|
|
131
|
+
async function upsertFile(token, user, branch, path, content, message) {
|
|
132
|
+
// Try to get existing file SHA
|
|
133
|
+
let existingSha;
|
|
134
|
+
const getRes = await ghFetch(`${GITHUB_API}/repos/${user}/${REGISTRY_REPO}/contents/${path}?ref=${branch}`, token);
|
|
135
|
+
if (getRes.ok) {
|
|
136
|
+
const data = await getRes.json();
|
|
137
|
+
existingSha = data.sha;
|
|
138
|
+
}
|
|
139
|
+
// Base64 encode content
|
|
140
|
+
const base64 = Buffer.from(content, 'utf-8').toString('base64');
|
|
141
|
+
const body = {
|
|
142
|
+
message,
|
|
143
|
+
content: base64,
|
|
144
|
+
branch,
|
|
145
|
+
};
|
|
146
|
+
if (existingSha) {
|
|
147
|
+
body.sha = existingSha;
|
|
148
|
+
}
|
|
149
|
+
const res = await ghFetch(`${GITHUB_API}/repos/${user}/${REGISTRY_REPO}/contents/${path}`, token, {
|
|
150
|
+
method: 'PUT',
|
|
151
|
+
body: JSON.stringify(body),
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const errBody = await res.json().catch(() => ({}));
|
|
155
|
+
throw new Error(`Failed to write ${path}: ${res.status} ${errBody.message || res.statusText}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Create a pull request.
|
|
160
|
+
*/
|
|
161
|
+
async function createPR(token, user, branch, title, body) {
|
|
162
|
+
const res = await ghFetch(`${GITHUB_API}/repos/${REGISTRY_FULL}/pulls`, token, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: JSON.stringify({
|
|
165
|
+
title,
|
|
166
|
+
body,
|
|
167
|
+
head: `${user}:${branch}`,
|
|
168
|
+
base: 'main',
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const errBody = await res.json().catch(() => ({}));
|
|
173
|
+
throw new Error(`Failed to create PR: ${res.status} ${errBody.message || res.statusText}`);
|
|
174
|
+
}
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
return data.html_url;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Publish a skill directory to the registry.
|
|
180
|
+
*/
|
|
181
|
+
export async function publishSkill(skillPath, options) {
|
|
182
|
+
const spin = spinner();
|
|
183
|
+
intro(chalk.green('📤 TransSkill Publish'));
|
|
184
|
+
// Step 1: Resolve and validate skill directory
|
|
185
|
+
const skillDirPath = skillPath.startsWith('/')
|
|
186
|
+
? skillPath
|
|
187
|
+
: join(process.cwd(), skillPath);
|
|
188
|
+
if (!existsSync(skillDirPath)) {
|
|
189
|
+
log.error(`Path does not exist: ${skillDirPath}`);
|
|
190
|
+
outro('Publish cancelled');
|
|
191
|
+
throw new PublishError();
|
|
192
|
+
}
|
|
193
|
+
const stat = statSync(skillDirPath);
|
|
194
|
+
if (!stat.isDirectory()) {
|
|
195
|
+
log.error(`Expected a directory, got a file: ${skillDirPath}`);
|
|
196
|
+
log.info('Usage: transskill publish ./my-skill/');
|
|
197
|
+
outro('Publish cancelled');
|
|
198
|
+
throw new PublishError();
|
|
199
|
+
}
|
|
200
|
+
// Check for SKILL.md
|
|
201
|
+
const skillMdPath = join(skillDirPath, 'SKILL.md');
|
|
202
|
+
if (!existsSync(skillMdPath)) {
|
|
203
|
+
log.error(`Directory does not contain SKILL.md: ${skillDirPath}`);
|
|
204
|
+
log.info('A valid skill directory must have a SKILL.md file with frontmatter.');
|
|
205
|
+
outro('Publish cancelled');
|
|
206
|
+
throw new PublishError();
|
|
207
|
+
}
|
|
208
|
+
// Step 2: Parse and validate frontmatter
|
|
209
|
+
spin.start('Validating SKILL.md…');
|
|
210
|
+
let skill;
|
|
211
|
+
let rawContent;
|
|
212
|
+
let skillDir;
|
|
213
|
+
let frontmatter;
|
|
214
|
+
try {
|
|
215
|
+
rawContent = readFileSync(skillMdPath, 'utf-8');
|
|
216
|
+
const parsed = matter(rawContent);
|
|
217
|
+
frontmatter = parsed.data;
|
|
218
|
+
const parser = getParser('skill.md');
|
|
219
|
+
skill = parser.parse(rawContent, skillMdPath);
|
|
220
|
+
// Also scan directory structure
|
|
221
|
+
skillDir = parser.parseDirectory(skillDirPath);
|
|
222
|
+
// Validate required frontmatter
|
|
223
|
+
if (!frontmatter.description || typeof frontmatter.description !== 'string') {
|
|
224
|
+
throw new Error('"description" field is required in frontmatter');
|
|
225
|
+
}
|
|
226
|
+
if (!Array.isArray(frontmatter.tags)) {
|
|
227
|
+
throw new Error('"tags" field is required and must be an array');
|
|
228
|
+
}
|
|
229
|
+
if (frontmatter.tags.length === 0) {
|
|
230
|
+
throw new Error('"tags" array must have at least one tag');
|
|
231
|
+
}
|
|
232
|
+
spin.stop('Validated ✓');
|
|
233
|
+
console.log(` Name: ${chalk.bold(skill.name)}`);
|
|
234
|
+
console.log(` Description: ${skill.description}`);
|
|
235
|
+
if (frontmatter.version) {
|
|
236
|
+
console.log(` Version: ${frontmatter.version}`);
|
|
237
|
+
}
|
|
238
|
+
console.log(` Tags: ${frontmatter.tags.map((t) => chalk.cyan(t)).join(', ')}`);
|
|
239
|
+
console.log('');
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
spin.stop('Validation failed');
|
|
243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
244
|
+
log.error(`Invalid skill: ${message}`);
|
|
245
|
+
outro('Publish cancelled');
|
|
246
|
+
throw new PublishError();
|
|
247
|
+
}
|
|
248
|
+
// Step 3: Security audit (mandatory)
|
|
249
|
+
spin.start('Running security audit…');
|
|
250
|
+
const engine = new AuditEngine({ lang: 'en' });
|
|
251
|
+
const report = engine.auditSkill(skill, skillMdPath);
|
|
252
|
+
const score = calculateScore(report);
|
|
253
|
+
spin.stop(`Audit complete — score: ${formatScore(score)}`);
|
|
254
|
+
if (report.findings.length > 0) {
|
|
255
|
+
console.log(engine.reportToString(report));
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
console.log(` ${chalk.green('✓ No security issues found')}`);
|
|
259
|
+
}
|
|
260
|
+
console.log('');
|
|
261
|
+
if (score < 90 && !options.force) {
|
|
262
|
+
log.error(`Audit score ${score}/100 is below the minimum of 90.`);
|
|
263
|
+
log.info('Fix the issues or use --force to publish anyway.');
|
|
264
|
+
outro('Publish cancelled');
|
|
265
|
+
throw new PublishError();
|
|
266
|
+
}
|
|
267
|
+
// Dry run — stop here
|
|
268
|
+
if (options.dryRun) {
|
|
269
|
+
outro(chalk.yellow('Dry run — no changes made. Use without --dry-run to publish.'));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Step 4: GitHub API — authenticate and publish
|
|
273
|
+
spin.start('Connecting to GitHub…');
|
|
274
|
+
let token;
|
|
275
|
+
let user;
|
|
276
|
+
let baseBranch;
|
|
277
|
+
let baseSha;
|
|
278
|
+
try {
|
|
279
|
+
token = getToken();
|
|
280
|
+
user = await getGitHubUser(token);
|
|
281
|
+
const base = await getDefaultBranch(token);
|
|
282
|
+
baseBranch = base.name;
|
|
283
|
+
baseSha = base.sha;
|
|
284
|
+
spin.stop(`Authenticated as ${chalk.bold(user)}`);
|
|
285
|
+
console.log(` Registry: ${chalk.cyan(REGISTRY_FULL)}`);
|
|
286
|
+
console.log('');
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
spin.stop('GitHub connection failed');
|
|
290
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
291
|
+
log.error(message);
|
|
292
|
+
outro('Publish cancelled');
|
|
293
|
+
throw new PublishError();
|
|
294
|
+
}
|
|
295
|
+
// Step 5: Fork
|
|
296
|
+
spin.start('Setting up fork…');
|
|
297
|
+
try {
|
|
298
|
+
await ensureFork(token, user);
|
|
299
|
+
spin.stop('Fork ready ✓');
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
spin.stop('Fork failed');
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
log.error(message);
|
|
305
|
+
outro('Publish cancelled');
|
|
306
|
+
throw new PublishError();
|
|
307
|
+
}
|
|
308
|
+
// Step 6: Create branch
|
|
309
|
+
const branchName = `skill/${skill.name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase()}`;
|
|
310
|
+
spin.start(`Creating branch ${chalk.cyan(branchName)}…`);
|
|
311
|
+
try {
|
|
312
|
+
const branchRes = await ghFetch(`${GITHUB_API}/repos/${user}/${REGISTRY_REPO}/git/refs`, token, {
|
|
313
|
+
method: 'POST',
|
|
314
|
+
body: JSON.stringify({
|
|
315
|
+
ref: `refs/heads/${branchName}`,
|
|
316
|
+
sha: baseSha,
|
|
317
|
+
}),
|
|
318
|
+
});
|
|
319
|
+
if (branchRes.status === 422) {
|
|
320
|
+
log.info(`Branch ${branchName} already exists — updating it`);
|
|
321
|
+
}
|
|
322
|
+
else if (!branchRes.ok) {
|
|
323
|
+
throw new Error(`Failed to create branch: ${branchRes.status}`);
|
|
324
|
+
}
|
|
325
|
+
spin.stop('Branch ready ✓');
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
spin.stop('Branch creation failed');
|
|
329
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
330
|
+
log.error(message);
|
|
331
|
+
outro('Publish cancelled');
|
|
332
|
+
throw new PublishError();
|
|
333
|
+
}
|
|
334
|
+
// Step 7: Upload files
|
|
335
|
+
const skillDirName = skill.name.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
|
|
336
|
+
const skillRepoPath = `skills/${skillDirName}/SKILL.md`;
|
|
337
|
+
spin.start('Uploading SKILL.md…');
|
|
338
|
+
try {
|
|
339
|
+
await upsertFile(token, user, branchName, skillRepoPath, rawContent, `Add ${skill.name} v${frontmatter.version || '1.0.0'} skill`);
|
|
340
|
+
spin.stop('SKILL.md uploaded ✓');
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
spin.stop('Upload failed');
|
|
344
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
345
|
+
log.error(message);
|
|
346
|
+
outro('Publish cancelled');
|
|
347
|
+
throw new PublishError();
|
|
348
|
+
}
|
|
349
|
+
// Upload extra files if any (scripts, assets, references, etc.)
|
|
350
|
+
const extraDirNames = ['scripts', 'assets', 'references'];
|
|
351
|
+
for (const dirName of extraDirNames) {
|
|
352
|
+
const dirPath = join(skillDirPath, dirName);
|
|
353
|
+
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
|
|
354
|
+
spin.start(`Uploading ${dirName}/…`);
|
|
355
|
+
try {
|
|
356
|
+
const entries = readdirSync(dirPath);
|
|
357
|
+
for (const entry of entries) {
|
|
358
|
+
const entryPath = join(dirPath, entry);
|
|
359
|
+
if (statSync(entryPath).isFile()) {
|
|
360
|
+
const content = readFileSync(entryPath, 'utf-8');
|
|
361
|
+
const repoEntryPath = `skills/${skillDirName}/${dirName}/${entry}`;
|
|
362
|
+
await upsertFile(token, user, branchName, repoEntryPath, content, `Add ${dirName}/${entry}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
spin.stop(`${dirName}/ uploaded ✓`);
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
spin.stop(`${dirName}/ upload issue`);
|
|
369
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
370
|
+
log.warn(`Failed to upload ${dirName}/: ${message}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Step 8: Update registry.json
|
|
375
|
+
spin.start('Updating registry index…');
|
|
376
|
+
try {
|
|
377
|
+
// Fetch current registry.json from the branch (or upstream)
|
|
378
|
+
const currentRegRes = await ghFetch(`${GITHUB_API}/repos/${user}/${REGISTRY_REPO}/contents/registry.json?ref=${branchName}`, token);
|
|
379
|
+
let registry;
|
|
380
|
+
if (currentRegRes.ok) {
|
|
381
|
+
const currentData = await currentRegRes.json();
|
|
382
|
+
const decoded = Buffer.from(currentData.content, 'base64').toString('utf-8');
|
|
383
|
+
registry = JSON.parse(decoded);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
// Fallback: fetch from upstream
|
|
387
|
+
const upRes = await ghFetch(`https://raw.githubusercontent.com/${REGISTRY_FULL}/main/registry.json`, token, { headers: { 'User-Agent': 'transskill-cli' } });
|
|
388
|
+
if (!upRes.ok)
|
|
389
|
+
throw new Error('Failed to fetch registry.json');
|
|
390
|
+
registry = await upRes.json();
|
|
391
|
+
}
|
|
392
|
+
// Update or add the skill entry
|
|
393
|
+
const existingIdx = registry.skills.findIndex((s) => s.name === skill.name);
|
|
394
|
+
const skillEntry = {
|
|
395
|
+
name: skill.name,
|
|
396
|
+
version: frontmatter.version || '1.0.0',
|
|
397
|
+
description: skill.description,
|
|
398
|
+
tags: frontmatter.tags || [],
|
|
399
|
+
author: user,
|
|
400
|
+
stars: existingIdx >= 0 ? registry.skills[existingIdx].stars : 0,
|
|
401
|
+
auditScore: score,
|
|
402
|
+
created: existingIdx >= 0
|
|
403
|
+
? registry.skills[existingIdx].created
|
|
404
|
+
: new Date().toISOString(),
|
|
405
|
+
};
|
|
406
|
+
if (existingIdx >= 0) {
|
|
407
|
+
registry.skills[existingIdx] = { ...registry.skills[existingIdx], ...skillEntry };
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
registry.skills.push(skillEntry);
|
|
411
|
+
}
|
|
412
|
+
registry.updated = new Date().toISOString();
|
|
413
|
+
const registryJson = JSON.stringify(registry, null, 2);
|
|
414
|
+
await upsertFile(token, user, branchName, 'registry.json', registryJson, `Update registry index: add ${skill.name}`);
|
|
415
|
+
spin.stop('Registry index updated ✓');
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
spin.stop('Registry update failed');
|
|
419
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
420
|
+
log.error(message);
|
|
421
|
+
outro('Publish cancelled');
|
|
422
|
+
throw new PublishError();
|
|
423
|
+
}
|
|
424
|
+
// Step 9: Create PR
|
|
425
|
+
spin.start('Creating pull request…');
|
|
426
|
+
try {
|
|
427
|
+
const prBody = [
|
|
428
|
+
`## 📦 ${skill.name}`,
|
|
429
|
+
'',
|
|
430
|
+
`${skill.description}`,
|
|
431
|
+
'',
|
|
432
|
+
'---',
|
|
433
|
+
'',
|
|
434
|
+
'### Audit Report',
|
|
435
|
+
'',
|
|
436
|
+
`- **Audit Score:** ${score}/100`,
|
|
437
|
+
`- **Author:** ${user}`,
|
|
438
|
+
...(Array.isArray(frontmatter.tags)
|
|
439
|
+
? [`- **Tags:** ${frontmatter.tags.join(', ')}`]
|
|
440
|
+
: []),
|
|
441
|
+
'',
|
|
442
|
+
'---',
|
|
443
|
+
'',
|
|
444
|
+
'> Published by [TransSkill CLI](https://github.com/ljk-777/transskill)',
|
|
445
|
+
].join('\n');
|
|
446
|
+
const prUrl = await createPR(token, user, branchName, `Add skill: ${skill.name} v${frontmatter.version || '1.0.0'}`, prBody);
|
|
447
|
+
spin.stop('Pull request created ✓');
|
|
448
|
+
console.log('');
|
|
449
|
+
console.log(` ${chalk.green('→')} ${prUrl}`);
|
|
450
|
+
console.log('');
|
|
451
|
+
outro(`${chalk.bold(skill.name)} published! 🎉`);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
spin.stop('PR creation failed');
|
|
455
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
456
|
+
log.error(message);
|
|
457
|
+
outro('Publish failed — files were pushed to the branch but PR could not be created.');
|
|
458
|
+
throw new PublishError();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Calculate audit score from report severity counts.
|
|
463
|
+
*/
|
|
464
|
+
function calculateScore(report) {
|
|
465
|
+
const c = report.severityCounts;
|
|
466
|
+
let score = 100;
|
|
467
|
+
score -= c.critical * 30; // -30 each
|
|
468
|
+
score -= c.high * 15; // -15 each
|
|
469
|
+
score -= c.medium * 5; // -5 each
|
|
470
|
+
score -= c.low * 2; // -2 each
|
|
471
|
+
return Math.max(0, score);
|
|
472
|
+
}
|
|
473
|
+
function formatScore(score) {
|
|
474
|
+
if (score >= 90)
|
|
475
|
+
return chalk.green(`${score}/100`);
|
|
476
|
+
if (score >= 70)
|
|
477
|
+
return chalk.yellow(`${score}/100`);
|
|
478
|
+
return chalk.red(`${score}/100`);
|
|
479
|
+
}
|
|
480
|
+
0;
|
|
481
|
+
//# sourceMappingURL=publish.js.map
|