ShiboSoftwareDev/linux-gameboy-advance
A multilayer PCB carrier/router with USB-C and battery power selection, buck regulation, microSD, LCD/backlight, UART/SPI/I²C service headers, controls, volume input, and RGB status LEDs, using vias and reviewed copper routing.
- Version
- 1.0.123
- 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-v4.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",
"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 })