Skip to content

DenisGorbachev/oneshot

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

645 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

#!/usr/bin/env -S deno run --node-modules-dir=false --allow-write --allow-read --allow-run=bash,git,cargo --allow-net=docs.rs:443,github.com:443 --allow-env --allow-sys --no-lock

// NOTE: Pin the versions of the packages because the script runs without a lock file
import * as zx from "npm:zx@8.3.2"
import {ProcessPromise, Shell} from "npm:zx@8.3.2"
import {z, ZodSchema, ZodTypeDef} from "https://deno.land/x/zod@v3.23.8/mod.ts"
import {assert, assertEquals} from "jsr:@std/assert@1.0.0"
import {toSnakeCase} from "jsr:@std/text@1.0.10"
import {parseArgs} from "jsr:@std/cli@1.0.13"
import {parse as parseToml} from "jsr:@std/toml@1.0.5"

export const args = parseArgs(Deno.args, {
  string: ["output"],
  alias: {
    output: "o",
  },
})

const CargoTomlSchema = z.object({
  package: z.object({
    name: z.string().min(1),
    description: z.string().min(1),
    repository: z.string().url().min(1),
    license: z.string().optional(),
    metadata: z.object({
      details: z.object({
        title: z.string().min(1).optional(),
        tagline: z.string().optional(),
        summary: z.string().optional(),
        readme: z.object({
          generate: z.boolean().default(true),
        }).default({}),
        peers: z.array(z.string()).default([]).describe("Packages that should be installed alongside this package"),
      }).default({}),
    }).default({}),
  }).optional(),
})

type CargoToml = z.infer<typeof CargoTomlSchema>

const CargoMetadataSchema = z.object({
  packages: z.array(z.object({
    name: z.string(),
    source: z.string().nullable(),
    targets: z.array(z.object({
      name: z.string(),
      kind: z.array(z.string()),
    })),
  })),
})

type CargoMetadata = z.infer<typeof CargoMetadataSchema>

const BadgeSchema = z.object({
  name: z.string().min(1),
  image: z.string().url(),
  url: z.string().url(),
})

type Badge = z.infer<typeof BadgeSchema>

const badge = (name: string, image: string, url: string): Badge => BadgeSchema.parse({name, url, image})

const SectionSchema = z.object({
  title: z.string().min(1),
  body: z.string(),
})

type Section = z.infer<typeof SectionSchema>

const section = (title: string, body: string): Section => SectionSchema.parse({title, body})

const pushSection = (sections: Section[], title: string, body: string) => sections.push(section(title, body))

// Nested sections not supported
const renderSection = ({title, body}: Section) => `## ${title}\n\n${body}`

const renderNonEmptySections = (sections: Section[]) => sections.filter((s) => s.body).map(renderSection).join("\n\n")

const stub = <T>(message = "Implement me"): T => {
  throw new Error(message)
}

/**
 * Examples:
 *
 * `normalizeGitRemoteUrl("git@github.com:DenisGorbachev/rust-private-template.git") == "https://github.com/DenisGorbachev/rust-private-template"`
 * `normalizeGitRemoteUrl("https://github.com/DenisGorbachev/rust-private-template.git") == "https://github.com/DenisGorbachev/rust-private-template"`
 *
 * @param url
 */
const normalizeGitRemoteUrl = (url: string) => {
  // Handle GitHub SSH format: git@github.com:username/repo.git
  const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)\.git$/)
  if (sshMatch) {
    const [, username, repo] = sshMatch
    return `https://github.com/${username}/${repo}`
  }

  // Handle GitHub HTTPS format: https://github.com/username/repo(.git)
  const httpsMatch = url.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/)
  if (httpsMatch) {
    const [, username, repo] = httpsMatch
    return `https://github.com/${username}/${repo}`
  }

  // Return original if not a GitHub URL we recognize
  return url
}

const dirname = import.meta.dirname
if (!dirname) throw new Error("Cannot determine the current script dirname")

const $: Shell<false, ProcessPromise> = zx.$({cwd: dirname})

