feat: Marketplace package lifecycle — CLI, API, and service contract protocols#777
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…act for marketplace lifecycle - cli-plugin-commands.zod.ts: schemas for os plugin build/validate/publish - package-api.zod.ts: REST API endpoints for install/upgrade/resolve-deps/upload - package-service.ts: IPackageService contract for runtime package management - Tests for all new files (57 new tests, all passing) - Updated kernel/index.ts, api/index.ts, contracts/index.ts exports - Updated ROADMAP.md with completed items Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds spec-layer protocol definitions to complete the Marketplace package lifecycle surface area across CLI schemas, REST API contracts, and a runtime service contract interface.
Changes:
- Added
os pluginCLI command input/output schemas (+ tests) for build/validate/publish. - Added
/api/v1/packages/*REST API request/response schemas andPackageApiContractsregistry (+ tests). - Added
IPackageServiceTypeScript contract (+ tests) and updated exports/ROADMAP.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/spec/src/kernel/index.ts | Exports the new CLI plugin commands protocol from the kernel barrel. |
| packages/spec/src/kernel/cli-plugin-commands.zod.ts | Introduces Zod schemas for `os plugin build |
| packages/spec/src/kernel/cli-plugin-commands.test.ts | Adds Vitest coverage for the new CLI command schemas. |
| packages/spec/src/contracts/package-service.ts | Introduces IPackageService runtime contract for install/upgrade/rollback/deps/namespaces. |
| packages/spec/src/contracts/package-service.test.ts | Adds contract-shape tests and usage examples for IPackageService. |
| packages/spec/src/contracts/index.ts | Exports the new package-service contract from the contracts barrel. |
| packages/spec/src/api/package-api.zod.ts | Defines package lifecycle REST API schemas + PackageApiContracts registry and error codes. |
| packages/spec/src/api/package-api.test.ts | Adds Vitest coverage for the new package API schemas and contract registry. |
| packages/spec/src/api/index.ts | Exports the new package-api protocol from the API barrel. |
| ROADMAP.md | Marks protocol items complete and updates contract counts/matrix to include IPackageService. |
| namespaceConflicts: z.array(z.object({ | ||
| type: z.literal('namespace_conflict').describe('Error type'), | ||
| requestedNamespace: z.string().describe('Requested namespace'), | ||
| conflictingPackageId: z.string().describe('Conflicting package ID'), | ||
| conflictingPackageName: z.string().describe('Conflicting package name'), | ||
| suggestion: z.string().optional().describe('Suggested alternative'), | ||
| })).optional().describe('Namespace conflicts detected'), |
There was a problem hiding this comment.
namespaceConflicts duplicates the existing NamespaceConflictErrorSchema from kernel/package-registry.zod.ts. Reusing the shared schema would keep the API and kernel contract in sync (and avoid future drift if fields/descriptions change).
| * Request body for uploading a package artifact. | ||
| * | ||
| * @example POST /api/v1/packages/upload | ||
| * Content-Type: multipart/form-data | ||
| * { artifact: <metadata>, file: <binary> } | ||
| */ | ||
| export const UploadArtifactRequestSchema = z.object({ | ||
| /** Artifact metadata */ | ||
| artifact: PackageArtifactSchema.describe('Package artifact metadata'), | ||
|
|
||
| /** SHA256 checksum of the uploaded file (for verification) */ | ||
| sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() | ||
| .describe('SHA256 checksum of the uploaded file'), |
There was a problem hiding this comment.
The JSDoc for UploadArtifactRequestSchema says the request is multipart/form-data with a file field, but the schema only models JSON fields (artifact, sha256, token, releaseNotes) and has no way to represent the binary/upload reference. Either update the documentation to match the modeled shape, or extend the schema to include the intended upload mechanism (e.g., presigned upload reference, fileId, etc.).
| * Request body for uploading a package artifact. | |
| * | |
| * @example POST /api/v1/packages/upload | |
| * Content-Type: multipart/form-data | |
| * { artifact: <metadata>, file: <binary> } | |
| */ | |
| export const UploadArtifactRequestSchema = z.object({ | |
| /** Artifact metadata */ | |
| artifact: PackageArtifactSchema.describe('Package artifact metadata'), | |
| /** SHA256 checksum of the uploaded file (for verification) */ | |
| sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() | |
| .describe('SHA256 checksum of the uploaded file'), | |
| * Request body for uploading (registering) a package artifact. | |
| * | |
| * @example POST /api/v1/packages/upload | |
| * Content-Type: application/json | |
| * { | |
| * "artifact": { /* Package artifact metadata */ }, | |
| * "sha256": "0123abcd...ff", // optional | |
| * "token": "publisher-auth-token", // optional | |
| * "releaseNotes": "Details about this release" // optional | |
| * } | |
| */ | |
| export const UploadArtifactRequestSchema = z.object({ | |
| /** Artifact metadata */ | |
| artifact: PackageArtifactSchema.describe('Package artifact metadata'), | |
| /** SHA256 checksum of the associated artifact file (for verification) */ | |
| sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() | |
| .describe('SHA256 checksum of the associated artifact file'), |
| export const PackageApiErrorCode = z.enum([ | ||
| 'package_not_found', | ||
| 'package_already_installed', | ||
| 'version_not_found', | ||
| 'dependency_conflict', | ||
| 'namespace_conflict', | ||
| 'platform_incompatible', | ||
| 'artifact_invalid', | ||
| 'checksum_mismatch', | ||
| 'signature_invalid', | ||
| 'upgrade_failed', | ||
| 'rollback_failed', | ||
| 'snapshot_not_found', | ||
| 'upload_failed', | ||
| ]); |
There was a problem hiding this comment.
PackageApiErrorCode is missing a .describe(...) annotation, which breaks the repo’s established pattern of documenting top-level enums for JSON Schema/docs generation.
| export const PackageApiErrorCode = z.enum([ | |
| 'package_not_found', | |
| 'package_already_installed', | |
| 'version_not_found', | |
| 'dependency_conflict', | |
| 'namespace_conflict', | |
| 'platform_incompatible', | |
| 'artifact_invalid', | |
| 'checksum_mismatch', | |
| 'signature_invalid', | |
| 'upgrade_failed', | |
| 'rollback_failed', | |
| 'snapshot_not_found', | |
| 'upload_failed', | |
| ]); | |
| export const PackageApiErrorCode = z | |
| .enum([ | |
| 'package_not_found', | |
| 'package_already_installed', | |
| 'version_not_found', | |
| 'dependency_conflict', | |
| 'namespace_conflict', | |
| 'platform_incompatible', | |
| 'artifact_invalid', | |
| 'checksum_mismatch', | |
| 'signature_invalid', | |
| 'upgrade_failed', | |
| 'rollback_failed', | |
| 'snapshot_not_found', | |
| 'upload_failed', | |
| ]) | |
| .describe('Standardized error codes for package lifecycle operations in the Package API.'); |
| export const UninstallPackageApiRequestSchema = PackagePathParamsSchema; | ||
| export type UninstallPackageApiRequest = z.infer<typeof UninstallPackageApiRequestSchema>; | ||
|
|
||
| /** | ||
| * Response after uninstalling a package. | ||
| */ | ||
| export const UninstallPackageApiResponseSchema = BaseResponseSchema.extend({ | ||
| data: z.object({ | ||
| packageId: z.string().describe('Uninstalled package ID'), | ||
| success: z.boolean().describe('Whether uninstall succeeded'), | ||
| message: z.string().optional().describe('Uninstall status message'), | ||
| }), | ||
| }).describe('Uninstall package response'); | ||
| export type UninstallPackageApiResponse = z.infer<typeof UninstallPackageApiResponseSchema>; | ||
|
|
There was a problem hiding this comment.
Naming is inconsistent with the rest of the API schemas: UninstallPackageApiRequestSchema / UninstallPackageApiResponseSchema are the only schemas in src/api using the *Api* infix. Consider renaming to match the established XRequestSchema / XResponseSchema pattern used elsewhere (e.g., GetFeedRequestSchema, ScheduleExportRequestSchema).
| export const UninstallPackageApiRequestSchema = PackagePathParamsSchema; | |
| export type UninstallPackageApiRequest = z.infer<typeof UninstallPackageApiRequestSchema>; | |
| /** | |
| * Response after uninstalling a package. | |
| */ | |
| export const UninstallPackageApiResponseSchema = BaseResponseSchema.extend({ | |
| data: z.object({ | |
| packageId: z.string().describe('Uninstalled package ID'), | |
| success: z.boolean().describe('Whether uninstall succeeded'), | |
| message: z.string().optional().describe('Uninstall status message'), | |
| }), | |
| }).describe('Uninstall package response'); | |
| export type UninstallPackageApiResponse = z.infer<typeof UninstallPackageApiResponseSchema>; | |
| export const UninstallPackageRequestSchema = PackagePathParamsSchema; | |
| export type UninstallPackageRequest = z.infer<typeof UninstallPackageRequestSchema>; | |
| /** | |
| * Response after uninstalling a package. | |
| */ | |
| export const UninstallPackageResponseSchema = BaseResponseSchema.extend({ | |
| data: z.object({ | |
| packageId: z.string().describe('Uninstalled package ID'), | |
| success: z.boolean().describe('Whether uninstall succeeded'), | |
| message: z.string().optional().describe('Uninstall status message'), | |
| }), | |
| }).describe('Uninstall package response'); | |
| export type UninstallPackageResponse = z.infer<typeof UninstallPackageResponseSchema>; | |
| /** | |
| * @deprecated Use {@link UninstallPackageRequest} instead. | |
| * Legacy alias retained for backwards compatibility. | |
| */ | |
| export type UninstallPackageApiRequest = UninstallPackageRequest; | |
| /** | |
| * @deprecated Use {@link UninstallPackageResponse} instead. | |
| * Legacy alias retained for backwards compatibility. | |
| */ | |
| export type UninstallPackageApiResponse = UninstallPackageResponse; |
| listPackages(options?: { status?: string; enabled?: boolean }): Promise<InstalledPackage[]>; | ||
|
|
There was a problem hiding this comment.
listPackages accepts options?: { status?: string; enabled?: boolean }, but status is a well-defined enum (PackageStatusEnum/PackageStatus) in kernel/package-registry.zod.ts. Typing this as string loses type-safety and makes it easier to pass unsupported values; consider using the existing PackageStatus type (or a union of its literals) here.
Adds protocol-layer definitions for the marketplace package lifecycle: CLI command schemas (
build/validate/publish), REST API endpoint contracts, and theIPackageServiceruntime interface. This completes sections 2–4 of the lifecycle issue at the spec level.New schemas
kernel/cli-plugin-commands.zod.ts—PluginBuild{Options,Result}Schema,PluginValidate{Options,Result}Schema,PluginPublish{Options,Result}Schema,ValidationFindingSchemawith severity levelsapi/package-api.zod.ts— 8 endpoints inPackageApiContracts(install, upgrade, resolve-dependencies, upload, rollback, uninstall, list, get) extendingBaseResponseSchema, plusPackageApiErrorCodeenumcontracts/package-service.ts—IPackageServicewithinstall,resolveDependencies,checkNamespaces,planUpgrade,upgrade,rollback,uploadArtifact,togglePackageUsage
Housekeeping
kernel/index.ts,api/index.ts,contracts/index.tsOriginal prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.