diff --git a/packages/cli/src/commands/skills.test.ts b/packages/cli/src/commands/skills.test.ts index 0f23e4ca..11837d76 100644 --- a/packages/cli/src/commands/skills.test.ts +++ b/packages/cli/src/commands/skills.test.ts @@ -151,3 +151,40 @@ describe('skills marketplaces --json', () => { } }); }); + +describe('skills new command', () => { + let tempDir: string; + + afterEach(() => { + vi.restoreAllMocks(); + if (tempDir) { + rmSync(tempDir, { recursive: true, force: true }); + tempDir = ''; + } + }); + + it('stores a non-negative integer listing price in the manifest and marketplace command', async () => { + tempDir = mkdtempSync(join(tmpdir(), 'sh1pt-skills-new-')); + const out = join(tempDir, 'sh1pt.skill.json'); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await newCmd?.parseAsync(['--out', out, '--name', 'qa-helper', '--price', '25'], { from: 'user' }); + + const manifest = JSON.parse(readFileSync(out, 'utf8')); + expect(manifest.price).toBe(25); + expect(manifest.marketplaces.ugig.command).toContain('--price 25'); + }); + + it.each(['-5', '1.9', 'abc', '9007199254740992'])('rejects invalid listing price %s before writing the manifest', async (price) => { + tempDir = mkdtempSync(join(tmpdir(), 'sh1pt-skills-new-')); + const out = join(tempDir, 'sh1pt.skill.json'); + const newCmd = skillsCmd.commands.find((c) => c.name() === 'new'); + expect(newCmd).toBeDefined(); + + await expect( + newCmd?.parseAsync(['--out', out, '--name', 'qa-helper', '--price', price], { from: 'user' }) + ).rejects.toThrow(/--price must be/); + expect(existsSync(out)).toBe(false); + }); +}); diff --git a/packages/cli/src/commands/skills.ts b/packages/cli/src/commands/skills.ts index d7ecaf82..605eeb6a 100644 --- a/packages/cli/src/commands/skills.ts +++ b/packages/cli/src/commands/skills.ts @@ -78,6 +78,17 @@ function slugify(s: string): string { } function q(s: string): string { return JSON.stringify(s); } async function exists(path: string): Promise { try { await access(path); return true; } catch { return false; } } +function parseListingPrice(value: string): number { + const normalized = value.trim(); + if (!/^\d+$/.test(normalized)) { + throw new Error('--price must be a non-negative integer sat amount'); + } + const price = Number.parseInt(normalized, 10); + if (!Number.isSafeInteger(price)) { + throw new Error('--price must be less than or equal to Number.MAX_SAFE_INTEGER'); + } + return price; +} function frontmatterValue(text: string, key: string): string | undefined { const m = text.match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+)["']?\\s*$`, 'm')); return m?.[1]?.trim(); @@ -349,17 +360,19 @@ skillsCmd const name = slugify(opts.name ?? inferred.name ?? basename(dirname(skillFile))); const title = opts.title ?? inferred.title ?? name; const description = opts.description ?? inferred.description ?? `Agent skill: ${title}`; + const price = parseListingPrice(opts.price); + const tags = opts.tags.split(',').map(t => t.trim()).filter(Boolean).slice(0, 10); const manifest: SkillManifest = { name, title, description, tagline: opts.tagline, category: opts.category, - tags: opts.tags.split(',').map(t => t.trim()).filter(Boolean).slice(0, 10), - price: Number.parseInt(opts.price, 10) || 0, + tags, + price, skillFile, sourceUrl: opts.sourceUrl, - marketplaces: Object.fromEntries(MARKETPLACES.map(mp => [mp.id, { enabled: true, status: 'pending', command: 'command' in mp && mp.command ? mp.command({ name, title, description, tagline: opts.tagline, category: opts.category, tags: opts.tags.split(',').map(t => t.trim()).filter(Boolean).slice(0, 10), price: Number.parseInt(opts.price, 10) || 0, skillFile, sourceUrl: opts.sourceUrl, marketplaces: {} }) : undefined, note: 'note' in mp ? mp.note : undefined }])) as SkillManifest['marketplaces'], + marketplaces: Object.fromEntries(MARKETPLACES.map(mp => [mp.id, { enabled: true, status: 'pending', command: 'command' in mp && mp.command ? mp.command({ name, title, description, tagline: opts.tagline, category: opts.category, tags, price, skillFile, sourceUrl: opts.sourceUrl, marketplaces: {} }) : undefined, note: 'note' in mp ? mp.note : undefined }])) as SkillManifest['marketplaces'], }; await mkdir(dirname(resolve(opts.out)), { recursive: true }); await saveManifest(opts.out, manifest);