mirror of
https://github.com/harivansh-afk/sandbox-agent.git
synced 2026-04-15 22:03:48 +00:00
Configure lefthook formatter checks (#231)
* Add lefthook formatter checks * Fix SDK mode hydration * Stabilize SDK mode integration test
This commit is contained in:
parent
0471214d65
commit
d2346bafb3
282 changed files with 5840 additions and 8399 deletions
|
|
@ -87,9 +87,7 @@ for (let i = 0; i < args.length; i++) {
|
|||
}
|
||||
|
||||
const RESOURCES_DIR = path.join(__dirname, "resources");
|
||||
const agents = agentFilter.length
|
||||
? agentFilter
|
||||
: ["claude", "codex", "opencode", "cursor"];
|
||||
const agents = agentFilter.length ? agentFilter : ["claude", "codex", "opencode", "cursor"];
|
||||
|
||||
async function main() {
|
||||
fs.mkdirSync(RESOURCES_DIR, { recursive: true });
|
||||
|
|
@ -123,13 +121,8 @@ function writeList(agent: string, list: AgentModelList) {
|
|||
fs.writeFileSync(filePath, JSON.stringify(list, null, 2) + "\n");
|
||||
const modeCount = list.modes?.length ?? 0;
|
||||
const thoughtCount = list.thoughtLevels?.length ?? 0;
|
||||
const extras = [
|
||||
modeCount ? `${modeCount} modes` : null,
|
||||
thoughtCount ? `${thoughtCount} thought levels` : null,
|
||||
].filter(Boolean).join(", ");
|
||||
console.log(
|
||||
` Wrote ${list.models.length} models${extras ? `, ${extras}` : ""} to ${filePath} (default: ${list.defaultModel})`
|
||||
);
|
||||
const extras = [modeCount ? `${modeCount} modes` : null, thoughtCount ? `${thoughtCount} thought levels` : null].filter(Boolean).join(", ");
|
||||
console.log(` Wrote ${list.models.length} models${extras ? `, ${extras}` : ""} to ${filePath} (default: ${list.defaultModel})`);
|
||||
}
|
||||
|
||||
// ─── Claude ───────────────────────────────────────────────────────────────────
|
||||
|
|
@ -177,16 +170,12 @@ async function dumpClaude() {
|
|||
|
||||
const response = await fetch(ANTHROPIC_API_URL, { headers });
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
console.log(
|
||||
` API returned ${response.status}, using fallback aliases`
|
||||
);
|
||||
console.log(` API returned ${response.status}, using fallback aliases`);
|
||||
writeList("claude", CLAUDE_FALLBACK);
|
||||
return;
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Anthropic API returned ${response.status}: ${await response.text()}`
|
||||
);
|
||||
throw new Error(`Anthropic API returned ${response.status}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
const body = (await response.json()) as {
|
||||
|
|
@ -330,9 +319,22 @@ async function dumpCodex() {
|
|||
models,
|
||||
defaultMode: "read-only",
|
||||
modes: [
|
||||
{ id: "read-only", name: "Read Only", description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet." },
|
||||
{ id: "auto", name: "Default", description: "Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files." },
|
||||
{ id: "full-access", name: "Full Access", description: "Codex can edit files outside this workspace and access the internet without asking for approval." },
|
||||
{
|
||||
id: "read-only",
|
||||
name: "Read Only",
|
||||
description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.",
|
||||
},
|
||||
{
|
||||
id: "auto",
|
||||
name: "Default",
|
||||
description:
|
||||
"Codex can read and edit files in the current workspace, and run commands. Approval is required to access the internet or edit other files.",
|
||||
},
|
||||
{
|
||||
id: "full-access",
|
||||
name: "Full Access",
|
||||
description: "Codex can edit files outside this workspace and access the internet without asking for approval.",
|
||||
},
|
||||
],
|
||||
defaultThoughtLevel: "high",
|
||||
thoughtLevels: [
|
||||
|
|
@ -344,10 +346,7 @@ async function dumpCodex() {
|
|||
});
|
||||
}
|
||||
|
||||
function readLineWithTimeout(
|
||||
rl: readline.Interface,
|
||||
timeoutMs: number
|
||||
): Promise<string> {
|
||||
function readLineWithTimeout(rl: readline.Interface, timeoutMs: number): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error("readline timeout"));
|
||||
|
|
@ -368,17 +367,12 @@ function readLineWithTimeout(
|
|||
async function dumpOpencode() {
|
||||
const baseUrl = opencodeUrl ?? process.env.OPENCODE_URL;
|
||||
if (!baseUrl) {
|
||||
console.log(
|
||||
" Skipped: --opencode-url not provided (set OPENCODE_URL or pass --opencode-url)"
|
||||
);
|
||||
console.log(" Skipped: --opencode-url not provided (set OPENCODE_URL or pass --opencode-url)");
|
||||
return;
|
||||
}
|
||||
console.log(`Fetching OpenCode models from ${baseUrl}...`);
|
||||
|
||||
const endpoints = [
|
||||
`${baseUrl}/config/providers`,
|
||||
`${baseUrl}/provider`,
|
||||
];
|
||||
const endpoints = [`${baseUrl}/config/providers`, `${baseUrl}/provider`];
|
||||
|
||||
for (const url of endpoints) {
|
||||
try {
|
||||
|
|
@ -402,16 +396,14 @@ async function dumpOpencode() {
|
|||
throw new Error("OpenCode model endpoints unavailable");
|
||||
}
|
||||
|
||||
function parseOpencodeProviders(
|
||||
value: Record<string, unknown>
|
||||
): AgentModelList | null {
|
||||
const providers = (
|
||||
(value.providers as unknown[]) ?? (value.all as unknown[])
|
||||
) as Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
models?: Record<string, { id?: string; name?: string }>;
|
||||
}> | undefined;
|
||||
function parseOpencodeProviders(value: Record<string, unknown>): AgentModelList | null {
|
||||
const providers = ((value.providers as unknown[]) ?? (value.all as unknown[])) as
|
||||
| Array<{
|
||||
id: string;
|
||||
name?: string;
|
||||
models?: Record<string, { id?: string; name?: string }>;
|
||||
}>
|
||||
| undefined;
|
||||
if (!providers) return null;
|
||||
|
||||
const defaultMap = (value.default as Record<string, string>) ?? {};
|
||||
|
|
@ -455,8 +447,7 @@ function parseOpencodeProviders(
|
|||
{
|
||||
id: "build",
|
||||
name: "Build",
|
||||
description:
|
||||
"The default agent. Executes tools based on configured permissions.",
|
||||
description: "The default agent. Executes tools based on configured permissions.",
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
|
|
@ -473,9 +464,7 @@ async function dumpCursor() {
|
|||
console.log("Fetching Cursor models...");
|
||||
const binary = cursorPath ?? findBinary("cursor-agent");
|
||||
if (!binary) {
|
||||
throw new Error(
|
||||
"cursor-agent binary not found (set --cursor-path or add to PATH)"
|
||||
);
|
||||
throw new Error("cursor-agent binary not found (set --cursor-path or add to PATH)");
|
||||
}
|
||||
console.log(` Using binary: ${binary}`);
|
||||
|
||||
|
|
@ -491,9 +480,7 @@ async function dumpCursor() {
|
|||
for (const rawLine of output.split("\n")) {
|
||||
// Strip ANSI escape codes
|
||||
const line = rawLine.replace(/\x1b\[[0-9;]*[A-Za-z]|\x1b\[?[0-9;]*[A-Za-z]/g, "").trim();
|
||||
const match = line.match(
|
||||
/^(\S+)\s+-\s+(.+?)(?:\s+\((current|default)\))?$/
|
||||
);
|
||||
const match = line.match(/^(\S+)\s+-\s+(.+?)(?:\s+\((current|default)\))?$/);
|
||||
if (!match) continue;
|
||||
const [, id, name, tag] = match;
|
||||
models.push({ id, name: name.trim() });
|
||||
|
|
@ -503,9 +490,7 @@ async function dumpCursor() {
|
|||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
throw new Error(
|
||||
"cursor-agent models returned no parseable models"
|
||||
);
|
||||
throw new Error("cursor-agent models returned no parseable models");
|
||||
}
|
||||
|
||||
writeList("cursor", {
|
||||
|
|
@ -518,13 +503,7 @@ async function dumpCursor() {
|
|||
|
||||
function findBinary(name: string): string | null {
|
||||
// Check sandbox-agent install dir
|
||||
const installDir = path.join(
|
||||
process.env.HOME ?? "",
|
||||
".local",
|
||||
"share",
|
||||
"sandbox-agent",
|
||||
"bin"
|
||||
);
|
||||
const installDir = path.join(process.env.HOME ?? "", ".local", "share", "sandbox-agent", "bin");
|
||||
const installed = path.join(installDir, name);
|
||||
if (fs.existsSync(installed)) return installed;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
{
|
||||
"name": "agent-configs",
|
||||
"version": "1.0.0",
|
||||
"description": "Fetches model/mode lists from agent backends and dumps them to JSON",
|
||||
"scripts": {
|
||||
"dump": "npx tsx dump.ts",
|
||||
"dump:claude": "npx tsx dump.ts --agent claude",
|
||||
"dump:codex": "npx tsx dump.ts --agent codex",
|
||||
"dump:opencode": "npx tsx dump.ts --agent opencode",
|
||||
"dump:cursor": "npx tsx dump.ts --agent cursor",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
"name": "agent-configs",
|
||||
"version": "1.0.0",
|
||||
"description": "Fetches model/mode lists from agent backends and dumps them to JSON",
|
||||
"scripts": {
|
||||
"dump": "npx tsx dump.ts",
|
||||
"dump:claude": "npx tsx dump.ts --agent claude",
|
||||
"dump:codex": "npx tsx dump.ts --agent codex",
|
||||
"dump:opencode": "npx tsx dump.ts --agent opencode",
|
||||
"dump:cursor": "npx tsx dump.ts --agent cursor",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,33 +4,30 @@ import type { ReleaseOpts } from "./main";
|
|||
import { assertDirExists, PREFIX, uploadDirToReleases } from "./utils";
|
||||
|
||||
export async function buildJsArtifacts(opts: ReleaseOpts) {
|
||||
await buildAndUploadTypescriptSdk(opts);
|
||||
await buildAndUploadTypescriptSdk(opts);
|
||||
}
|
||||
|
||||
async function buildAndUploadTypescriptSdk(opts: ReleaseOpts) {
|
||||
console.log(`==> Building TypeScript SDK`);
|
||||
console.log(`==> Building TypeScript SDK`);
|
||||
|
||||
// Build TypeScript SDK
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`pnpm --filter sandbox-agent build`;
|
||||
// Build TypeScript SDK
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`pnpm --filter sandbox-agent build`;
|
||||
|
||||
console.log(`✅ TypeScript SDK built successfully`);
|
||||
console.log(`✅ TypeScript SDK built successfully`);
|
||||
|
||||
// Upload TypeScript SDK to R2
|
||||
console.log(`==> Uploading TypeScript SDK Artifacts`);
|
||||
// Upload TypeScript SDK to R2
|
||||
console.log(`==> Uploading TypeScript SDK Artifacts`);
|
||||
|
||||
const sdkDistPath = path.resolve(
|
||||
opts.root,
|
||||
"sdks/typescript/dist",
|
||||
);
|
||||
const sdkDistPath = path.resolve(opts.root, "sdks/typescript/dist");
|
||||
|
||||
await assertDirExists(sdkDistPath);
|
||||
await assertDirExists(sdkDistPath);
|
||||
|
||||
// Upload to commit directory
|
||||
console.log(`Uploading TypeScript SDK to ${PREFIX}/${opts.commit}/typescript/`);
|
||||
await uploadDirToReleases(sdkDistPath, `${PREFIX}/${opts.commit}/typescript/`);
|
||||
// Upload to commit directory
|
||||
console.log(`Uploading TypeScript SDK to ${PREFIX}/${opts.commit}/typescript/`);
|
||||
await uploadDirToReleases(sdkDistPath, `${PREFIX}/${opts.commit}/typescript/`);
|
||||
|
||||
console.log(`✅ TypeScript SDK artifacts uploaded successfully`);
|
||||
console.log(`✅ TypeScript SDK artifacts uploaded successfully`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,45 +5,45 @@ import { fetchGitRef, versionOrCommitToRef } from "./utils";
|
|||
const IMAGE = "rivetdev/sandbox-agent";
|
||||
|
||||
export async function tagDocker(opts: ReleaseOpts) {
|
||||
// Determine which commit to use for source images
|
||||
let sourceCommit = opts.commit;
|
||||
if (opts.reuseEngineVersion) {
|
||||
console.log(`==> Reusing Docker images from ${opts.reuseEngineVersion}`);
|
||||
const ref = versionOrCommitToRef(opts.reuseEngineVersion);
|
||||
await fetchGitRef(ref);
|
||||
const result = await $`git rev-parse ${ref}`;
|
||||
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||
console.log(`==> Source commit: ${sourceCommit}`);
|
||||
}
|
||||
// Determine which commit to use for source images
|
||||
let sourceCommit = opts.commit;
|
||||
if (opts.reuseEngineVersion) {
|
||||
console.log(`==> Reusing Docker images from ${opts.reuseEngineVersion}`);
|
||||
const ref = versionOrCommitToRef(opts.reuseEngineVersion);
|
||||
await fetchGitRef(ref);
|
||||
const result = await $`git rev-parse ${ref}`;
|
||||
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||
console.log(`==> Source commit: ${sourceCommit}`);
|
||||
}
|
||||
|
||||
// Check both architecture images exist using manifest inspect
|
||||
console.log(`==> Checking images exist: ${IMAGE}:${sourceCommit}-{amd64,arm64}`);
|
||||
try {
|
||||
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-amd64`);
|
||||
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-amd64`;
|
||||
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-arm64`);
|
||||
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-arm64`;
|
||||
console.log(`==> Both images exist`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Docker images ${IMAGE}:${sourceCommit}-{amd64,arm64} not found - skipping Docker tagging`);
|
||||
console.warn(` To enable Docker tagging, build and push images first, then retry the release.`);
|
||||
return;
|
||||
}
|
||||
// Check both architecture images exist using manifest inspect
|
||||
console.log(`==> Checking images exist: ${IMAGE}:${sourceCommit}-{amd64,arm64}`);
|
||||
try {
|
||||
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-amd64`);
|
||||
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-amd64`;
|
||||
console.log(`==> Inspecting ${IMAGE}:${sourceCommit}-arm64`);
|
||||
await $({ stdio: "inherit" })`docker manifest inspect ${IMAGE}:${sourceCommit}-arm64`;
|
||||
console.log(`==> Both images exist`);
|
||||
} catch (error) {
|
||||
console.warn(`⚠️ Docker images ${IMAGE}:${sourceCommit}-{amd64,arm64} not found - skipping Docker tagging`);
|
||||
console.warn(` To enable Docker tagging, build and push images first, then retry the release.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and push manifest with version
|
||||
await createManifest(sourceCommit, opts.version);
|
||||
// Create and push manifest with version
|
||||
await createManifest(sourceCommit, opts.version);
|
||||
|
||||
// Create and push manifest with latest
|
||||
if (opts.latest) {
|
||||
await createManifest(sourceCommit, "latest");
|
||||
await createManifest(sourceCommit, opts.minorVersionChannel);
|
||||
}
|
||||
// Create and push manifest with latest
|
||||
if (opts.latest) {
|
||||
await createManifest(sourceCommit, "latest");
|
||||
await createManifest(sourceCommit, opts.minorVersionChannel);
|
||||
}
|
||||
}
|
||||
|
||||
async function createManifest(from: string, to: string) {
|
||||
console.log(`==> Creating manifest: ${IMAGE}:${to} from ${IMAGE}:${from}-{amd64,arm64}`);
|
||||
console.log(`==> Creating manifest: ${IMAGE}:${to} from ${IMAGE}:${from}-{amd64,arm64}`);
|
||||
|
||||
// Use buildx imagetools to create and push multi-arch manifest
|
||||
// This works with manifest lists as inputs (unlike docker manifest create)
|
||||
await $({ stdio: "inherit" })`docker buildx imagetools create --tag ${IMAGE}:${to} ${IMAGE}:${from}-amd64 ${IMAGE}:${from}-arm64`;
|
||||
// Use buildx imagetools to create and push multi-arch manifest
|
||||
// This works with manifest lists as inputs (unlike docker manifest create)
|
||||
await $({ stdio: "inherit" })`docker buildx imagetools create --tag ${IMAGE}:${to} ${IMAGE}:${from}-amd64 ${IMAGE}:${from}-arm64`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,83 +2,75 @@ import { $ } from "execa";
|
|||
import type { ReleaseOpts } from "./main";
|
||||
|
||||
export async function validateGit(_opts: ReleaseOpts) {
|
||||
// Validate there's no uncommitted changes
|
||||
const result = await $`git status --porcelain`;
|
||||
const status = result.stdout;
|
||||
if (status.trim().length > 0) {
|
||||
throw new Error(
|
||||
"There are uncommitted changes. Please commit or stash them.",
|
||||
);
|
||||
}
|
||||
// Validate there's no uncommitted changes
|
||||
const result = await $`git status --porcelain`;
|
||||
const status = result.stdout;
|
||||
if (status.trim().length > 0) {
|
||||
throw new Error("There are uncommitted changes. Please commit or stash them.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAndPushTag(opts: ReleaseOpts) {
|
||||
console.log(`Creating tag v${opts.version}...`);
|
||||
try {
|
||||
// Create tag and force update if it exists
|
||||
await $({ stdio: "inherit", cwd: opts.root })`git tag -f v${opts.version}`;
|
||||
console.log(`Creating tag v${opts.version}...`);
|
||||
try {
|
||||
// Create tag and force update if it exists
|
||||
await $({ stdio: "inherit", cwd: opts.root })`git tag -f v${opts.version}`;
|
||||
|
||||
// Push tag with force to ensure it's updated
|
||||
await $({ stdio: "inherit", cwd: opts.root })`git push origin v${opts.version} -f`;
|
||||
// Push tag with force to ensure it's updated
|
||||
await $({ stdio: "inherit", cwd: opts.root })`git push origin v${opts.version} -f`;
|
||||
|
||||
console.log(`✅ Tag v${opts.version} created and pushed`);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to create or push tag");
|
||||
throw err;
|
||||
}
|
||||
console.log(`✅ Tag v${opts.version} created and pushed`);
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to create or push tag");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGitHubRelease(opts: ReleaseOpts) {
|
||||
console.log("Creating GitHub release...");
|
||||
console.log("Creating GitHub release...");
|
||||
|
||||
try {
|
||||
// Get the current tag name (should be the tag created during the release process)
|
||||
const { stdout: currentTag } = await $({
|
||||
cwd: opts.root,
|
||||
})`git describe --tags --exact-match`;
|
||||
const tagName = currentTag.trim();
|
||||
try {
|
||||
// Get the current tag name (should be the tag created during the release process)
|
||||
const { stdout: currentTag } = await $({
|
||||
cwd: opts.root,
|
||||
})`git describe --tags --exact-match`;
|
||||
const tagName = currentTag.trim();
|
||||
|
||||
console.log(`Looking for existing release for ${opts.version}`);
|
||||
console.log(`Looking for existing release for ${opts.version}`);
|
||||
|
||||
// Check if a release with this version name already exists
|
||||
const { stdout: releaseJson } = await $({
|
||||
cwd: opts.root,
|
||||
})`gh release list --json name,tagName`;
|
||||
const releases = JSON.parse(releaseJson);
|
||||
const existingRelease = releases.find(
|
||||
(r: any) => r.name === opts.version,
|
||||
);
|
||||
// Check if a release with this version name already exists
|
||||
const { stdout: releaseJson } = await $({
|
||||
cwd: opts.root,
|
||||
})`gh release list --json name,tagName`;
|
||||
const releases = JSON.parse(releaseJson);
|
||||
const existingRelease = releases.find((r: any) => r.name === opts.version);
|
||||
|
||||
if (existingRelease) {
|
||||
console.log(
|
||||
`Updating release ${opts.version} to point to new tag ${tagName}`,
|
||||
);
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`gh release edit ${existingRelease.tagName} --tag ${tagName}`;
|
||||
} else {
|
||||
console.log(
|
||||
`Creating new release ${opts.version} pointing to tag ${tagName}`,
|
||||
);
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`gh release create ${tagName} --title ${opts.version} --generate-notes`;
|
||||
if (existingRelease) {
|
||||
console.log(`Updating release ${opts.version} to point to new tag ${tagName}`);
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`gh release edit ${existingRelease.tagName} --tag ${tagName}`;
|
||||
} else {
|
||||
console.log(`Creating new release ${opts.version} pointing to tag ${tagName}`);
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`gh release create ${tagName} --title ${opts.version} --generate-notes`;
|
||||
|
||||
// Check if this is a pre-release (contains -rc. or similar)
|
||||
if (opts.version.includes("-")) {
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`gh release edit ${tagName} --prerelease`;
|
||||
}
|
||||
}
|
||||
// Check if this is a pre-release (contains -rc. or similar)
|
||||
if (opts.version.includes("-")) {
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: opts.root,
|
||||
})`gh release edit ${tagName} --prerelease`;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ GitHub release created/updated");
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to create GitHub release");
|
||||
console.warn("! You may need to create the release manually");
|
||||
throw err;
|
||||
}
|
||||
console.log("✅ GitHub release created/updated");
|
||||
} catch (err) {
|
||||
console.error("❌ Failed to create GitHub release");
|
||||
console.warn("! You may need to create the release manually");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,25 +1,25 @@
|
|||
{
|
||||
"name": "release",
|
||||
"version": "2.0.21",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"glob": "^10.3.10",
|
||||
"semver": "^7.6.0"
|
||||
}
|
||||
"name": "release",
|
||||
"version": "2.0.21",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
"typescript": "^5.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": "^12.1.0",
|
||||
"execa": "^8.0.1",
|
||||
"glob": "^10.3.10",
|
||||
"semver": "^7.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,119 +2,101 @@ import * as fs from "node:fs/promises";
|
|||
import * as path from "node:path";
|
||||
import { $ } from "execa";
|
||||
import type { ReleaseOpts } from "./main";
|
||||
import {
|
||||
copyReleasesPath,
|
||||
deleteReleasesPath,
|
||||
fetchGitRef,
|
||||
listReleasesObjects,
|
||||
PREFIX,
|
||||
uploadContentToReleases,
|
||||
versionOrCommitToRef,
|
||||
} from "./utils";
|
||||
import { copyReleasesPath, deleteReleasesPath, fetchGitRef, listReleasesObjects, PREFIX, uploadContentToReleases, versionOrCommitToRef } from "./utils";
|
||||
|
||||
export async function promoteArtifacts(opts: ReleaseOpts) {
|
||||
// Determine which commit to use for source artifacts
|
||||
let sourceCommit = opts.commit;
|
||||
if (opts.reuseEngineVersion) {
|
||||
console.log(`==> Reusing artifacts from ${opts.reuseEngineVersion}`);
|
||||
const ref = versionOrCommitToRef(opts.reuseEngineVersion);
|
||||
await fetchGitRef(ref);
|
||||
const result = await $`git rev-parse ${ref}`;
|
||||
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||
console.log(`==> Source commit: ${sourceCommit}`);
|
||||
}
|
||||
// Determine which commit to use for source artifacts
|
||||
let sourceCommit = opts.commit;
|
||||
if (opts.reuseEngineVersion) {
|
||||
console.log(`==> Reusing artifacts from ${opts.reuseEngineVersion}`);
|
||||
const ref = versionOrCommitToRef(opts.reuseEngineVersion);
|
||||
await fetchGitRef(ref);
|
||||
const result = await $`git rev-parse ${ref}`;
|
||||
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||
console.log(`==> Source commit: ${sourceCommit}`);
|
||||
}
|
||||
|
||||
// Promote TypeScript SDK artifacts (uploaded by build-artifacts.ts to sandbox-agent/{commit}/typescript/)
|
||||
await promotePath(opts, sourceCommit, "typescript");
|
||||
// Promote TypeScript SDK artifacts (uploaded by build-artifacts.ts to sandbox-agent/{commit}/typescript/)
|
||||
await promotePath(opts, sourceCommit, "typescript");
|
||||
|
||||
// Promote binary artifacts (uploaded by CI in release.yaml to sandbox-agent/{commit}/binaries/)
|
||||
await promotePath(opts, sourceCommit, "binaries");
|
||||
// Promote binary artifacts (uploaded by CI in release.yaml to sandbox-agent/{commit}/binaries/)
|
||||
await promotePath(opts, sourceCommit, "binaries");
|
||||
|
||||
// Upload install scripts
|
||||
await uploadInstallScripts(opts, opts.version);
|
||||
if (opts.latest) {
|
||||
await uploadInstallScripts(opts, "latest");
|
||||
await uploadInstallScripts(opts, opts.minorVersionChannel);
|
||||
}
|
||||
// Upload install scripts
|
||||
await uploadInstallScripts(opts, opts.version);
|
||||
if (opts.latest) {
|
||||
await uploadInstallScripts(opts, "latest");
|
||||
await uploadInstallScripts(opts, opts.minorVersionChannel);
|
||||
}
|
||||
|
||||
// Upload gigacode install scripts
|
||||
await uploadGigacodeInstallScripts(opts, opts.version);
|
||||
if (opts.latest) {
|
||||
await uploadGigacodeInstallScripts(opts, "latest");
|
||||
}
|
||||
// Upload gigacode install scripts
|
||||
await uploadGigacodeInstallScripts(opts, opts.version);
|
||||
if (opts.latest) {
|
||||
await uploadGigacodeInstallScripts(opts, "latest");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function uploadInstallScripts(opts: ReleaseOpts, version: string) {
|
||||
const installScriptPaths = [
|
||||
path.resolve(opts.root, "scripts/release/static/install.sh"),
|
||||
path.resolve(opts.root, "scripts/release/static/install.ps1"),
|
||||
];
|
||||
const installScriptPaths = [path.resolve(opts.root, "scripts/release/static/install.sh"), path.resolve(opts.root, "scripts/release/static/install.ps1")];
|
||||
|
||||
for (const scriptPath of installScriptPaths) {
|
||||
let scriptContent = await fs.readFile(scriptPath, "utf-8");
|
||||
scriptContent = scriptContent.replace(/__VERSION__/g, version);
|
||||
for (const scriptPath of installScriptPaths) {
|
||||
let scriptContent = await fs.readFile(scriptPath, "utf-8");
|
||||
scriptContent = scriptContent.replace(/__VERSION__/g, version);
|
||||
|
||||
const uploadKey = `${PREFIX}/${version}/${scriptPath.split("/").pop() ?? ""}`;
|
||||
const uploadKey = `${PREFIX}/${version}/${scriptPath.split("/").pop() ?? ""}`;
|
||||
|
||||
console.log(`Uploading install script: ${uploadKey}`);
|
||||
await uploadContentToReleases(scriptContent, uploadKey);
|
||||
}
|
||||
console.log(`Uploading install script: ${uploadKey}`);
|
||||
await uploadContentToReleases(scriptContent, uploadKey);
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadGigacodeInstallScripts(opts: ReleaseOpts, version: string) {
|
||||
const installScriptPaths = [
|
||||
path.resolve(opts.root, "scripts/release/static/gigacode-install.sh"),
|
||||
path.resolve(opts.root, "scripts/release/static/gigacode-install.ps1"),
|
||||
];
|
||||
const installScriptPaths = [
|
||||
path.resolve(opts.root, "scripts/release/static/gigacode-install.sh"),
|
||||
path.resolve(opts.root, "scripts/release/static/gigacode-install.ps1"),
|
||||
];
|
||||
|
||||
for (const scriptPath of installScriptPaths) {
|
||||
let scriptContent = await fs.readFile(scriptPath, "utf-8");
|
||||
scriptContent = scriptContent.replace(/__VERSION__/g, version);
|
||||
for (const scriptPath of installScriptPaths) {
|
||||
let scriptContent = await fs.readFile(scriptPath, "utf-8");
|
||||
scriptContent = scriptContent.replace(/__VERSION__/g, version);
|
||||
|
||||
const uploadKey = `${PREFIX}/${version}/${scriptPath.split("/").pop() ?? ""}`;
|
||||
const uploadKey = `${PREFIX}/${version}/${scriptPath.split("/").pop() ?? ""}`;
|
||||
|
||||
console.log(`Uploading gigacode install script: ${uploadKey}`);
|
||||
await uploadContentToReleases(scriptContent, uploadKey);
|
||||
}
|
||||
console.log(`Uploading gigacode install script: ${uploadKey}`);
|
||||
await uploadContentToReleases(scriptContent, uploadKey);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPath(sourcePrefix: string, targetPrefix: string) {
|
||||
console.log(`Copying ${sourcePrefix} -> ${targetPrefix}`);
|
||||
await deleteReleasesPath(targetPrefix);
|
||||
await copyReleasesPath(sourcePrefix, targetPrefix);
|
||||
console.log(`Copying ${sourcePrefix} -> ${targetPrefix}`);
|
||||
await deleteReleasesPath(targetPrefix);
|
||||
await copyReleasesPath(sourcePrefix, targetPrefix);
|
||||
}
|
||||
|
||||
/** S3-to-S3 copy from sandbox-agent/{commit}/{name}/ to sandbox-agent/{version}/{name}/ */
|
||||
async function promotePath(opts: ReleaseOpts, sourceCommit: string, name: string) {
|
||||
console.log(`==> Promoting ${name} artifacts`);
|
||||
console.log(`==> Promoting ${name} artifacts`);
|
||||
|
||||
const sourcePrefix = `${PREFIX}/${sourceCommit}/${name}/`;
|
||||
const commitFiles = await listReleasesObjects(sourcePrefix);
|
||||
if (!Array.isArray(commitFiles?.Contents) || commitFiles.Contents.length === 0) {
|
||||
throw new Error(`No files found under ${sourcePrefix}`);
|
||||
}
|
||||
const sourcePrefix = `${PREFIX}/${sourceCommit}/${name}/`;
|
||||
const commitFiles = await listReleasesObjects(sourcePrefix);
|
||||
if (!Array.isArray(commitFiles?.Contents) || commitFiles.Contents.length === 0) {
|
||||
throw new Error(`No files found under ${sourcePrefix}`);
|
||||
}
|
||||
|
||||
await copyPath(sourcePrefix, `${PREFIX}/${opts.version}/${name}/`);
|
||||
if (opts.latest) {
|
||||
await copyPath(sourcePrefix, `${PREFIX}/latest/${name}/`);
|
||||
if (name === "binaries") {
|
||||
const binariesSourcePrefix = `${PREFIX}/${sourceCommit}/binaries/sandbox-agent-`;
|
||||
const sandboxAgentBinaries = await listReleasesObjects(binariesSourcePrefix);
|
||||
if (
|
||||
!Array.isArray(sandboxAgentBinaries?.Contents) ||
|
||||
sandboxAgentBinaries.Contents.length === 0
|
||||
) {
|
||||
throw new Error(`No sandbox-agent binaries found under ${binariesSourcePrefix}`);
|
||||
}
|
||||
await copyPath(sourcePrefix, `${PREFIX}/${opts.version}/${name}/`);
|
||||
if (opts.latest) {
|
||||
await copyPath(sourcePrefix, `${PREFIX}/latest/${name}/`);
|
||||
if (name === "binaries") {
|
||||
const binariesSourcePrefix = `${PREFIX}/${sourceCommit}/binaries/sandbox-agent-`;
|
||||
const sandboxAgentBinaries = await listReleasesObjects(binariesSourcePrefix);
|
||||
if (!Array.isArray(sandboxAgentBinaries?.Contents) || sandboxAgentBinaries.Contents.length === 0) {
|
||||
throw new Error(`No sandbox-agent binaries found under ${binariesSourcePrefix}`);
|
||||
}
|
||||
|
||||
await copyPath(
|
||||
binariesSourcePrefix,
|
||||
`${PREFIX}/${opts.minorVersionChannel}/binaries/sandbox-agent-`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await copyPath(binariesSourcePrefix, `${PREFIX}/${opts.minorVersionChannel}/binaries/sandbox-agent-`);
|
||||
return;
|
||||
}
|
||||
|
||||
await copyPath(sourcePrefix, `${PREFIX}/${opts.minorVersionChannel}/${name}/`);
|
||||
}
|
||||
await copyPath(sourcePrefix, `${PREFIX}/${opts.minorVersionChannel}/${name}/`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,260 +8,243 @@ import { downloadFromReleases, PREFIX } from "./utils";
|
|||
// ─── Platform binary mapping (npm package → Rust target) ────────────────────
|
||||
// Maps CLI platform packages to their Rust build targets.
|
||||
// Keep in sync when adding new target platforms.
|
||||
const CLI_PLATFORM_MAP: Record<
|
||||
string,
|
||||
{ target: string; binaryExt: string; binaryName: string }
|
||||
> = {
|
||||
"@sandbox-agent/cli-linux-x64": {
|
||||
target: "x86_64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-linux-arm64": {
|
||||
target: "aarch64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-win32-x64": {
|
||||
target: "x86_64-pc-windows-gnu",
|
||||
binaryExt: ".exe",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-darwin-x64": {
|
||||
target: "x86_64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-darwin-arm64": {
|
||||
target: "aarch64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/gigacode-linux-x64": {
|
||||
target: "x86_64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-linux-arm64": {
|
||||
target: "aarch64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-win32-x64": {
|
||||
target: "x86_64-pc-windows-gnu",
|
||||
binaryExt: ".exe",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-darwin-x64": {
|
||||
target: "x86_64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-darwin-arm64": {
|
||||
target: "aarch64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
const CLI_PLATFORM_MAP: Record<string, { target: string; binaryExt: string; binaryName: string }> = {
|
||||
"@sandbox-agent/cli-linux-x64": {
|
||||
target: "x86_64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-linux-arm64": {
|
||||
target: "aarch64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-win32-x64": {
|
||||
target: "x86_64-pc-windows-gnu",
|
||||
binaryExt: ".exe",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-darwin-x64": {
|
||||
target: "x86_64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/cli-darwin-arm64": {
|
||||
target: "aarch64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "sandbox-agent",
|
||||
},
|
||||
"@sandbox-agent/gigacode-linux-x64": {
|
||||
target: "x86_64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-linux-arm64": {
|
||||
target: "aarch64-unknown-linux-musl",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-win32-x64": {
|
||||
target: "x86_64-pc-windows-gnu",
|
||||
binaryExt: ".exe",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-darwin-x64": {
|
||||
target: "x86_64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
"@sandbox-agent/gigacode-darwin-arm64": {
|
||||
target: "aarch64-apple-darwin",
|
||||
binaryExt: "",
|
||||
binaryName: "gigacode",
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Shared helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
async function npmVersionExists(
|
||||
packageName: string,
|
||||
version: string,
|
||||
): Promise<boolean> {
|
||||
console.log(
|
||||
`==> Checking if NPM version exists: ${packageName}@${version}`,
|
||||
);
|
||||
try {
|
||||
await $({
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
})`npm view ${packageName}@${version} version`;
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error.stderr) {
|
||||
const stderr = error.stderr;
|
||||
// Expected errors when version or package doesn't exist
|
||||
const expected =
|
||||
stderr.includes(`No match found for version ${version}`) ||
|
||||
stderr.includes(`'${packageName}@${version}' is not in this registry.`) ||
|
||||
stderr.includes("404 Not Found") ||
|
||||
stderr.includes("is not in the npm registry");
|
||||
if (!expected) {
|
||||
throw new Error(
|
||||
`unexpected npm view version output: ${stderr}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async function npmVersionExists(packageName: string, version: string): Promise<boolean> {
|
||||
console.log(`==> Checking if NPM version exists: ${packageName}@${version}`);
|
||||
try {
|
||||
await $({
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
})`npm view ${packageName}@${version} version`;
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error.stderr) {
|
||||
const stderr = error.stderr;
|
||||
// Expected errors when version or package doesn't exist
|
||||
const expected =
|
||||
stderr.includes(`No match found for version ${version}`) ||
|
||||
stderr.includes(`'${packageName}@${version}' is not in this registry.`) ||
|
||||
stderr.includes("404 Not Found") ||
|
||||
stderr.includes("is not in the npm registry");
|
||||
if (!expected) {
|
||||
throw new Error(`unexpected npm view version output: ${stderr}`);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function crateVersionExists(
|
||||
crateName: string,
|
||||
version: string,
|
||||
): Promise<boolean> {
|
||||
console.log(`==> Checking if crate version exists: ${crateName}@${version}`);
|
||||
try {
|
||||
const result = await $({
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})`cargo search ${crateName} --limit 1`;
|
||||
const output = result.stdout;
|
||||
const match = output.match(new RegExp(`^${crateName}\\s*=\\s*"([^"]+)"`));
|
||||
if (match && match[1] === version) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
async function crateVersionExists(crateName: string, version: string): Promise<boolean> {
|
||||
console.log(`==> Checking if crate version exists: ${crateName}@${version}`);
|
||||
try {
|
||||
const result = await $({
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})`cargo search ${crateName} --limit 1`;
|
||||
const output = result.stdout;
|
||||
const match = output.match(new RegExp(`^${crateName}\\s*=\\s*"([^"]+)"`));
|
||||
if (match && match[1] === version) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Package discovery ──────────────────────────────────────────────────────
|
||||
|
||||
interface NpmPackageInfo {
|
||||
name: string;
|
||||
dir: string;
|
||||
hasBuildScript: boolean;
|
||||
localDeps: string[];
|
||||
name: string;
|
||||
dir: string;
|
||||
hasBuildScript: boolean;
|
||||
localDeps: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover non-private npm packages matching the given glob patterns.
|
||||
*/
|
||||
async function discoverNpmPackages(root: string, patterns: string[]): Promise<NpmPackageInfo[]> {
|
||||
const packages: NpmPackageInfo[] = [];
|
||||
const packages: NpmPackageInfo[] = [];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const matches = await glob(pattern, { cwd: root });
|
||||
for (const match of matches) {
|
||||
const fullPath = join(root, match);
|
||||
const pkg = JSON.parse(await fs.readFile(fullPath, "utf-8"));
|
||||
if (pkg.private) continue;
|
||||
for (const pattern of patterns) {
|
||||
const matches = await glob(pattern, { cwd: root });
|
||||
for (const match of matches) {
|
||||
const fullPath = join(root, match);
|
||||
const pkg = JSON.parse(await fs.readFile(fullPath, "utf-8"));
|
||||
if (pkg.private) continue;
|
||||
|
||||
const allDeps = { ...pkg.dependencies, ...pkg.peerDependencies };
|
||||
const localDeps = Object.entries(allDeps || {})
|
||||
.filter(([_, v]) => String(v).startsWith("workspace:"))
|
||||
.map(([k]) => k);
|
||||
const allDeps = { ...pkg.dependencies, ...pkg.peerDependencies };
|
||||
const localDeps = Object.entries(allDeps || {})
|
||||
.filter(([_, v]) => String(v).startsWith("workspace:"))
|
||||
.map(([k]) => k);
|
||||
|
||||
packages.push({
|
||||
name: pkg.name,
|
||||
dir: dirname(fullPath),
|
||||
hasBuildScript: !!pkg.scripts?.build,
|
||||
localDeps,
|
||||
});
|
||||
}
|
||||
}
|
||||
packages.push({
|
||||
name: pkg.name,
|
||||
dir: dirname(fullPath),
|
||||
hasBuildScript: !!pkg.scripts?.build,
|
||||
localDeps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return packages;
|
||||
return packages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Topologically sort packages so dependencies are published before dependents.
|
||||
*/
|
||||
function topoSort(packages: NpmPackageInfo[]): NpmPackageInfo[] {
|
||||
const byName = new Map(packages.map(p => [p.name, p]));
|
||||
const visited = new Set<string>();
|
||||
const result: NpmPackageInfo[] = [];
|
||||
const byName = new Map(packages.map((p) => [p.name, p]));
|
||||
const visited = new Set<string>();
|
||||
const result: NpmPackageInfo[] = [];
|
||||
|
||||
function visit(pkg: NpmPackageInfo) {
|
||||
if (visited.has(pkg.name)) return;
|
||||
visited.add(pkg.name);
|
||||
for (const dep of pkg.localDeps) {
|
||||
const d = byName.get(dep);
|
||||
if (d) visit(d);
|
||||
}
|
||||
result.push(pkg);
|
||||
}
|
||||
function visit(pkg: NpmPackageInfo) {
|
||||
if (visited.has(pkg.name)) return;
|
||||
visited.add(pkg.name);
|
||||
for (const dep of pkg.localDeps) {
|
||||
const d = byName.get(dep);
|
||||
if (d) visit(d);
|
||||
}
|
||||
result.push(pkg);
|
||||
}
|
||||
|
||||
for (const pkg of packages) visit(pkg);
|
||||
return result;
|
||||
for (const pkg of packages) visit(pkg);
|
||||
return result;
|
||||
}
|
||||
|
||||
interface CrateInfo {
|
||||
name: string;
|
||||
dir: string;
|
||||
name: string;
|
||||
dir: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover workspace crates via `cargo metadata` and return them in dependency order.
|
||||
*/
|
||||
async function discoverCrates(root: string): Promise<CrateInfo[]> {
|
||||
const result = await $({ cwd: root, stdout: "pipe" })`cargo metadata --no-deps --format-version 1`;
|
||||
const metadata = JSON.parse(result.stdout);
|
||||
const result = await $({ cwd: root, stdout: "pipe" })`cargo metadata --no-deps --format-version 1`;
|
||||
const metadata = JSON.parse(result.stdout);
|
||||
|
||||
const memberIds = new Set<string>(metadata.workspace_members);
|
||||
const workspacePackages = metadata.packages.filter((p: any) => memberIds.has(p.id));
|
||||
const memberIds = new Set<string>(metadata.workspace_members);
|
||||
const workspacePackages = metadata.packages.filter((p: any) => memberIds.has(p.id));
|
||||
|
||||
// Build name→package map for topo sort
|
||||
const byName = new Map<string, any>(workspacePackages.map((p: any) => [p.name, p]));
|
||||
// Build name→package map for topo sort
|
||||
const byName = new Map<string, any>(workspacePackages.map((p: any) => [p.name, p]));
|
||||
|
||||
const visited = new Set<string>();
|
||||
const sorted: CrateInfo[] = [];
|
||||
const visited = new Set<string>();
|
||||
const sorted: CrateInfo[] = [];
|
||||
|
||||
function visit(pkg: any) {
|
||||
if (visited.has(pkg.name)) return;
|
||||
visited.add(pkg.name);
|
||||
for (const dep of pkg.dependencies) {
|
||||
const internal = byName.get(dep.name);
|
||||
if (internal) visit(internal);
|
||||
}
|
||||
sorted.push({
|
||||
name: pkg.name,
|
||||
dir: dirname(pkg.manifest_path),
|
||||
});
|
||||
}
|
||||
function visit(pkg: any) {
|
||||
if (visited.has(pkg.name)) return;
|
||||
visited.add(pkg.name);
|
||||
for (const dep of pkg.dependencies) {
|
||||
const internal = byName.get(dep.name);
|
||||
if (internal) visit(internal);
|
||||
}
|
||||
sorted.push({
|
||||
name: pkg.name,
|
||||
dir: dirname(pkg.manifest_path),
|
||||
});
|
||||
}
|
||||
|
||||
for (const pkg of workspacePackages) visit(pkg);
|
||||
return sorted;
|
||||
for (const pkg of workspacePackages) visit(pkg);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
// ─── Crate publishing ───────────────────────────────────────────────────────
|
||||
|
||||
export async function publishCrates(opts: ReleaseOpts) {
|
||||
console.log("==> Discovering workspace crates");
|
||||
const crates = await discoverCrates(opts.root);
|
||||
console.log("==> Discovering workspace crates");
|
||||
const crates = await discoverCrates(opts.root);
|
||||
|
||||
console.log(`Found ${crates.length} crates to publish:`);
|
||||
for (const c of crates) console.log(` - ${c.name}`);
|
||||
console.log(`Found ${crates.length} crates to publish:`);
|
||||
for (const c of crates) console.log(` - ${c.name}`);
|
||||
|
||||
for (const crate of crates) {
|
||||
const versionExists = await crateVersionExists(crate.name, opts.version);
|
||||
if (versionExists) {
|
||||
console.log(
|
||||
`Version ${opts.version} of ${crate.name} already exists on crates.io. Skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const crate of crates) {
|
||||
const versionExists = await crateVersionExists(crate.name, opts.version);
|
||||
if (versionExists) {
|
||||
console.log(`Version ${opts.version} of ${crate.name} already exists on crates.io. Skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`==> Publishing to crates.io: ${crate.name}@${opts.version}`);
|
||||
console.log(`==> Publishing to crates.io: ${crate.name}@${opts.version}`);
|
||||
|
||||
try {
|
||||
await $({
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: crate.dir,
|
||||
})`cargo publish --allow-dirty --no-verify`;
|
||||
console.log(`✅ Published ${crate.name}@${opts.version}`);
|
||||
} catch (err: any) {
|
||||
if (err.stderr?.includes("already exists")) {
|
||||
console.log(
|
||||
`Version ${opts.version} of ${crate.name} already exists on crates.io. Skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
console.error(`❌ Failed to publish ${crate.name}`);
|
||||
console.error(err.stderr || err.message);
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
await $({
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
cwd: crate.dir,
|
||||
})`cargo publish --allow-dirty --no-verify`;
|
||||
console.log(`✅ Published ${crate.name}@${opts.version}`);
|
||||
} catch (err: any) {
|
||||
if (err.stderr?.includes("already exists")) {
|
||||
console.log(`Version ${opts.version} of ${crate.name} already exists on crates.io. Skipping...`);
|
||||
continue;
|
||||
}
|
||||
console.error(`❌ Failed to publish ${crate.name}`);
|
||||
console.error(err.stderr || err.message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
console.log("Waiting for crates.io to index...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 30000));
|
||||
}
|
||||
console.log("Waiting for crates.io to index...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 30000));
|
||||
}
|
||||
|
||||
console.log("✅ All crates published");
|
||||
console.log("✅ All crates published");
|
||||
}
|
||||
|
||||
// ─── NPM library publishing ────────────────────────────────────────────────
|
||||
|
|
@ -272,50 +255,49 @@ export async function publishCrates(opts: ReleaseOpts) {
|
|||
* Publishes in dependency order via topological sort.
|
||||
*/
|
||||
export async function publishNpmLibraries(opts: ReleaseOpts) {
|
||||
console.log("==> Discovering library packages");
|
||||
const all = await discoverNpmPackages(opts.root, ["sdks/*/package.json"]);
|
||||
console.log("==> Discovering library packages");
|
||||
const all = await discoverNpmPackages(opts.root, ["sdks/*/package.json"]);
|
||||
|
||||
// Exclude CLI and gigacode directories (handled by publishNpmCli)
|
||||
const libraries = all.filter(p => {
|
||||
const rel = relative(opts.root, p.dir);
|
||||
return !(rel === "sdks/cli" || rel.startsWith("sdks/cli/")) &&
|
||||
!(rel === "sdks/gigacode" || rel.startsWith("sdks/gigacode/"));
|
||||
});
|
||||
// Exclude CLI and gigacode directories (handled by publishNpmCli)
|
||||
const libraries = all.filter((p) => {
|
||||
const rel = relative(opts.root, p.dir);
|
||||
return !(rel === "sdks/cli" || rel.startsWith("sdks/cli/")) && !(rel === "sdks/gigacode" || rel.startsWith("sdks/gigacode/"));
|
||||
});
|
||||
|
||||
const sorted = topoSort(libraries);
|
||||
const sorted = topoSort(libraries);
|
||||
|
||||
console.log(`Found ${sorted.length} library packages to publish:`);
|
||||
for (const pkg of sorted) console.log(` - ${pkg.name}`);
|
||||
console.log(`Found ${sorted.length} library packages to publish:`);
|
||||
for (const pkg of sorted) console.log(` - ${pkg.name}`);
|
||||
|
||||
const isReleaseCandidate = opts.version.includes("-rc.");
|
||||
const tag = isReleaseCandidate ? "rc" : (opts.latest ? "latest" : opts.minorVersionChannel);
|
||||
const isReleaseCandidate = opts.version.includes("-rc.");
|
||||
const tag = isReleaseCandidate ? "rc" : opts.latest ? "latest" : opts.minorVersionChannel;
|
||||
|
||||
for (const pkg of sorted) {
|
||||
const versionExists = await npmVersionExists(pkg.name, opts.version);
|
||||
if (versionExists) {
|
||||
console.log(`Version ${opts.version} of ${pkg.name} already exists. Skipping...`);
|
||||
continue;
|
||||
}
|
||||
for (const pkg of sorted) {
|
||||
const versionExists = await npmVersionExists(pkg.name, opts.version);
|
||||
if (versionExists) {
|
||||
console.log(`Version ${opts.version} of ${pkg.name} already exists. Skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (pkg.hasBuildScript) {
|
||||
console.log(`==> Building ${pkg.name}`);
|
||||
await $({ stdio: "inherit", cwd: opts.root })`pnpm --filter ${pkg.name} build`;
|
||||
}
|
||||
if (pkg.hasBuildScript) {
|
||||
console.log(`==> Building ${pkg.name}`);
|
||||
await $({ stdio: "inherit", cwd: opts.root })`pnpm --filter ${pkg.name} build`;
|
||||
}
|
||||
|
||||
console.log(`==> Publishing to NPM: ${pkg.name}@${opts.version}`);
|
||||
try {
|
||||
await $({ stdio: "inherit", cwd: pkg.dir })`pnpm publish --access public --tag ${tag} --no-git-checks`;
|
||||
console.log(`✅ Published ${pkg.name}@${opts.version}`);
|
||||
} catch (err: any) {
|
||||
if (err.stderr?.includes("You cannot publish over the previously published versions") || err.stderr?.includes("403")) {
|
||||
console.log(`⚠️ ${pkg.name}@${opts.version} already published (npm registry). Skipping.`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`==> Publishing to NPM: ${pkg.name}@${opts.version}`);
|
||||
try {
|
||||
await $({ stdio: "inherit", cwd: pkg.dir })`pnpm publish --access public --tag ${tag} --no-git-checks`;
|
||||
console.log(`✅ Published ${pkg.name}@${opts.version}`);
|
||||
} catch (err: any) {
|
||||
if (err.stderr?.includes("You cannot publish over the previously published versions") || err.stderr?.includes("403")) {
|
||||
console.log(`⚠️ ${pkg.name}@${opts.version} already published (npm registry). Skipping.`);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ All library packages published");
|
||||
console.log("✅ All library packages published");
|
||||
}
|
||||
|
||||
// ─── NPM CLI publishing ────────────────────────────────────────────────────
|
||||
|
|
@ -325,100 +307,91 @@ export async function publishNpmLibraries(opts: ReleaseOpts) {
|
|||
* Platform packages get their binaries downloaded from R2 before publishing.
|
||||
*/
|
||||
export async function publishNpmCli(opts: ReleaseOpts) {
|
||||
console.log("==> Discovering CLI packages");
|
||||
const packages = await discoverNpmPackages(opts.root, [
|
||||
"sdks/cli/package.json",
|
||||
"sdks/cli/platforms/*/package.json",
|
||||
"sdks/gigacode/package.json",
|
||||
"sdks/gigacode/platforms/*/package.json",
|
||||
]);
|
||||
console.log("==> Discovering CLI packages");
|
||||
const packages = await discoverNpmPackages(opts.root, [
|
||||
"sdks/cli/package.json",
|
||||
"sdks/cli/platforms/*/package.json",
|
||||
"sdks/gigacode/package.json",
|
||||
"sdks/gigacode/platforms/*/package.json",
|
||||
]);
|
||||
|
||||
console.log(`Found ${packages.length} CLI packages to publish:`);
|
||||
for (const pkg of packages) console.log(` - ${pkg.name}`);
|
||||
console.log(`Found ${packages.length} CLI packages to publish:`);
|
||||
for (const pkg of packages) console.log(` - ${pkg.name}`);
|
||||
|
||||
// Determine which commit to use for downloading binaries
|
||||
let sourceCommit = opts.commit;
|
||||
if (opts.reuseEngineVersion) {
|
||||
const ref = opts.reuseEngineVersion.includes(".")
|
||||
? `v${opts.reuseEngineVersion}`
|
||||
: opts.reuseEngineVersion;
|
||||
const result = await $`git rev-parse ${ref}`;
|
||||
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||
console.log(`Using binaries from commit: ${sourceCommit}`);
|
||||
}
|
||||
// Determine which commit to use for downloading binaries
|
||||
let sourceCommit = opts.commit;
|
||||
if (opts.reuseEngineVersion) {
|
||||
const ref = opts.reuseEngineVersion.includes(".") ? `v${opts.reuseEngineVersion}` : opts.reuseEngineVersion;
|
||||
const result = await $`git rev-parse ${ref}`;
|
||||
sourceCommit = result.stdout.trim().slice(0, 7);
|
||||
console.log(`Using binaries from commit: ${sourceCommit}`);
|
||||
}
|
||||
|
||||
for (const pkg of packages) {
|
||||
const versionExists = await npmVersionExists(pkg.name, opts.version);
|
||||
if (versionExists) {
|
||||
console.log(
|
||||
`Version ${opts.version} of ${pkg.name} already exists. Skipping...`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const pkg of packages) {
|
||||
const versionExists = await npmVersionExists(pkg.name, opts.version);
|
||||
if (versionExists) {
|
||||
console.log(`Version ${opts.version} of ${pkg.name} already exists. Skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Download binary for platform-specific packages
|
||||
const platformInfo = CLI_PLATFORM_MAP[pkg.name];
|
||||
if (platformInfo) {
|
||||
const binDir = join(pkg.dir, "bin");
|
||||
const binaryName = `${platformInfo.binaryName}${platformInfo.binaryExt}`;
|
||||
const localBinaryPath = join(binDir, binaryName);
|
||||
const remoteBinaryPath = `${PREFIX}/${sourceCommit}/binaries/${platformInfo.binaryName}-${platformInfo.target}${platformInfo.binaryExt}`;
|
||||
// Download binary for platform-specific packages
|
||||
const platformInfo = CLI_PLATFORM_MAP[pkg.name];
|
||||
if (platformInfo) {
|
||||
const binDir = join(pkg.dir, "bin");
|
||||
const binaryName = `${platformInfo.binaryName}${platformInfo.binaryExt}`;
|
||||
const localBinaryPath = join(binDir, binaryName);
|
||||
const remoteBinaryPath = `${PREFIX}/${sourceCommit}/binaries/${platformInfo.binaryName}-${platformInfo.target}${platformInfo.binaryExt}`;
|
||||
|
||||
console.log(`==> Downloading binary for ${pkg.name}`);
|
||||
console.log(` From: ${remoteBinaryPath}`);
|
||||
console.log(` To: ${localBinaryPath}`);
|
||||
console.log(`==> Downloading binary for ${pkg.name}`);
|
||||
console.log(` From: ${remoteBinaryPath}`);
|
||||
console.log(` To: ${localBinaryPath}`);
|
||||
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await downloadFromReleases(remoteBinaryPath, localBinaryPath);
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
await downloadFromReleases(remoteBinaryPath, localBinaryPath);
|
||||
|
||||
if (!platformInfo.binaryExt) {
|
||||
await fs.chmod(localBinaryPath, 0o755);
|
||||
}
|
||||
}
|
||||
if (!platformInfo.binaryExt) {
|
||||
await fs.chmod(localBinaryPath, 0o755);
|
||||
}
|
||||
}
|
||||
|
||||
// Publish
|
||||
console.log(`==> Publishing to NPM: ${pkg.name}@${opts.version}`);
|
||||
// Publish
|
||||
console.log(`==> Publishing to NPM: ${pkg.name}@${opts.version}`);
|
||||
|
||||
const isReleaseCandidate = opts.version.includes("-rc.");
|
||||
const tag = getCliPackageNpmTag({
|
||||
packageName: pkg.name,
|
||||
isReleaseCandidate,
|
||||
latest: opts.latest,
|
||||
minorVersionChannel: opts.minorVersionChannel,
|
||||
});
|
||||
const isReleaseCandidate = opts.version.includes("-rc.");
|
||||
const tag = getCliPackageNpmTag({
|
||||
packageName: pkg.name,
|
||||
isReleaseCandidate,
|
||||
latest: opts.latest,
|
||||
minorVersionChannel: opts.minorVersionChannel,
|
||||
});
|
||||
|
||||
try {
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: pkg.dir,
|
||||
})`pnpm publish --access public --tag ${tag} --no-git-checks`;
|
||||
console.log(`✅ Published ${pkg.name}@${opts.version}`);
|
||||
} catch (err: any) {
|
||||
if (err.stderr?.includes("You cannot publish over the previously published versions") || err.stderr?.includes("403")) {
|
||||
console.log(`⚠️ ${pkg.name}@${opts.version} already published (npm registry). Skipping.`);
|
||||
} else {
|
||||
console.error(`❌ Failed to publish ${pkg.name}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await $({
|
||||
stdio: "inherit",
|
||||
cwd: pkg.dir,
|
||||
})`pnpm publish --access public --tag ${tag} --no-git-checks`;
|
||||
console.log(`✅ Published ${pkg.name}@${opts.version}`);
|
||||
} catch (err: any) {
|
||||
if (err.stderr?.includes("You cannot publish over the previously published versions") || err.stderr?.includes("403")) {
|
||||
console.log(`⚠️ ${pkg.name}@${opts.version} already published (npm registry). Skipping.`);
|
||||
} else {
|
||||
console.error(`❌ Failed to publish ${pkg.name}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("✅ All CLI packages published");
|
||||
console.log("✅ All CLI packages published");
|
||||
}
|
||||
|
||||
function getCliPackageNpmTag(opts: {
|
||||
packageName: string;
|
||||
isReleaseCandidate: boolean;
|
||||
latest: boolean;
|
||||
minorVersionChannel: string;
|
||||
}): string {
|
||||
if (opts.isReleaseCandidate) {
|
||||
return "rc";
|
||||
}
|
||||
function getCliPackageNpmTag(opts: { packageName: string; isReleaseCandidate: boolean; latest: boolean; minorVersionChannel: string }): string {
|
||||
if (opts.isReleaseCandidate) {
|
||||
return "rc";
|
||||
}
|
||||
|
||||
if (opts.latest) {
|
||||
return "latest";
|
||||
}
|
||||
if (opts.latest) {
|
||||
return "latest";
|
||||
}
|
||||
|
||||
return opts.minorVersionChannel;
|
||||
return opts.minorVersionChannel;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,77 +5,68 @@ import { glob } from "glob";
|
|||
import type { ReleaseOpts } from "./main";
|
||||
|
||||
function assert(condition: any, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message || "Assertion failed");
|
||||
}
|
||||
if (!condition) {
|
||||
throw new Error(message || "Assertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateVersion(opts: ReleaseOpts) {
|
||||
// 1. Update workspace version and internal crate versions in root Cargo.toml
|
||||
const cargoTomlPath = join(opts.root, "Cargo.toml");
|
||||
let cargoContent = await fs.readFile(cargoTomlPath, "utf-8");
|
||||
// 1. Update workspace version and internal crate versions in root Cargo.toml
|
||||
const cargoTomlPath = join(opts.root, "Cargo.toml");
|
||||
let cargoContent = await fs.readFile(cargoTomlPath, "utf-8");
|
||||
|
||||
// Update [workspace.package] version
|
||||
assert(
|
||||
/\[workspace\.package\]\nversion = ".*"/.test(cargoContent),
|
||||
"Could not find workspace.package version in Cargo.toml",
|
||||
);
|
||||
cargoContent = cargoContent.replace(
|
||||
/\[workspace\.package\]\nversion = ".*"/,
|
||||
`[workspace.package]\nversion = "${opts.version}"`,
|
||||
);
|
||||
// Update [workspace.package] version
|
||||
assert(/\[workspace\.package\]\nversion = ".*"/.test(cargoContent), "Could not find workspace.package version in Cargo.toml");
|
||||
cargoContent = cargoContent.replace(/\[workspace\.package\]\nversion = ".*"/, `[workspace.package]\nversion = "${opts.version}"`);
|
||||
|
||||
// Discover internal crates from [workspace.dependencies] by matching
|
||||
// lines with both `version = "..."` and `path = "..."` (internal path deps)
|
||||
const internalCratePattern = /^(\S+)\s*=\s*\{[^}]*version\s*=\s*"[^"]+"\s*,[^}]*path\s*=/gm;
|
||||
let match;
|
||||
const internalCrates: string[] = [];
|
||||
while ((match = internalCratePattern.exec(cargoContent)) !== null) {
|
||||
internalCrates.push(match[1]);
|
||||
}
|
||||
// Discover internal crates from [workspace.dependencies] by matching
|
||||
// lines with both `version = "..."` and `path = "..."` (internal path deps)
|
||||
const internalCratePattern = /^(\S+)\s*=\s*\{[^}]*version\s*=\s*"[^"]+"\s*,[^}]*path\s*=/gm;
|
||||
let match;
|
||||
const internalCrates: string[] = [];
|
||||
while ((match = internalCratePattern.exec(cargoContent)) !== null) {
|
||||
internalCrates.push(match[1]);
|
||||
}
|
||||
|
||||
console.log(`Discovered ${internalCrates.length} internal crates to version-bump:`);
|
||||
for (const crate of internalCrates) console.log(` - ${crate}`);
|
||||
console.log(`Discovered ${internalCrates.length} internal crates to version-bump:`);
|
||||
for (const crate of internalCrates) console.log(` - ${crate}`);
|
||||
|
||||
for (const crate of internalCrates) {
|
||||
const pattern = new RegExp(
|
||||
`(${crate.replace(/-/g, "-")} = \\{ version = ")[^"]+(",)`,
|
||||
"g",
|
||||
);
|
||||
cargoContent = cargoContent.replace(pattern, `$1${opts.version}$2`);
|
||||
}
|
||||
for (const crate of internalCrates) {
|
||||
const pattern = new RegExp(`(${crate.replace(/-/g, "-")} = \\{ version = ")[^"]+(",)`, "g");
|
||||
cargoContent = cargoContent.replace(pattern, `$1${opts.version}$2`);
|
||||
}
|
||||
|
||||
await fs.writeFile(cargoTomlPath, cargoContent);
|
||||
await $({ cwd: opts.root })`git add Cargo.toml`;
|
||||
await fs.writeFile(cargoTomlPath, cargoContent);
|
||||
await $({ cwd: opts.root })`git add Cargo.toml`;
|
||||
|
||||
// 2. Discover and update all non-private SDK package.json versions
|
||||
const packageJsonPaths = await glob("sdks/**/package.json", {
|
||||
cwd: opts.root,
|
||||
ignore: ["**/node_modules/**"],
|
||||
});
|
||||
// 2. Discover and update all non-private SDK package.json versions
|
||||
const packageJsonPaths = await glob("sdks/**/package.json", {
|
||||
cwd: opts.root,
|
||||
ignore: ["**/node_modules/**"],
|
||||
});
|
||||
|
||||
// Filter to non-private packages only
|
||||
const toUpdate: string[] = [];
|
||||
for (const relPath of packageJsonPaths) {
|
||||
const fullPath = join(opts.root, relPath);
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
const pkg = JSON.parse(content);
|
||||
if (pkg.private) continue;
|
||||
toUpdate.push(relPath);
|
||||
}
|
||||
// Filter to non-private packages only
|
||||
const toUpdate: string[] = [];
|
||||
for (const relPath of packageJsonPaths) {
|
||||
const fullPath = join(opts.root, relPath);
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
const pkg = JSON.parse(content);
|
||||
if (pkg.private) continue;
|
||||
toUpdate.push(relPath);
|
||||
}
|
||||
|
||||
console.log(`Discovered ${toUpdate.length} SDK package.json files to version-bump:`);
|
||||
for (const relPath of toUpdate) console.log(` - ${relPath}`);
|
||||
console.log(`Discovered ${toUpdate.length} SDK package.json files to version-bump:`);
|
||||
for (const relPath of toUpdate) console.log(` - ${relPath}`);
|
||||
|
||||
for (const relPath of toUpdate) {
|
||||
const fullPath = join(opts.root, relPath);
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
for (const relPath of toUpdate) {
|
||||
const fullPath = join(opts.root, relPath);
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
|
||||
const versionPattern = /"version": ".*"/;
|
||||
assert(versionPattern.test(content), `No version field in ${relPath}`);
|
||||
const versionPattern = /"version": ".*"/;
|
||||
assert(versionPattern.test(content), `No version field in ${relPath}`);
|
||||
|
||||
const updated = content.replace(versionPattern, `"version": "${opts.version}"`);
|
||||
await fs.writeFile(fullPath, updated);
|
||||
await $({ cwd: opts.root })`git add ${relPath}`;
|
||||
}
|
||||
const updated = content.replace(versionPattern, `"version": "${opts.version}"`);
|
||||
await fs.writeFile(fullPath, updated);
|
||||
await $({ cwd: opts.root })`git add ${relPath}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { $ } from "execa";
|
|||
export const PREFIX = "sandbox-agent";
|
||||
|
||||
export function assert(condition: any, message?: string): asserts condition {
|
||||
if (!condition) {
|
||||
throw new Error(message || "Assertion failed");
|
||||
}
|
||||
if (!condition) {
|
||||
throw new Error(message || "Assertion failed");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -15,126 +15,112 @@ export function assert(condition: any, message?: string): asserts condition {
|
|||
* Otherwise, it's treated as a git revision and returned as-is (e.g., "bb7f292").
|
||||
*/
|
||||
export function versionOrCommitToRef(versionOrCommit: string): string {
|
||||
if (versionOrCommit.includes(".")) {
|
||||
assert(
|
||||
!versionOrCommit.startsWith("v"),
|
||||
`Version should not start with "v" (got "${versionOrCommit}", use "${versionOrCommit.slice(1)}" instead)`,
|
||||
);
|
||||
return `v${versionOrCommit}`;
|
||||
}
|
||||
return versionOrCommit;
|
||||
if (versionOrCommit.includes(".")) {
|
||||
assert(!versionOrCommit.startsWith("v"), `Version should not start with "v" (got "${versionOrCommit}", use "${versionOrCommit.slice(1)}" instead)`);
|
||||
return `v${versionOrCommit}`;
|
||||
}
|
||||
return versionOrCommit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a git ref from the remote. For tags, fetches all tags. For commits, unshallows the repo.
|
||||
*/
|
||||
export async function fetchGitRef(ref: string): Promise<void> {
|
||||
if (ref.startsWith("v")) {
|
||||
console.log(`Fetching tags...`);
|
||||
await $({ stdio: "inherit" })`git fetch --tags --force`;
|
||||
} else {
|
||||
// Git doesn't allow fetching commits directly by SHA, and CI often uses
|
||||
// shallow clones. Unshallow the repo to ensure the commit is available.
|
||||
console.log(`Unshallowing repo to find commit ${ref}...`);
|
||||
try {
|
||||
await $({ stdio: "inherit" })`git fetch --unshallow origin`;
|
||||
} catch {
|
||||
// Already unshallowed, just fetch
|
||||
await $({ stdio: "inherit" })`git fetch origin`;
|
||||
}
|
||||
}
|
||||
if (ref.startsWith("v")) {
|
||||
console.log(`Fetching tags...`);
|
||||
await $({ stdio: "inherit" })`git fetch --tags --force`;
|
||||
} else {
|
||||
// Git doesn't allow fetching commits directly by SHA, and CI often uses
|
||||
// shallow clones. Unshallow the repo to ensure the commit is available.
|
||||
console.log(`Unshallowing repo to find commit ${ref}...`);
|
||||
try {
|
||||
await $({ stdio: "inherit" })`git fetch --unshallow origin`;
|
||||
} catch {
|
||||
// Already unshallowed, just fetch
|
||||
await $({ stdio: "inherit" })`git fetch origin`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ReleasesS3Config {
|
||||
awsEnv: Record<string, string>;
|
||||
endpointUrl: string;
|
||||
awsEnv: Record<string, string>;
|
||||
endpointUrl: string;
|
||||
}
|
||||
|
||||
let cachedConfig: ReleasesS3Config | null = null;
|
||||
|
||||
async function getReleasesS3Config(): Promise<ReleasesS3Config> {
|
||||
if (cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
if (cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
let awsAccessKeyId = process.env.R2_RELEASES_ACCESS_KEY_ID;
|
||||
if (!awsAccessKeyId) {
|
||||
const result =
|
||||
await $`op read ${"op://Engineering/rivet-releases R2 Upload/username"}`;
|
||||
awsAccessKeyId = result.stdout.trim();
|
||||
}
|
||||
let awsSecretAccessKey = process.env.R2_RELEASES_SECRET_ACCESS_KEY;
|
||||
if (!awsSecretAccessKey) {
|
||||
const result =
|
||||
await $`op read ${"op://Engineering/rivet-releases R2 Upload/password"}`;
|
||||
awsSecretAccessKey = result.stdout.trim();
|
||||
}
|
||||
let awsAccessKeyId = process.env.R2_RELEASES_ACCESS_KEY_ID;
|
||||
if (!awsAccessKeyId) {
|
||||
const result = await $`op read ${"op://Engineering/rivet-releases R2 Upload/username"}`;
|
||||
awsAccessKeyId = result.stdout.trim();
|
||||
}
|
||||
let awsSecretAccessKey = process.env.R2_RELEASES_SECRET_ACCESS_KEY;
|
||||
if (!awsSecretAccessKey) {
|
||||
const result = await $`op read ${"op://Engineering/rivet-releases R2 Upload/password"}`;
|
||||
awsSecretAccessKey = result.stdout.trim();
|
||||
}
|
||||
|
||||
assert(awsAccessKeyId, "AWS_ACCESS_KEY_ID is required");
|
||||
assert(awsSecretAccessKey, "AWS_SECRET_ACCESS_KEY is required");
|
||||
assert(awsAccessKeyId, "AWS_ACCESS_KEY_ID is required");
|
||||
assert(awsSecretAccessKey, "AWS_SECRET_ACCESS_KEY is required");
|
||||
|
||||
cachedConfig = {
|
||||
awsEnv: {
|
||||
AWS_ACCESS_KEY_ID: awsAccessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey,
|
||||
AWS_DEFAULT_REGION: "auto",
|
||||
},
|
||||
endpointUrl:
|
||||
"https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com",
|
||||
};
|
||||
cachedConfig = {
|
||||
awsEnv: {
|
||||
AWS_ACCESS_KEY_ID: awsAccessKeyId,
|
||||
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey,
|
||||
AWS_DEFAULT_REGION: "auto",
|
||||
},
|
||||
endpointUrl: "https://2a94c6a0ced8d35ea63cddc86c2681e7.r2.cloudflarestorage.com",
|
||||
};
|
||||
|
||||
return cachedConfig;
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
export async function uploadDirToReleases(
|
||||
localPath: string,
|
||||
remotePath: string,
|
||||
): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
// Use --checksum-algorithm CRC32 for R2 compatibility (matches CI upload in release.yaml)
|
||||
await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
})`aws s3 cp ${localPath} s3://rivet-releases/${remotePath} --recursive --checksum-algorithm CRC32 --endpoint-url ${endpointUrl}`;
|
||||
export async function uploadDirToReleases(localPath: string, remotePath: string): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
// Use --checksum-algorithm CRC32 for R2 compatibility (matches CI upload in release.yaml)
|
||||
await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
})`aws s3 cp ${localPath} s3://rivet-releases/${remotePath} --recursive --checksum-algorithm CRC32 --endpoint-url ${endpointUrl}`;
|
||||
}
|
||||
|
||||
export async function uploadContentToReleases(
|
||||
content: string,
|
||||
remotePath: string,
|
||||
): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
await $({
|
||||
env: awsEnv,
|
||||
input: content,
|
||||
shell: true,
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
})`aws s3 cp - s3://rivet-releases/${remotePath} --endpoint-url ${endpointUrl}`;
|
||||
export async function uploadContentToReleases(content: string, remotePath: string): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
await $({
|
||||
env: awsEnv,
|
||||
input: content,
|
||||
shell: true,
|
||||
stdio: ["pipe", "inherit", "inherit"],
|
||||
})`aws s3 cp - s3://rivet-releases/${remotePath} --endpoint-url ${endpointUrl}`;
|
||||
}
|
||||
|
||||
export interface ListReleasesResult {
|
||||
Contents?: { Key: string; Size: number }[];
|
||||
Contents?: { Key: string; Size: number }[];
|
||||
}
|
||||
|
||||
export async function listReleasesObjects(
|
||||
prefix: string,
|
||||
): Promise<ListReleasesResult> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
const result = await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
})`aws s3api list-objects --bucket rivet-releases --prefix ${prefix} --endpoint-url ${endpointUrl}`;
|
||||
return JSON.parse(result.stdout);
|
||||
export async function listReleasesObjects(prefix: string): Promise<ListReleasesResult> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
const result = await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
})`aws s3api list-objects --bucket rivet-releases --prefix ${prefix} --endpoint-url ${endpointUrl}`;
|
||||
return JSON.parse(result.stdout);
|
||||
}
|
||||
|
||||
export async function deleteReleasesPath(remotePath: string): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
})`aws s3 rm s3://rivet-releases/${remotePath} --recursive --endpoint-url ${endpointUrl}`;
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
})`aws s3 rm s3://rivet-releases/${remotePath} --recursive --endpoint-url ${endpointUrl}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -147,68 +133,59 @@ export async function deleteReleasesPath(remotePath: string): Promise<void> {
|
|||
*
|
||||
* See: https://community.cloudflare.com/t/r2-s3-compat-doesnt-support-net-sdk-for-copy-operations-due-to-tagging-header/616867
|
||||
*/
|
||||
export async function copyReleasesPath(
|
||||
sourcePath: string,
|
||||
targetPath: string,
|
||||
): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
export async function copyReleasesPath(sourcePath: string, targetPath: string): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
|
||||
const listResult = await $({
|
||||
env: awsEnv,
|
||||
})`aws s3api list-objects --bucket rivet-releases --prefix ${sourcePath} --endpoint-url ${endpointUrl}`;
|
||||
const listResult = await $({
|
||||
env: awsEnv,
|
||||
})`aws s3api list-objects --bucket rivet-releases --prefix ${sourcePath} --endpoint-url ${endpointUrl}`;
|
||||
|
||||
const objects = JSON.parse(listResult.stdout);
|
||||
if (!objects.Contents?.length) {
|
||||
throw new Error(`No objects found under ${sourcePath}`);
|
||||
}
|
||||
const objects = JSON.parse(listResult.stdout);
|
||||
if (!objects.Contents?.length) {
|
||||
throw new Error(`No objects found under ${sourcePath}`);
|
||||
}
|
||||
|
||||
for (const obj of objects.Contents) {
|
||||
const sourceKey = obj.Key;
|
||||
const targetKey = sourceKey.replace(sourcePath, targetPath);
|
||||
console.log(` ${sourceKey} -> ${targetKey}`);
|
||||
await $({
|
||||
env: awsEnv,
|
||||
})`aws s3api copy-object --bucket rivet-releases --key ${targetKey} --copy-source rivet-releases/${sourceKey} --endpoint-url ${endpointUrl}`;
|
||||
}
|
||||
for (const obj of objects.Contents) {
|
||||
const sourceKey = obj.Key;
|
||||
const targetKey = sourceKey.replace(sourcePath, targetPath);
|
||||
console.log(` ${sourceKey} -> ${targetKey}`);
|
||||
await $({
|
||||
env: awsEnv,
|
||||
})`aws s3api copy-object --bucket rivet-releases --key ${targetKey} --copy-source rivet-releases/${sourceKey} --endpoint-url ${endpointUrl}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function assertEquals<T>(actual: T, expected: T, message?: string): void {
|
||||
if (actual !== expected) {
|
||||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
if (actual !== expected) {
|
||||
throw new Error(message || `Expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function assertExists<T>(
|
||||
value: T | null | undefined,
|
||||
message?: string,
|
||||
): asserts value is T {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(message || "Value does not exist");
|
||||
}
|
||||
export function assertExists<T>(value: T | null | undefined, message?: string): asserts value is T {
|
||||
if (value === null || value === undefined) {
|
||||
throw new Error(message || "Value does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertDirExists(dirPath: string): Promise<void> {
|
||||
try {
|
||||
const stat = await fs.stat(dirPath);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Path exists but is not a directory: ${dirPath}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new Error(`Directory not found: ${dirPath}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
const stat = await fs.stat(dirPath);
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`Path exists but is not a directory: ${dirPath}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.code === "ENOENT") {
|
||||
throw new Error(`Directory not found: ${dirPath}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFromReleases(
|
||||
remotePath: string,
|
||||
localPath: string,
|
||||
): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
})`aws s3 cp s3://rivet-releases/${remotePath} ${localPath} --endpoint-url ${endpointUrl}`;
|
||||
export async function downloadFromReleases(remotePath: string, localPath: string): Promise<void> {
|
||||
const { awsEnv, endpointUrl } = await getReleasesS3Config();
|
||||
await $({
|
||||
env: awsEnv,
|
||||
shell: true,
|
||||
stdio: "inherit",
|
||||
})`aws s3 cp s3://rivet-releases/${remotePath} ${localPath} --endpoint-url ${endpointUrl}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
{
|
||||
"name": "@sandbox-agent/testing",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "tsx test-sandbox.ts",
|
||||
"test:docker": "tsx test-sandbox.ts docker",
|
||||
"test:daytona": "tsx test-sandbox.ts daytona",
|
||||
"test:mock": "tsx test-sandbox.ts docker --agent=mock",
|
||||
"test:verbose": "tsx test-sandbox.ts docker --verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"@daytonaio/sdk": "latest",
|
||||
"@e2b/code-interpreter": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "latest",
|
||||
"tsx": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
"name": "@sandbox-agent/testing",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "tsx test-sandbox.ts",
|
||||
"test:docker": "tsx test-sandbox.ts docker",
|
||||
"test:daytona": "tsx test-sandbox.ts daytona",
|
||||
"test:mock": "tsx test-sandbox.ts docker --agent=mock",
|
||||
"test:verbose": "tsx test-sandbox.ts docker --verbose"
|
||||
},
|
||||
"dependencies": {
|
||||
"@daytonaio/sdk": "latest",
|
||||
"@e2b/code-interpreter": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "latest",
|
||||
"tsx": "latest",
|
||||
"typescript": "latest"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -68,12 +68,14 @@ async function main() {
|
|||
const referenceMap = buildReferenceMap(references);
|
||||
const template = await fsp.readFile(TEMPLATE_PATH, "utf8");
|
||||
|
||||
const skillFile = template
|
||||
.replace("{{QUICKSTART}}", quickstartContent)
|
||||
.replace("{{REFERENCE_MAP}}", referenceMap);
|
||||
const skillFile = template.replace("{{QUICKSTART}}", quickstartContent).replace("{{REFERENCE_MAP}}", referenceMap);
|
||||
|
||||
await fsp.writeFile(path.join(OUTPUT_ROOT, "SKILL.md"), `${skillFile.trim()}
|
||||
`, "utf8");
|
||||
await fsp.writeFile(
|
||||
path.join(OUTPUT_ROOT, "SKILL.md"),
|
||||
`${skillFile.trim()}
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
console.log(`Generated skill files in ${OUTPUT_ROOT}`);
|
||||
}
|
||||
|
|
@ -188,7 +190,10 @@ function buildReferenceMap(references) {
|
|||
|
||||
for (const group of sortedGroups) {
|
||||
lines.push(`### ${formatSegment(group)}`, "");
|
||||
const items = grouped.get(group).slice().sort((a, b) => a.title.localeCompare(b.title));
|
||||
const items = grouped
|
||||
.get(group)
|
||||
.slice()
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
for (const item of items) {
|
||||
lines.push(`- [${item.title}](${item.referencePath})`);
|
||||
}
|
||||
|
|
@ -281,9 +286,7 @@ function normalizeCodeBlock(block) {
|
|||
const opening = lines[0].trim();
|
||||
const closing = lines[lines.length - 1].trim();
|
||||
const contentLines = lines.slice(1, -1);
|
||||
const indents = contentLines
|
||||
.filter((line) => line.trim() !== "")
|
||||
.map((line) => line.match(/^\s*/)?.[0].length ?? 0);
|
||||
const indents = contentLines.filter((line) => line.trim() !== "").map((line) => line.match(/^\s*/)?.[0].length ?? 0);
|
||||
const minIndent = indents.length ? Math.min(...indents) : 0;
|
||||
const normalizedContent = contentLines.map((line) => line.slice(minIndent));
|
||||
|
||||
|
|
@ -344,7 +347,10 @@ function getAttributeValue(attrs, name) {
|
|||
}
|
||||
|
||||
function stripHtml(value) {
|
||||
return value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
||||
return value
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function collapseWhitespace(value) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue