ShiboSoftwareDev/linux-gameboy-advance

A multilayer PCB carrier board routing the MCU, USB‑C, battery/AA power selector and TPS63020 regulator, LCD/SD interfaces, UART/I²C/SPI service headers, controls, audio/status LEDs, and backlight with reviewed traces and vias.

Version
1.0.125
License
unset
Stars
0

scripts/publish-tsci.mjs

import { spawn } from "node:child_process"
import {
  cpSync,
  existsSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  rmSync,
  writeFileSync,
} from "node:fs"
import { homedir, tmpdir } from "node:os"
import { dirname, extname, join, resolve } from "node:path"
import { fileURLToPath } from "node:url"

const projectDirectory = fileURLToPath(new URL("../", import.meta.url))
const stagingDirectory = mkdtempSync(join(tmpdir(), "linux-gba-tsci-publish-"))

// `tsci push` currently enumerates the whole project and does not apply the
// config's ignoredFiles list. Keep local manufacturing exports, review images,
// and the 40 MiB service assembly out of the registry release by publishing an
// explicit, reproducible source set. The interactive site artifacts remain in
// cloud-artifacts and are verified by scripts/cloud-build.mjs.
const publishEntries = [
  ".npmrc",
  "README.md",
  "POWER-REVISION.md",
  "assembly-parts.json",
  "assets/er-tft035ips-6-oem-installed-v5.glb",
  "assets/sod-123fl.glb",
  "backlight-routes.json",
  "carrier-router.ts",
  "carrier-routes-v2.json",
  "cloud-artifacts",
  "footprints",
  "firmware",
  "handheld-imports",
  "imports",
  "index.circuit.tsx",
  "jlc-availability.json",
  "mechanical",
  "modules/f1c100s/imports",
  "modules/f1c100s/package.json",
  "modules/f1c100s/src",
  "package.json",
  "schematic",
  "scripts",
  "subcircuits",
  "tscircuit.config.json",
  "tsconfig.json",
  "types",
]

for (const entry of publishEntries) {
  const source = resolve(projectDirectory, entry)
  if (!existsSync(source)) {
    throw new Error(`Required publish input is missing: ${entry}`)
  }
  const destination = join(stagingDirectory, entry)
  mkdirSync(dirname(destination), { recursive: true })
  cpSync(source, destination, { recursive: true })
}

// This STEP file is a local mechanical reference; the component imports its
// compact OBJ. The YHNR inductor STEP remains because that component uses it.
rmSync(join(stagingDirectory, "imports/TPS63020DSJR/TPS63020DSJR.step"), {
  force: true,
})

const child = spawn("tsci", ["push"], {
  cwd: stagingDirectory,
  env: process.env,
  stdio: ["inherit", "pipe", "pipe"],
})
let pushOutput = ""
for (const stream of [child.stdout, child.stderr]) {
  stream.on("data", (chunk) => {
    const text = chunk.toString()
    pushOutput += text
    process.stdout.write(text)
  })
}
const pushStatus = await new Promise((resolveStatus, reject) => {
  child.once("error", reject)
  child.once("exit", (code) => resolveStatus(code ?? 1))
})

const stagedPackagePath = join(stagingDirectory, "package.json")
const sourcePackagePath = resolve(projectDirectory, "package.json")
const stagedPackage = JSON.parse(readFileSync(stagedPackagePath, "utf8"))
const sourcePackage = JSON.parse(readFileSync(sourcePackagePath, "utf8"))

// The registry occasionally times out one request after accepting the other
// files. Resume that same release instead of creating a trail of incomplete
// versions. This is only an upload retry; `tsci push` still creates the release
// and determines its version.
if (pushStatus !== 0) {
  const failedFiles = [...pushOutput.matchAll(/^  (.+) - failed$/gm)]
    .map((match) => match[1])
    .filter((path, index, paths) => paths.indexOf(path) === index)

  if (failedFiles.length === 0) {
    console.error(`[publish] staging directory retained for diagnosis: ${stagingDirectory}`)
    process.exit(pushStatus)
  }

  const configCandidates = [
    join(homedir(), "Library/Preferences/tscircuit-nodejs/config.json"),
    join(homedir(), ".config/tscircuit-nodejs/config.json"),
  ]
  const configPath = configCandidates.find(existsSync)
  const cliConfig = configPath
    ? JSON.parse(readFileSync(configPath, "utf8"))
    : {}
  const sessionToken = process.env.TSCIRCUIT_TOKEN ?? cliConfig.sessionToken
  if (!sessionToken) {
    throw new Error("Cannot resume registry upload: no tscircuit session token")
  }

  const packageMatch = stagedPackage.name.match(/^@tsci\/([^.]+)\.(.+)$/)
  if (!packageMatch) throw new Error(`Unexpected package name: ${stagedPackage.name}`)
  const packageNameWithVersion = `${packageMatch[1]}/${packageMatch[2]}@${stagedPackage.version}`
  const registryUrl = cliConfig.registryApiUrl ?? "https://registry-api.tscircuit.com"
  const binaryExtensions = new Set([
    ".glb", ".gltf", ".obj", ".stl", ".step", ".png", ".jpg",
    ".jpeg", ".gif", ".webp", ".zip", ".gz", ".tar",
  ])

  const postJson = async (endpoint, payload) => {
    let lastError
    for (let attempt = 1; attempt <= 5; attempt += 1) {
      try {
        const response = await fetch(`${registryUrl}/${endpoint}`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${sessionToken}`,
            "Content-Type": "application/json",
          },
          body: JSON.stringify(payload),
          signal: AbortSignal.timeout(120_000),
        })
        if (!response.ok) {
          const detail = (await response.text()).slice(0, 500)
          throw new Error(`${response.status} ${response.statusText}: ${detail}`)
        }
        return
      } catch (error) {
        lastError = error
        if (attempt < 5) await new Promise((resolveDelay) => setTimeout(resolveDelay, 1000 * 2 ** (attempt - 1)))
      }
    }
    throw lastError
  }

  for (const relativePath of failedFiles) {
    const fileBytes = readFileSync(join(stagingDirectory, relativePath))
    const isBinary = binaryExtensions.has(extname(relativePath).toLowerCase()) || fileBytes.includes(0)
    await postJson("package_files/create", {
      file_path: relativePath,
      package_name_with_version: packageNameWithVersion,
      ...(isBinary
        ? { content_base64: fileBytes.toString("base64") }
        : { content_text: fileBytes.toString("utf8") }),
    })
    console.log(`[publish] resumed ${relativePath}`)
  }

  await postJson("package_releases/update", {
    package_name_with_version: packageNameWithVersion,
    ready_to_build: true,
    ...(typeof stagedPackage.license === "string" && stagedPackage.license.trim()
      ? { license: stagedPackage.license.trim() }
      : {}),
  })
  console.log(`[publish] ${packageNameWithVersion} marked ready to build`)
}

if (stagedPackage.version !== sourcePackage.version) {
  sourcePackage.version = stagedPackage.version
  writeFileSync(sourcePackagePath, `${JSON.stringify(sourcePackage, null, 2)}\n`)
  console.log(`[publish] synchronized package.json to ${stagedPackage.version}`)
}

rmSync(stagingDirectory, { recursive: true, force: true })