Ctrl K
ComunidadGuías…

scripts/skills-admin.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env npx tsx
/** Local reviewer CLI. Never loads .env.local or accepts a GitHub token. */
import { createClient } from '@supabase/supabase-js'
import { createHash } from 'node:crypto'
import { writeFile, mkdir } from 'node:fs/promises'
import { dirname, resolve, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { execFileSync } from 'node:child_process'
import yaml from 'js-yaml'
import { z } from 'zod'
import { SkillBodySchema } from '../src/lib/agent-skills/validation'

const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const REPO = 'erp-mafia/accounted'
const hash = (body: string) => createHash('sha256').update(body).digest('hex')
const gh = (parameters: string[]) => execFileSync('gh', parameters, { encoding: 'utf8', maxBuffer: 1024 * 1024 })

export async function runSkillsAdmin(args: string[], env: Record<string, string | undefined> = process.env, root = ROOT) {
  function option(name: string): string {
    const index = args.indexOf(`--${name}`)
    const value = index >= 0 ? args[index + 1] : undefined
    if (!value || value.startsWith('--')) throw new Error(`Missing --${name}`)
    return value
  }
  const command = args[0]
  if (!command || command === '--help') {
    console.log('Commands: list; show --id; prepare --id --slug --reviewed-at --review-confirmed; record-pr --id --pr; published --id --slug --pr; disable|enable --slug; reviewed --slug --date --review-confirmed. All commands require --project <Supabase ref>. Mutations against production also require --production-write-approved. Credentials: SKILLS_SUPABASE_URL and SKILLS_SERVICE_ROLE_KEY, explicitly supplied by the reviewer. No .env files are loaded.')
    return
  }
  // No withdraw command: the author's "Dra tillbaka" hides a published text in the database itself (migration 20260926172514).
  if (!['list', 'show', 'prepare', 'record-pr', 'published', 'disable', 'enable', 'reviewed'].includes(command)) throw new Error('Unknown command')
  const url = env.SKILLS_SUPABASE_URL
  const key = env.SKILLS_SERVICE_ROLE_KEY
  const project = option('project')
  if (!url || !key || new URL(url).hostname !== `${project}.supabase.co`) throw new Error('Explicit matching project URL and reviewer credentials required')
  if (!['list', 'show', 'prepare'].includes(command) && project === 'pwxtzglxptnnvjrpixpg' && !args.includes('--production-write-approved')) throw new Error('Specific production write approval required')
  const db = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } })
  if (command === 'list') {
    const { data, error } = await db.from('company_skills').select('id, name, share_status, author_handle, submission_body_hash, published_atom_id, review_url, updated_at').eq('share_status', 'submitted').order('updated_at').limit(100)
    if (error) throw error
    console.log(JSON.stringify(data, null, 2))
    return
  }
  if (['disable', 'enable', 'reviewed'].includes(command)) {
    const slug = z.string().regex(/^(horizontal|vertical|modifier|community)\/[a-z0-9/-]+$/).parse(option('slug'))
    if (command === 'reviewed' && !args.includes('--review-confirmed')) throw new Error('Review confirmation required')
    if (command === 'reviewed' && slug.startsWith('community/')) throw new Error('Community review dates describe publication, not ongoing Accounted review')
    const update = command === 'reviewed'
      ? db.from('agent_atom_registry').update({ reviewed_at: z.iso.date().parse(option('date')) })
      : db.from('agent_atom_registry').update({ is_active: command === 'enable' })
    const { data, error } = await update.eq('id', slug).select('id').single()
    if (error) throw error
    console.log(`${command}: ${data.id}`)
    return
  }
  const id = z.string().uuid().parse(option('id'))
  const { data: row, error } = await db.from('company_skills').select('*').eq('id', id).is('atom_id', null).single()
  if (error) throw error
  if (command === 'show') { console.log(JSON.stringify(row, null, 2)); return }
  if (row.share_status !== 'submitted') throw new Error('Submission is no longer pending review')
  if (command === 'record-pr') {
    const number = z.coerce.number().int().positive().parse(option('pr'))
    gh(['pr', 'view', String(number), '--repo', REPO, '--json', 'number'])
    const { error: updateError } = await db.from('company_skills').update({ review_url: `https://github.com/${REPO}/pull/${number}` })
      .eq('id', id).eq('share_status', row.share_status).eq('submission_body_hash', row.submission_body_hash).select('id').single()
    if (updateError) throw updateError
    return
  }
  const slug = z.string().regex(/^[a-z0-9][a-z0-9-]{0,99}$/).parse(option('slug'))
  const atomId = `community/${slug}`
  const bodyPath = `registry/skills/${slug}/SKILL.md`
  const entryPath = `registry/entries/${slug}.mdx`
  if (row.share_status !== 'submitted' || !row.share_confirmed_at || !row.author_handle || hash(row.body) !== row.submission_body_hash) throw new Error('Consent or frozen submission evidence is missing')
  const body = SkillBodySchema.parse(row.body)
  if (command === 'prepare') {
    if (!args.includes('--review-confirmed')) throw new Error('Human privacy and accounting review confirmation required')
    const date = z.iso.date().parse(option('reviewed-at'))
    const entry = {
      title: row.name, description: row.description, slug, kind: 'skill', author: row.author_handle,
      status: 'live', lang: 'sv', personas: ['finance', 'byra'], publishedAt: date, updatedAt: date,
      reviewedAt: date, mcp_exposed: true, submissionHash: row.submission_body_hash,
      repoUrl: `https://github.com/${REPO}/tree/main/registry/skills/${slug}`,
    }
    // No tenant/user identifiers or private metadata leave the database.
    await mkdir(join(root, dirname(bodyPath)), { recursive: true })
    await writeFile(join(root, bodyPath), body, { flag: 'wx' })
    await writeFile(join(root, entryPath), `---\n${yaml.dump(entry)}---\n\n${row.description}\n`, { flag: 'wx' })
    const authorPath = join(root, 'registry/authors', `${row.author_handle}.mdx`)
    try { await writeFile(authorPath, `---\n${yaml.dump({ handle: row.author_handle, name: row.author_handle, kind: 'community' })}---\n\nCommunity contributor.\n`, { flag: 'wx' }) }
    catch (e) { if ((e as NodeJS.ErrnoException).code !== 'EEXIST') throw e }
    console.log(`Prepared ${entryPath} and ${bodyPath}. Run validate:registry, skills:generate and the compliance review before opening a PR. No PR was opened.`)
    return
  }
  const number = z.coerce.number().int().positive().parse(option('pr'))
  const pr = JSON.parse(gh(['pr', 'view', String(number), '--repo', REPO, '--json', 'state,mergeCommit,files'])) as { state: string; mergeCommit: { oid: string } | null; files: { path: string }[] }
  if (pr.state !== 'MERGED' || !pr.mergeCommit?.oid || !pr.files.some((file) => file.path === bodyPath) || !pr.files.some((file) => file.path === entryPath)) throw new Error('The skill publication PR is not merged')
  const mergedContent = JSON.parse(gh(['api', `repos/${REPO}/contents/${bodyPath}?ref=${pr.mergeCommit.oid}`])) as { content: string }
  if (hash(Buffer.from(mergedContent.content, 'base64').toString()) !== hash(body)) throw new Error('Merged body differs from the frozen submission')
  const mergedEntry = JSON.parse(gh(['api', `repos/${REPO}/contents/${entryPath}?ref=${pr.mergeCommit.oid}`])) as { content: string }
  const entryText = Buffer.from(mergedEntry.content, 'base64').toString()
  const entry = yaml.load(/^---\n([\s\S]*?)\n---/.exec(entryText)?.[1] ?? '') as Record<string, unknown>
  if (entry.author !== row.author_handle || entry.submissionHash !== row.submission_body_hash || entry.title !== row.name || entry.description !== row.description || entry.mcp_exposed !== true) throw new Error('Merged registry attribution differs from the frozen submission')
  const { data: atom, error: atomError } = await db.from('agent_atom_registry').select('body, reviewed_at').eq('id', atomId).eq('is_active', true).eq('mcp_exposed', true).single()
  const reviewedDate = z.iso.date().parse(entry.reviewedAt)
  if (atomError || !atom.reviewed_at || Date.parse(atom.reviewed_at) !== Date.parse(reviewedDate) || hash(atom.body) !== hash(body)) throw new Error('Merged skill is not yet deployed in this registry')
  const { error: updateError } = await db.from('company_skills').update({ share_status: 'published', published_atom_id: atomId, reviewed_at: atom.reviewed_at, review_url: `https://github.com/${REPO}/pull/${number}` })
    .eq('id', id).eq('share_status', row.share_status).eq('submission_body_hash', row.submission_body_hash).select('id').single()
  if (updateError) throw updateError
  console.log('Publication verified and recorded.')
}

if (process.argv[1] === fileURLToPath(import.meta.url)) runSkillsAdmin(process.argv.slice(2)).catch((error) => { console.error(error instanceof Error ? error.message : 'Review failed'); process.exitCode = 1 })