const parseProcessOutput = (input: zx.ProcessOutput) => JSON.parse(input.stdout)
// deno-lint-ignore no-explicit-any
const parse = <Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, input: zx.ProcessOutput) => schema.parse(parseProcessOutput(input))
const nail = (str: string) => {
  const spacesAtStart = str.match(/^\n(\s+)/)
  if (spacesAtStart?.[1]) {
    return str.replace(new RegExp(`^[^\\S\r\n]{0,${spacesAtStart[1].length}}`, "gm"), "")
  } else {
    return str
  }
}

const theCargoTomlText = await Deno.readTextFile(`${dirname}/Cargo.toml`)
// deno-lint-ignore no-explicit-any
const theCargoTomlRaw = parseToml(theCargoTomlText) as any

// If Cargo.toml is not a package manifest (e.g. a virtual workspace manifest), just exit successfully
if (!theCargoTomlRaw.package) {
  Deno.exit(0)
}

// If README generation is manually disabled in the Cargo.toml, just exit successfully
if (theCargoTomlRaw.package?.metadata?.details?.readme?.generate === false) {
  Deno.exit(0)
}

// launch multiple promises in parallel
const cargoMetadataPromise = $`cargo metadata --format-version 1`
const originUrlPromise = $`git remote get-url origin`

const theCargoMetadataRaw = JSON.parse((await cargoMetadataPromise).stdout)

const theCargoToml = CargoTomlSchema.parse(theCargoTomlRaw)
const theCargoMetadata = CargoMetadataSchema.parse(theCargoMetadataRaw)
const theOriginUrl = normalizeGitRemoteUrl((await originUrlPromise).stdout.trim())

assertEquals(theOriginUrl, theCargoToml.package.repository)

const {package: {name, description, license, metadata: {details: {title: titleExplicit, peers}}}} = theCargoToml
const title = titleExplicit || description
const _libTargetName = toSnakeCase(name)
const thePackageMetadata = theCargoMetadata.packages.find((p) => p.name == name)
assert(thePackageMetadata, "Could not find package metadata")
const primaryTarget = thePackageMetadata.targets[0]
assert(primaryTarget, "Could not find package primary target")
const primaryBinTarget = thePackageMetadata.targets.find((t) => t.kind.includes("bin"))
// NOTE: primaryTarget may be equal to primaryBinTarget
const primaryTargets = [primaryTarget, primaryBinTarget]
const secondaryTargets = thePackageMetadata.targets.filter((t) => !primaryTargets.includes(t))
const secondaryBinTargets = secondaryTargets.filter((t) => t.kind.includes("bin"))
const docsUrl = `https://docs.rs/${name}`
const crateDocsPlaceholder = `
<!-- crate documentation start -->
<!-- crate documentation end -->
`.trim()
const docsUrlPromise = fetch(docsUrl, {method: "HEAD"})
const helpPromise = primaryBinTarget ? $`cargo run --quiet --bin ${primaryBinTarget.name} -- --help` : undefined
const isPublicGitHubRepoPromise = (async () => {
  if (!theOriginUrl.startsWith("https://github.com")) return false
  const response = await fetch(theOriginUrl, {method: "GET"})
  if (response.status === 200) return true
  if (response.status === 404) return false
  throw new Error(`Unexpected response status while checking GitHub repo visibility: ${response.status} ${response.statusText}`)
})()

const docsUrlHead = await docsUrlPromise
const docsUrlIs200 = docsUrlHead.status === 200

// Hack: await the promise instead of calling `then` because `then` has incorrect type in `zx`
const insertCrateDocsIntoReadme = async (readmePath: string) => {
  await $`cargo insert-docs crate-into-readme --allow-dirty --link-to-latest --shrink-headings 0 --readme-path ${readmePath}`
}

const isPublicGitHubRepo = await isPublicGitHubRepoPromise

const badges: Badge[] = []
if (isPublicGitHubRepo) {
  badges.push(badge("Build", `${theCargoToml.package.repository}/actions/workflows/ci.yml/badge.svg`, theCargoToml.package.repository))
}
if (docsUrlIs200) {
  badges.push(badge("Documentation", `https://docs.rs/${name}/badge.svg`, docsUrl))
}
const badgesStr = badges.map(({name, image, url}) => `[![${name}](${image})](${url})`).join("\n")

const licenseNameFileMap: Record<string, string> = {
  "Apache-2.0": "LICENSE-APACHE",
  "MIT": "LICENSE-MIT",
}
const getLicenseFile = (name: string) => {
  const file = licenseNameFileMap[name]
  if (file === undefined) throw new Error(`licenseNameFileMap is missing the following key: \`${name}\``)
  return file
}
const licenseNames = license ? license.split("OR").map((name) => name.trim()) : []

const renderMarkdownList = (items: string[]) => items.map((bin) => `* ${bin}`).join("\n")
const renderShellCode = (code: string) => `\`\`\`shell\n${code}\n\`\`\``

const titleSectionBodyParts = [
  badgesStr,
  crateDocsPlaceholder,
].filter((s) => s.length)
const titleSectionBody = titleSectionBodyParts.join("\n\n")

const sections: Section[] = []
// NOTE: We need to use the package name (not the target name) in cargo commands
const installationSectionBodyParts = []
const installationSectionUseExpandedFormat = primaryBinTarget && primaryTarget !== primaryBinTarget
if (primaryBinTarget) {
  const cmd = renderShellCode(`cargo install --locked ${name}`)
  const text = installationSectionUseExpandedFormat ? `Install as executable:\n\n${cmd}` : cmd
  installationSectionBodyParts.push(text)
}
if (primaryTarget !== primaryBinTarget) {
  const cmd = renderShellCode(`cargo add ${[name, ...peers].join(" ")}`)
  const text = installationSectionUseExpandedFormat ? `Install as library dependency in your package:\n\n${cmd}` : cmd
  installationSectionBodyParts.push(text)
}
pushSection(sections, "Installation", installationSectionBodyParts.join("\n\n"))
if (helpPromise) {
  const help = await helpPromise
  pushSection(sections, "Usage", renderShellCode(help.stdout.trim()))
}
if (secondaryBinTargets.length) {
  const secondaryBinTargetsNames = secondaryBinTargets.map((t) => t.name)
  pushSection(sections, "Additional binaries", renderMarkdownList(secondaryBinTargetsNames.map((bin) => `\`${bin}\``)))
}
if (isPublicGitHubRepo) {
  pushSection(sections, "Gratitude", `Like the project? [⭐ Star this repo](${theCargoToml.package.repository}) on GitHub!`)
}

if (licenseNames.length) {
  const licenseLinks = licenseNames.map((name) => {
    const file = getLicenseFile(name)
    return `[${name}](${file})`
  })
  pushSection(
    sections,
    "License",
    `
${licenseLinks.join(" or ")}.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, shall be licensed as above, without any additional terms or conditions.
`.trim(),
  )
}

const header = `
<!-- DO NOT EDIT -->
<!-- This file is automatically generated by README.ts. -->
<!-- Edit README.ts if you want to make changes. -->
`.trim()
const body = renderNonEmptySections(sections)

const contentArray = [header, `# ${title}`, titleSectionBody, body]
const content = contentArray.filter(s => s.length > 0).join("\n\n");

if (args.output) {
  await Deno.writeTextFile(args.output, content + "\n")
  await insertCrateDocsIntoReadme(args.output)
} else {
  const tempReadmePath = await Deno.makeTempFile({
    prefix: "README",
    suffix: ".md",
  })
  await Deno.writeTextFile(tempReadmePath, content + "\n")
  await insertCrateDocsIntoReadme(tempReadmePath)
  const readme = await Deno.readTextFile(tempReadmePath)
  await Deno.remove(tempReadmePath)
  console.info(readme.trimEnd())
}

About

OneShot is an LLM driver for generating whole apps in one shot.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors