← Files NV oOS GatewayARCHIVED FILE
bin/package-plugin.mjs
3.44 KB · Nov 4, 2026 · 18:12 UTC
#!/usr/bin/env node
/**
* Package the plugin folder as a ZIP for the OpenAI Agents API
* `environment.plugins` contract (inline base64 uploads) and for the plugin
* submission portal.
*
* OpenAI requires each ZIP to contain ONE plugin folder with the manifest
* inside it, so this script stages the plugin root under a folder named
* after the manifest `name` (e.g. nvoos-site-bridge/) before archiving.
* The manifest name/description are printed for the session-create request,
* whose name and description must match the manifest.
*
* Usage:
* node bin/package-plugin.mjs [output.zip]
*
* Cross-platform: `zip` on POSIX runners (mirror CI), bsdtar on Windows
* (System32 tar.exe — PowerShell's Compress-Archive writes backslash entry
* separators, which the OpenAI portal rejects as unsafe paths).
* Dependency-free: Node built-ins only.
*
* @see https://developers.openai.com/api/docs/guides/agents-api/tools/plugins
*/
import {
readFileSync,
existsSync,
mkdirSync,
mkdtempSync,
cpSync,
rmSync,
} from 'node:fs';
import { dirname, join, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
const root = join( dirname( fileURLToPath( import.meta.url ) ), '..' );
const manifestPath = join( root, 'plugin.json' );
if ( !existsSync( manifestPath ) ) {
console.error( 'plugin.json not found in', root );
process.exit( 1 );
}
const manifest = JSON.parse( readFileSync( manifestPath, 'utf8' ) );
const out = process.argv[2] || join( root, 'dist', 'nvoos-gateway.zip' );
mkdirSync( dirname( out ), { recursive: true } );
// Stage the plugin under a single folder inside a temp dir, excluding build
// junk and local secrets, then archive the staging dir so the ZIP opens as
// <name>/plugin.json, <name>/skills/..., etc.
const staging = mkdtempSync( join( tmpdir(), 'nvoos-plugin-' ) );
const folder = join( staging, manifest.name );
try {
cpSync( root, folder, {
recursive: true,
filter: ( src ) => {
const rel = src.slice( root.length );
if ( rel === '' ) {
return true;
}
const parts = rel.split( sep );
// Dot-prefixed members are stripped: the OpenAI portal
// validator rejects hidden paths as unsafe.
return !parts.includes( 'dist' ) &&
!parts.includes( 'node_modules' ) &&
!parts.includes( '.git' ) &&
!parts.includes( '.github' ) &&
!parts.includes( '.codex-plugin' ) &&
!parts.includes( 'docs' ) &&
!parts.some( ( p ) => p.startsWith( '.' ) ) &&
!parts.some( ( p ) => p === '.env' || p.startsWith( '.env.' ) );
},
} );
const isWindows = process.platform === 'win32';
if ( isWindows ) {
// System32 bsdtar writes spec-compliant forward-slash ZIP entry
// names. Resolve it explicitly — a Git Bash PATH puts GNU tar first,
// and GNU tar cannot create ZIP archives.
const sysTar = join(
process.env.SystemRoot || 'C:\\Windows',
'System32',
'tar.exe'
);
execFileSync(
sysTar,
[ '-a', '-c', '-f', out, '-C', staging, manifest.name ],
{ stdio: 'inherit' }
);
} else {
execFileSync( 'zip', [ '-r', '-q', out, manifest.name ], { cwd: staging, stdio: 'inherit' } );
}
} catch ( error ) {
console.error( 'Archive step failed.', error.message );
process.exit( 1 );
} finally {
rmSync( staging, { recursive: true, force: true } );
}
console.log( 'Archive written:', out );
console.log( 'Manifest name:', manifest.name );
console.log( 'Manifest description:', manifest.description );
SHA-256: 0c3a7a0155fcb4998fdaaa2ee2b236e52457cb48a5f2d60dd9df7de82ccbee76