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.
@@ -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":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAa,MAAM,SAAS,CAAC;AACpG,OAAO,EAAE,IAAI,EAAY,MAAM,WAAW,CAAC;AAC3C,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,MAAM,MAAM,aAAa,CAAC;AAEjC,MAAM,cAAc,GAAG,SAAS,CAAC;AACjC,MAAM,aAAa,GAAG,qBAAqB,CAAC;AAS5C;;GAEG;AACH,SAAS,UAAU,CAAC,OAAe;IACjC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,GAAG,CAAC,KAAK,CAAC,wBAAwB,OAAO,EAAE,CAAC,CAAC;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAEhC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QACtC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE;YAAE,SAAS;QAEhD,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC/C,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,SAAS;QAEvC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;YAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,EAAE,GAAG,MAAM,CAAC,IAA+B,CAAC;YAElD,uBAAuB;YACvB,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC;YAE3D,MAAM,CAAC,IAAI,CAAC;gBACV,IAAI;gBACJ,OAAO,EAAE,QAAQ;gBACjB,WAAW;gBACX,WAAW,EAAE,EAAE;aAChB,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,IAAI,CAAC,aAAa,KAAK,yBAAyB,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CAAC,KAAiB,EAAE,aAAqB;IACjE,MAAM,EAAE,GAAG,KAAK,CAAC,WAAW,CAAC;IAC7B,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,IAAI,CAAC,EAAE,CAAC,WAAW,IAAI,OAAO,EAAE,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;QAC1D,GAAG,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,kCAAkC,CAAC,CAAC;QAC5D,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,oCAAoC;QACpC,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1D,EAAE,CAAC,IAAI,GAAG,QAAQ,CAAC;QACnB,QAAQ,GAAG,IAAI,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,MAAM,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAChD,EAAE,CAAC,MAAM,GAAG,aAAa,CAAC;QAC1B,QAAQ,GAAG,IAAI,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,OAAO,IAAI,OAAO,EAAE,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QAClD,EAAE,CAAC,OAAO,GAAG,OAAO,CAAC;QACrB,QAAQ,GAAG,IAAI,CAAC;IAClB,CAAC;IAED,yBAAyB;IACzB,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;QACvE,aAAa,CAAC,KAAK,CAAC,WAAW,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IACxD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAW,EACX,OAA+D;IAE/D,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;IACrE,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,IAAI,WAAW,CAAC;IAEpD,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC,CAAC;IAElD,0BAA0B;IAC1B,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,IAAI,CAAC,KAAK,CAAC,YAAY,OAAO,GAAG,CAAC,CAAC;IACnC,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,MAAM,WAAW,CAAC,CAAC;IAE7C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,GAAG,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;QAChE,KAAK,CAAC,MAAM,CAAC,CAAC;QACd,OAAO;IACT,CAAC;IAED,cAAc;IACd,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QACnF,MAAM,SAAS,GAAG,OAAO,CAAC,CAAC,WAAW,CAAC,MAAM,KAAK,QAAQ,CAAC;QAC3D,MAAM,UAAU,GAAG,OAAO,IAAI,SAAS,CAAC;QACxC,OAAO,CAAC,GAAG,CACT,KAAK,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;YAC9E,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE;YAC5C,GAAG,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,CACjD,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAEhB,UAAU;IACV,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC;YAC5B,OAAO,EAAE,WAAW,MAAM,CAAC,MAAM,gBAAgB,KAAK,CAAC,IAAI,CAAC,GAAG,cAAc,IAAI,aAAa,EAAE,CAAC,GAAG;YACpG,MAAM,EAAE,kBAAkB;YAC1B,QAAQ,EAAE,YAAY;YACtB,YAAY,EAAE,KAAK;SACpB,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,KAAK,CAAC,WAAW,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;IACH,CAAC;IAED,6BAA6B;IAC7B,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,MAAM,GAAG,CAAC,CAAC;IAEf,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;QAE/C,qBAAqB;QACrB,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,aAAa,CAAC,EAAE,CAAC;YAC7C,OAAO,EAAE,CAAC;YACV,SAAS;QACX,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,EAAE,CAAC,CAAC;YAClD,SAAS,EAAE,CAAC;YACZ,SAAS;QACX,CAAC;QAED,wCAAwC;QACxC,IAAI,CAAC;YACH,8CAA8C;YAC9C,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;YACtD,IAAI,CAAC;gBACH,MAAM,YAAY,CAAC,KAAK,CAAC,OAAO,EAAE;oBAChC,KAAK,EAAE,IAAI;oBACX,MAAM,EAAE,KAAK;iBACd,CAAC,CAAC;gBACH,SAAS,EAAE,CAAC;YACd,CAAC;YAAC,OAAO,IAAI,EAAE,CAAC;gBACd,qEAAqE;gBACrE,MAAM,EAAE,CAAC;YACX,CAAC;QACH,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,GAAG,CAAC,KAAK,CAAC,aAAa,OAAO,EAAE,CAAC,CAAC;YAClC,MAAM,EAAE,CAAC;QACX,CAAC;IACH,CAAC;IAED,UAAU;IACV,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxC,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,KAAK,SAAS,YAAY,CAAC,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,MAAM,SAAS,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1K,KAAK,CAAC,wBAAwB,CAAC,CAAC;AAClC,CAAC"}
@@ -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