From da4c738638d9f2f32ed4d3ca4e2f3ffac5567ae0 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 6 Aug 2026 13:35:26 +0000 Subject: [PATCH 1/9] feat(targets): add vercel deploy target Adds a release-gated `vercel` target that promotes a prebuilt docs artifact to production via `vercel deploy --prod --prebuilt`, modeled on the existing cloudflare target. Reads VERCEL_TOKEN (secret) plus optional VERCEL_ORG_ID / VERCEL_PROJECT_ID identifiers, guards against dry-run, and bundles the vercel CLI in the Docker image. Fixes #864 --- Dockerfile | 4 + docs/src/content/docs/targets/index.md | 1 + docs/src/content/docs/targets/vercel.md | 63 ++++++ src/targets/__tests__/vercel.test.ts | 264 ++++++++++++++++++++++ src/targets/index.ts | 2 + src/targets/vercel.ts | 280 ++++++++++++++++++++++++ 6 files changed, 614 insertions(+) create mode 100644 docs/src/content/docs/targets/vercel.md create mode 100644 src/targets/__tests__/vercel.test.ts create mode 100644 src/targets/vercel.ts diff --git a/Dockerfile b/Dockerfile index c957ed926..1cda4b4e3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -104,6 +104,10 @@ RUN curl -fsSL https://github.com/PowerShell/PowerShell/releases/download/v7.4.1 RUN npm install -g wrangler@4.111.0 \ && wrangler --version +# Vercel CLI for the "vercel" target (pinned) +RUN npm install -g vercel@58.7.1 \ + && vercel --version + # craft does `git` things against mounted directories as root RUN git config --global --add safe.directory '*' diff --git a/docs/src/content/docs/targets/index.md b/docs/src/content/docs/targets/index.md index 8218e9c34..4cf1ab53a 100644 --- a/docs/src/content/docs/targets/index.md +++ b/docs/src/content/docs/targets/index.md @@ -19,6 +19,7 @@ Targets define where Craft publishes your release artifacts. Configure them in ` | [GCS](./gcs/) | Upload to Google Cloud Storage | | [GitHub Pages](./gh-pages/) | Deploy static sites | | [Cloudflare](./cloudflare/) | Deploy static sites or Workers to Cloudflare | +| [Vercel](./vercel/) | Deploy a prebuilt static site to Vercel | | [CocoaPods](./cocoapods/) | Publish iOS/macOS pods | | [Ruby Gems](./gem/) | Publish Ruby gems | | [Maven](./maven/) | Publish to Maven Central | diff --git a/docs/src/content/docs/targets/vercel.md b/docs/src/content/docs/targets/vercel.md new file mode 100644 index 000000000..33de06712 --- /dev/null +++ b/docs/src/content/docs/targets/vercel.md @@ -0,0 +1,63 @@ +--- +title: Vercel +description: Deploy a prebuilt static site to Vercel +--- + +Deploys a release artifact to [Vercel](https://vercel.com/) as a production deployment. + +The target extracts a ZIP artifact and shells out to the [`vercel`](https://vercel.com/docs/cli) CLI to promote it to production (`vercel deploy --prod`). The `vercel` CLI is bundled in the Craft Docker image. + +This target is release-gated: it runs as part of `craft publish`, so a deployment only happens on release and the deployed site stays in sync with the published version — the same guarantee the [`gh-pages`](./gh-pages/) target provides, but for Vercel-hosted sites. + +## Configuration + +| Option | Description | +|--------|-------------| +| `prebuilt` | Whether the artifact contains a prebuilt `.vercel/output` (the result of `vercel build`). When `true` (default), the CLI is invoked with `--prebuilt` and skips the remote build. Set to `false` to have Vercel build from source. | +| `vercelCliPath` | Path to the `vercel` binary. Default: `vercel` (or the `VERCEL_BIN` env var). | +| `workingDir` | Subdirectory within the extracted artifact to deploy from. | + +## Environment Variables + +| Name | Required | Description | +|------|----------|-------------| +| `VERCEL_TOKEN` | Yes | Vercel access token. Passed to the CLI via the environment, never on the command line. | +| `VERCEL_ORG_ID` | No | Vercel organization/team ID. An identifier, not a secret. Forwarded to the CLI to link the deployment non-interactively; when unset, the CLI falls back to the `.vercel/project.json` inside the artifact. | +| `VERCEL_PROJECT_ID` | No | Vercel project ID. An identifier, not a secret. Same behavior as `VERCEL_ORG_ID`. | + +:::note +For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (or ship a `.vercel/project.json` in the artifact) so the CLI knows which project to deploy to. +::: + +## Default Behavior + +By default, this target: + +1. Looks for a single artifact matching `vercel.zip` (or `*-vercel.zip`). Override with `includeNames`. +2. Extracts its contents (flattening a single top-level directory if present). +3. Deploys to production via `vercel deploy --prod --prebuilt`. + +The version being released is attached to the deployment as `--meta craftRelease=` for traceability. + +## Example + +```yaml +targets: + - name: vercel + # prebuilt defaults to true: the docs site is built in CI and this + # target only promotes the prebuilt output to production. +``` + +Deploying from a subdirectory of the artifact: + +```yaml +targets: + - name: vercel + workingDir: docs +``` + +## Workflow + +1. Build the site in CI (e.g. `vercel build`) and create a `vercel.zip` artifact containing the prebuilt `.vercel/output` (or the source when `prebuilt: false`). +2. Configure the target in `.craft.yml`. +3. Set `VERCEL_TOKEN` in your environment, plus `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (or include a `.vercel/project.json` in the artifact) so the deploy targets the right project. diff --git a/src/targets/__tests__/vercel.test.ts b/src/targets/__tests__/vercel.test.ts new file mode 100644 index 000000000..8ae6baeac --- /dev/null +++ b/src/targets/__tests__/vercel.test.ts @@ -0,0 +1,264 @@ +import { vi } from 'vitest'; + +import { VercelTarget, targetSecrets } from '../vercel'; +import { NoneArtifactProvider } from '../../artifact_providers/none'; +import * as system from '../../utils/system'; +import { isDryRun } from '../../utils/helpers'; + +const TMP_DIR = '/tmp/craft-vercel-test'; +const DEFAULT_SECRET_VALUE = 'secret_value'; +const ORG_ID = 'org_1234'; +const PROJECT_ID = 'prj_1234'; + +vi.mock('../../utils/helpers'); + +vi.mock('../../utils/system', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + checkExecutableIsPresent: vi.fn(), + spawnProcess: vi.fn(async () => undefined), + extractZipArchiveWithFlattening: vi.fn(async () => undefined), + }; +}); + +vi.mock('../../utils/files', async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + withTempDir: async (cb: (dir: string) => Promise) => cb(TMP_DIR), + }; +}); + +function setTargetSecretsInEnv(): void { + for (const secret of targetSecrets) { + process.env[secret] = DEFAULT_SECRET_VALUE; + } +} + +function removeTargetSecretsFromEnv(): void { + for (const secret of targetSecrets) { + delete process.env[secret]; + } +} + +function createVercelTarget( + targetConfig?: Record, +): VercelTarget { + return new VercelTarget( + { + name: 'vercel', + ...targetConfig, + }, + new NoneArtifactProvider(), + { owner: 'testOwner', repo: 'testRepo' }, + ); +} + +beforeEach(() => { + setTargetSecretsInEnv(); + delete process.env.VERCEL_BIN; + delete process.env.VERCEL_ORG_ID; + delete process.env.VERCEL_PROJECT_ID; + (isDryRun as any).mockReturnValue(false); +}); + +afterEach(() => { + removeTargetSecretsFromEnv(); + delete process.env.VERCEL_ORG_ID; + delete process.env.VERCEL_PROJECT_ID; + vi.clearAllMocks(); +}); + +describe('vercel target configuration', () => { + test('exports the expected secrets (only the token is a secret)', () => { + expect(targetSecrets).toContain('VERCEL_TOKEN'); + expect(targetSecrets).not.toContain('VERCEL_ORG_ID'); + expect(targetSecrets).not.toContain('VERCEL_PROJECT_ID'); + }); + + test('enforces the required token secret', () => { + removeTargetSecretsFromEnv(); + + expect(() => createVercelTarget({})).toThrowErrorMatchingInlineSnapshot( + `[Error: Required value(s) VERCEL_TOKEN not found in configuration files or the environment. See the documentation for more details.]`, + ); + }); + + test('does not require VERCEL_ORG_ID or VERCEL_PROJECT_ID', () => { + expect(() => createVercelTarget({})).not.toThrow(); + }); + + test('applies default options', () => { + const target = createVercelTarget({}); + + expect(target.vercelConfig).toStrictEqual({ + VERCEL_TOKEN: DEFAULT_SECRET_VALUE, + prebuilt: true, + vercelCliPath: 'vercel', + workingDir: undefined, + orgId: undefined, + projectId: undefined, + }); + }); + + test('picks up VERCEL_ORG_ID and VERCEL_PROJECT_ID from env when set', () => { + process.env.VERCEL_ORG_ID = ORG_ID; + process.env.VERCEL_PROJECT_ID = PROJECT_ID; + const target = createVercelTarget({}); + expect(target.vercelConfig.orgId).toBe(ORG_ID); + expect(target.vercelConfig.projectId).toBe(PROJECT_ID); + }); + + test('allows overriding default options', () => { + const target = createVercelTarget({ + prebuilt: false, + vercelCliPath: '/custom/vercel', + workingDir: 'subdir', + }); + + expect(target.vercelConfig).toStrictEqual({ + VERCEL_TOKEN: DEFAULT_SECRET_VALUE, + prebuilt: false, + vercelCliPath: '/custom/vercel', + workingDir: 'subdir', + orgId: undefined, + projectId: undefined, + }); + }); + + test('resolves vercel path from VERCEL_BIN env', () => { + process.env.VERCEL_BIN = '/env/vercel'; + const target = createVercelTarget({}); + expect(target.vercelConfig.vercelCliPath).toBe('/env/vercel'); + }); + + test('checks vercel is present in the constructor', () => { + createVercelTarget({}); + expect(system.checkExecutableIsPresent).toHaveBeenCalledWith('vercel'); + }); + + test('rejects config values that look like env-var expansions', () => { + expect(() => createVercelTarget({ workingDir: '${VERCEL_TOKEN}' })).toThrow( + /workingDir.*must not be an environment-variable expansion/, + ); + }); +}); + +describe('publish', () => { + const revision = 'deadbeef'; + const version = '1.2.3'; + const artifact = { filename: 'vercel.zip' } as any; + + function stubArtifacts(target: VercelTarget, artifacts: any[]): void { + target.getArtifactsForRevision = vi.fn(async () => artifacts); + target.artifactProvider.downloadArtifact = vi.fn( + async () => '/downloads/vercel.zip', + ); + } + + test('deploys a prebuilt artifact to production with provenance', async () => { + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + expect(system.extractZipArchiveWithFlattening).toHaveBeenCalledWith( + '/downloads/vercel.zip', + TMP_DIR, + ); + + expect(system.spawnProcess).toHaveBeenCalledTimes(1); + const [bin, args, options] = (system.spawnProcess as any).mock.calls[0]; + expect(bin).toBe('vercel'); + expect(args).toEqual([ + 'deploy', + '--prod', + '--yes', + '--prebuilt', + '--meta', + `craftRelease=${version}`, + ]); + expect(options.cwd).toBe(TMP_DIR); + // Secret must be in env, not argv + expect(options.env.VERCEL_TOKEN).toBe(DEFAULT_SECRET_VALUE); + expect(args).not.toContain(DEFAULT_SECRET_VALUE); + }); + + test('omits --prebuilt when prebuilt is false', async () => { + const target = createVercelTarget({ prebuilt: false }); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + const [, args] = (system.spawnProcess as any).mock.calls[0]; + expect(args).not.toContain('--prebuilt'); + }); + + test('does not forward org/project IDs when unset', async () => { + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + const [, , options] = (system.spawnProcess as any).mock.calls[0]; + expect('VERCEL_ORG_ID' in options.env).toBe(false); + expect('VERCEL_PROJECT_ID' in options.env).toBe(false); + }); + + test('forwards org/project IDs when set', async () => { + process.env.VERCEL_ORG_ID = ORG_ID; + process.env.VERCEL_PROJECT_ID = PROJECT_ID; + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + const [, , options] = (system.spawnProcess as any).mock.calls[0]; + expect(options.env.VERCEL_ORG_ID).toBe(ORG_ID); + expect(options.env.VERCEL_PROJECT_ID).toBe(PROJECT_ID); + }); + + test('deploys from workingDir subdirectory when configured', async () => { + const target = createVercelTarget({ workingDir: 'dist' }); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + const [, , options] = (system.spawnProcess as any).mock.calls[0]; + expect(options.cwd).toBe(`${TMP_DIR}/dist`); + }); + + test('reports an error and does not deploy when no artifacts found', async () => { + const target = createVercelTarget({}); + stubArtifacts(target, []); + + await expect(target.publish(version, revision)).rejects.toThrow( + /no artifacts found/, + ); + expect(system.spawnProcess).not.toHaveBeenCalled(); + }); + + test('reports an error when more than one artifact found', async () => { + const target = createVercelTarget({}); + stubArtifacts(target, [artifact, artifact]); + + await expect(target.publish(version, revision)).rejects.toThrow( + /more than one Vercel archive/, + ); + expect(system.spawnProcess).not.toHaveBeenCalled(); + }); + + test('does not deploy in dry-run mode (including worktree mode)', async () => { + (isDryRun as any).mockReturnValue(true); + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + // Artifact is still extracted (local, safe), but the remote deploy is + // skipped. + expect(system.extractZipArchiveWithFlattening).toHaveBeenCalled(); + expect(system.spawnProcess).not.toHaveBeenCalled(); + }); +}); diff --git a/src/targets/index.ts b/src/targets/index.ts index ff51af271..2b2053edc 100644 --- a/src/targets/index.ts +++ b/src/targets/index.ts @@ -21,6 +21,7 @@ import { PubDevTarget } from './pubDev'; import { HexTarget } from './hex'; import { CommitOnGitRepositoryTarget } from './commitOnGitRepository'; import { PowerShellTarget } from './powershell'; +import { VercelTarget } from './vercel'; export const TARGET_MAP: { [key: string]: typeof BaseTarget } = { brew: BrewTarget, @@ -45,6 +46,7 @@ export const TARGET_MAP: { [key: string]: typeof BaseTarget } = { hex: HexTarget, 'commit-on-git-repository': CommitOnGitRepositoryTarget, powershell: PowerShellTarget, + vercel: VercelTarget, }; /** Targets that are treated specially */ diff --git a/src/targets/vercel.ts b/src/targets/vercel.ts new file mode 100644 index 000000000..3b0514f78 --- /dev/null +++ b/src/targets/vercel.ts @@ -0,0 +1,280 @@ +import { join } from 'path'; + +import { + GitHubGlobalConfig, + TargetConfig, + TypedTargetConfig, +} from '../schemas/project_config'; +import { checkEnvForPrerequisite } from '../utils/env'; +import { ConfigurationError, reportError } from '../utils/errors'; +import { withTempDir } from '../utils/files'; +import { isDryRun } from '../utils/helpers'; +import { logDryRun } from '../utils/dryRun'; +import { + checkExecutableIsPresent, + extractZipArchiveWithFlattening, + resolveExecutable, + spawnProcess, +} from '../utils/system'; +import { BaseTarget } from './base'; +import { BaseArtifactProvider } from '../artifact_providers/base'; + +/** + * Secrets required to authenticate with Vercel. + * + * Only the token is a true secret. The org and project IDs are identifiers, not + * credentials, and are handled separately (see the `*_ID_ENV_VAR` constants + * below): they are optional and, when set, forwarded to the Vercel CLI through + * the environment. + * + * Exported so tests (and documentation tooling) can reference the canonical + * list of environment variables this target consumes. + */ +export const targetSecrets = ['VERCEL_TOKEN'] as const; +type SecretsType = (typeof targetSecrets)[number]; + +/** + * Optional, non-secret identifiers forwarded to the Vercel CLI when present. + * They link the deployment to a specific org/project non-interactively, which + * is what CI needs (there is no interactive `vercel link` step). When unset, + * the CLI falls back to the `.vercel/project.json` inside the artifact. + */ +const ORG_ID_ENV_VAR = 'VERCEL_ORG_ID'; +const PROJECT_ID_ENV_VAR = 'VERCEL_PROJECT_ID'; + +/** Vercel executable configuration */ +const VERCEL_CONFIG = { + name: 'vercel', + envVar: 'VERCEL_BIN', + errorHint: + 'Install the Vercel CLI (npm install -g vercel) or set VERCEL_BIN to its path', +} as const; + +/** + * Matches a string that is exactly an environment-variable expansion, e.g. + * `${VERCEL_TOKEN}`. `spawnProcess` expands args of this exact form against the + * environment (which includes the token), so any value flowing into the CLI + * argv must be rejected if it matches. + */ +const ENV_EXPANSION_REGEX = /^\$\{.*\}$/; + +/** + * Regex for the Vercel deploy archive. + * + * Matches e.g. `vercel.zip` or `my-docs-vercel.zip`. Can be overridden via the + * `includeNames` target option. + */ +const DEFAULT_DEPLOY_ARCHIVE_REGEX = /^(?:.+-)?vercel\.zip$/; + +/** Fields on the vercel target config accessed at runtime */ +interface VercelConfigFields extends Record { + prebuilt?: boolean; + vercelCliPath?: string; + workingDir?: string; +} + +/** Target options for "vercel" */ +export interface VercelTargetConfig { + /** + * Whether the artifact contains a prebuilt `.vercel/output` (the result of + * `vercel build`). When true, the CLI is invoked with `--prebuilt` and skips + * the remote build step. Defaults to true: the docs website is built in CI + * and the release just promotes the prebuilt output to production. + */ + prebuilt: boolean; + /** Resolved path/name of the vercel binary */ + vercelCliPath: string; + /** Subdirectory within the extracted artifact to deploy from */ + workingDir?: string; + /** + * Optional Vercel org ID (an identifier, not a secret). Forwarded to the CLI + * through the environment when set. + */ + orgId?: string; + /** + * Optional Vercel project ID (an identifier, not a secret). Forwarded to the + * CLI through the environment when set. + */ + projectId?: string; +} + +/** + * Full config for the "vercel" target, including secrets. + */ +export type VercelTargetFullConfig = VercelTargetConfig & + Record; + +/** + * Target responsible for deploying a prebuilt static site to Vercel. + * + * Shells out to the `vercel` CLI to promote a release artifact to production + * (`vercel deploy --prod`). Intended for release-gated documentation sites: the + * artifact is built in CI and this target only publishes it, keeping the docs + * in sync with the released version. + */ +export class VercelTarget extends BaseTarget { + /** Target name */ + public readonly name: string = 'vercel'; + /** Target options */ + public readonly vercelConfig: VercelTargetFullConfig; + /** GitHub repo configuration */ + public readonly githubRepo: GitHubGlobalConfig; + + public constructor( + config: TargetConfig, + artifactProvider: BaseArtifactProvider, + githubRepo: GitHubGlobalConfig, + ) { + super(config, artifactProvider, githubRepo); + this.githubRepo = githubRepo; + this.vercelConfig = this.getVercelConfig(); + checkExecutableIsPresent(this.vercelConfig.vercelCliPath); + } + + /** + * Extracts, validates and returns the "vercel" target options. + * + * @returns the vercel config for this target. + */ + public getVercelConfig(): VercelTargetFullConfig { + const config = this.config as TypedTargetConfig; + + // These config values are passed to the CLI as command-line arguments. + // spawnProcess() expands args of the exact form "${VAR}" using the + // environment -- which includes VERCEL_TOKEN. Reject such values so a + // config string can never be expanded into a secret. + if ( + typeof config.workingDir === 'string' && + ENV_EXPANSION_REGEX.test(config.workingDir) + ) { + throw new ConfigurationError( + `[vercel] "workingDir" must not be an environment-variable ` + + `expansion (got "${config.workingDir}")`, + ); + } + + const vercelCliPath = config.vercelCliPath + ? config.vercelCliPath + : resolveExecutable(VERCEL_CONFIG); + + return { + prebuilt: config.prebuilt ?? true, + vercelCliPath, + workingDir: config.workingDir, + // Optional, non-secret identifiers. Forwarded to the CLI when present. + orgId: process.env[ORG_ID_ENV_VAR] || undefined, + projectId: process.env[PROJECT_ID_ENV_VAR] || undefined, + ...this.getTargetSecrets(), + }; + } + + private getTargetSecrets(): Record { + return targetSecrets + .map(name => { + checkEnvForPrerequisite({ name }); + return { + name, + value: process.env[name] as string, + }; + }) + .reduce( + (prev, current) => ({ + ...prev, + [current.name]: current.value, + }), + {}, + ) as Record; + } + + /** + * Builds the vercel CLI argument list for a production deploy. + * + * @param version The version being released + */ + private getVercelArgs(version: string): string[] { + // `--prod` promotes to production; `--yes` skips interactive prompts (CI). + const args = ['deploy', '--prod', '--yes']; + if (this.vercelConfig.prebuilt) { + args.push('--prebuilt'); + } + // Attach release provenance so the deployment is traceable to the version. + args.push('--meta', `craftRelease=${version}`); + return args; + } + + /** + * Deploys the release artifact to Vercel via the `vercel` CLI. + * + * @param version New version to be released + * @param revision Git commit SHA to be published + */ + public async publish(version: string, revision: string): Promise { + this.logger.debug('Fetching artifact list...'); + const packageFiles = await this.getArtifactsForRevision(revision, { + includeNames: DEFAULT_DEPLOY_ARCHIVE_REGEX, + }); + if (!packageFiles.length) { + reportError('Cannot deploy to Vercel: no artifacts found'); + return; + } else if (packageFiles.length > 1) { + reportError( + `Not implemented: more than one Vercel archive found\nDetails: ${JSON.stringify( + packageFiles, + )}`, + ); + return; + } + + const archivePath = await this.artifactProvider.downloadArtifact( + packageFiles[0], + ); + + await withTempDir( + async directory => { + this.logger.info(`Extracting "${archivePath}" to "${directory}"...`); + await extractZipArchiveWithFlattening(archivePath, directory); + + const deployDir = this.vercelConfig.workingDir + ? join(directory, this.vercelConfig.workingDir) + : directory; + + const args = this.getVercelArgs(version); + + // A Vercel deploy is a remote, irreversible operation with no local + // isolation. Unlike git/fs operations, it must NEVER run in dry-run + // mode -- including worktree mode, where spawnProcess would otherwise + // execute the command for real. Guard explicitly here. + if (isDryRun()) { + logDryRun(`${this.vercelConfig.vercelCliPath} ${args.join(' ')}`); + return; + } + + const env: NodeJS.ProcessEnv = { + ...process.env, + VERCEL_TOKEN: this.vercelConfig.VERCEL_TOKEN, + }; + // Org/project IDs are optional identifiers that link the deploy + // non-interactively: forward them only when set, otherwise let the CLI + // fall back to the artifact's `.vercel/project.json`. + if (this.vercelConfig.orgId) { + env.VERCEL_ORG_ID = this.vercelConfig.orgId; + } + if (this.vercelConfig.projectId) { + env.VERCEL_PROJECT_ID = this.vercelConfig.projectId; + } + + this.logger.info('Deploying to Vercel...'); + await spawnProcess( + this.vercelConfig.vercelCliPath, + args, + { cwd: deployDir, env }, + { showStdout: true }, + ); + }, + true, + 'craft-vercel-', + ); + + this.logger.info('Vercel deploy complete'); + } +} From 94f70493f052c610d1c3f4ac859cad11ce97e188 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 6 Aug 2026 13:47:56 +0000 Subject: [PATCH 2/9] fix(vercel): preserve archive layout instead of flattening extractZipArchiveWithFlattening unwraps a sole top-level directory, which would turn a prebuilt `.vercel/output` artifact into a bare `output/` and break `vercel deploy --prebuilt`. Use plain extractZipArchive so the `.vercel` layout is preserved. #skip-changelog --- docs/src/content/docs/targets/vercel.md | 2 +- src/targets/__tests__/vercel.test.ts | 6 +++--- src/targets/vercel.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/src/content/docs/targets/vercel.md b/docs/src/content/docs/targets/vercel.md index 33de06712..2d14d99b7 100644 --- a/docs/src/content/docs/targets/vercel.md +++ b/docs/src/content/docs/targets/vercel.md @@ -34,7 +34,7 @@ For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT By default, this target: 1. Looks for a single artifact matching `vercel.zip` (or `*-vercel.zip`). Override with `includeNames`. -2. Extracts its contents (flattening a single top-level directory if present). +2. Extracts its contents (preserving the archive layout, e.g. a top-level `.vercel/output`). 3. Deploys to production via `vercel deploy --prod --prebuilt`. The version being released is attached to the deployment as `--meta craftRelease=` for traceability. diff --git a/src/targets/__tests__/vercel.test.ts b/src/targets/__tests__/vercel.test.ts index 8ae6baeac..58b92146a 100644 --- a/src/targets/__tests__/vercel.test.ts +++ b/src/targets/__tests__/vercel.test.ts @@ -18,7 +18,7 @@ vi.mock('../../utils/system', async importOriginal => { ...actual, checkExecutableIsPresent: vi.fn(), spawnProcess: vi.fn(async () => undefined), - extractZipArchiveWithFlattening: vi.fn(async () => undefined), + extractZipArchive: vi.fn(async () => undefined), }; }); @@ -163,7 +163,7 @@ describe('publish', () => { await target.publish(version, revision); - expect(system.extractZipArchiveWithFlattening).toHaveBeenCalledWith( + expect(system.extractZipArchive).toHaveBeenCalledWith( '/downloads/vercel.zip', TMP_DIR, ); @@ -258,7 +258,7 @@ describe('publish', () => { // Artifact is still extracted (local, safe), but the remote deploy is // skipped. - expect(system.extractZipArchiveWithFlattening).toHaveBeenCalled(); + expect(system.extractZipArchive).toHaveBeenCalled(); expect(system.spawnProcess).not.toHaveBeenCalled(); }); }); diff --git a/src/targets/vercel.ts b/src/targets/vercel.ts index 3b0514f78..67ff358d5 100644 --- a/src/targets/vercel.ts +++ b/src/targets/vercel.ts @@ -12,7 +12,7 @@ import { isDryRun } from '../utils/helpers'; import { logDryRun } from '../utils/dryRun'; import { checkExecutableIsPresent, - extractZipArchiveWithFlattening, + extractZipArchive, resolveExecutable, spawnProcess, } from '../utils/system'; @@ -232,7 +232,7 @@ export class VercelTarget extends BaseTarget { await withTempDir( async directory => { this.logger.info(`Extracting "${archivePath}" to "${directory}"...`); - await extractZipArchiveWithFlattening(archivePath, directory); + await extractZipArchive(archivePath, directory); const deployDir = this.vercelConfig.workingDir ? join(directory, this.vercelConfig.workingDir) From 003a98aee8ef2147f0afe33aab33a0f1d40b9ac3 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 6 Aug 2026 16:06:59 +0000 Subject: [PATCH 3/9] refactor(vercel): deploy via the Vercel API instead of the CLI use @vercel/client's createDeployment instead of shelling out to the pinned vercel CLI. drops the CLI from the Docker image and removes the argv env-expansion guard (nothing goes to a command line anymore). org/project IDs are forwarded as teamId/name; token stays in-process. --- Dockerfile | 4 - docs/src/content/docs/targets/vercel.md | 17 +- package.json | 1 + pnpm-lock.yaml | 664 ++++++++++++++++++++++++ src/targets/__tests__/vercel.test.ts | 116 +++-- src/targets/vercel.ts | 162 +++--- 6 files changed, 797 insertions(+), 167 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1cda4b4e3..c957ed926 100644 --- a/Dockerfile +++ b/Dockerfile @@ -104,10 +104,6 @@ RUN curl -fsSL https://github.com/PowerShell/PowerShell/releases/download/v7.4.1 RUN npm install -g wrangler@4.111.0 \ && wrangler --version -# Vercel CLI for the "vercel" target (pinned) -RUN npm install -g vercel@58.7.1 \ - && vercel --version - # craft does `git` things against mounted directories as root RUN git config --global --add safe.directory '*' diff --git a/docs/src/content/docs/targets/vercel.md b/docs/src/content/docs/targets/vercel.md index 2d14d99b7..e136391ae 100644 --- a/docs/src/content/docs/targets/vercel.md +++ b/docs/src/content/docs/targets/vercel.md @@ -5,7 +5,7 @@ description: Deploy a prebuilt static site to Vercel Deploys a release artifact to [Vercel](https://vercel.com/) as a production deployment. -The target extracts a ZIP artifact and shells out to the [`vercel`](https://vercel.com/docs/cli) CLI to promote it to production (`vercel deploy --prod`). The `vercel` CLI is bundled in the Craft Docker image. +The target extracts a ZIP artifact and deploys it via the Vercel deploy API (using [`@vercel/client`](https://www.npmjs.com/package/@vercel/client)) to promote it to production. It does not use or require the `vercel` CLI. This target is release-gated: it runs as part of `craft publish`, so a deployment only happens on release and the deployed site stays in sync with the published version — the same guarantee the [`gh-pages`](./gh-pages/) target provides, but for Vercel-hosted sites. @@ -13,20 +13,19 @@ This target is release-gated: it runs as part of `craft publish`, so a deploymen | Option | Description | |--------|-------------| -| `prebuilt` | Whether the artifact contains a prebuilt `.vercel/output` (the result of `vercel build`). When `true` (default), the CLI is invoked with `--prebuilt` and skips the remote build. Set to `false` to have Vercel build from source. | -| `vercelCliPath` | Path to the `vercel` binary. Default: `vercel` (or the `VERCEL_BIN` env var). | +| `prebuilt` | Whether the artifact contains a prebuilt `.vercel/output` (the result of `vercel build`). When `true` (default), the artifact's prebuilt `.vercel/output` is uploaded and the remote build step is skipped. Set to `false` to have Vercel build from source. | | `workingDir` | Subdirectory within the extracted artifact to deploy from. | ## Environment Variables | Name | Required | Description | |------|----------|-------------| -| `VERCEL_TOKEN` | Yes | Vercel access token. Passed to the CLI via the environment, never on the command line. | -| `VERCEL_ORG_ID` | No | Vercel organization/team ID. An identifier, not a secret. Forwarded to the CLI to link the deployment non-interactively; when unset, the CLI falls back to the `.vercel/project.json` inside the artifact. | -| `VERCEL_PROJECT_ID` | No | Vercel project ID. An identifier, not a secret. Same behavior as `VERCEL_ORG_ID`. | +| `VERCEL_TOKEN` | Yes | Vercel access token. Passed to the deploy API, never on the command line. | +| `VERCEL_ORG_ID` | No | Vercel organization/team ID. An identifier, not a secret. Forwarded to the deploy API as the team ID so the deployment links non-interactively; when unset, the deploy falls back to the `.vercel/project.json` inside the artifact. | +| `VERCEL_PROJECT_ID` | No | Vercel project ID. An identifier, not a secret. Forwarded to the deploy API to link the deployment to the project; when unset, the deploy falls back to the `.vercel/project.json` inside the artifact. | :::note -For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (or ship a `.vercel/project.json` in the artifact) so the CLI knows which project to deploy to. +For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (or ship a `.vercel/project.json` in the artifact) so the deploy API knows which project to deploy to. ::: ## Default Behavior @@ -35,9 +34,9 @@ By default, this target: 1. Looks for a single artifact matching `vercel.zip` (or `*-vercel.zip`). Override with `includeNames`. 2. Extracts its contents (preserving the archive layout, e.g. a top-level `.vercel/output`). -3. Deploys to production via `vercel deploy --prod --prebuilt`. +3. Deploys to production via the Vercel deploy API. -The version being released is attached to the deployment as `--meta craftRelease=` for traceability. +The version being released is attached to the deployment as metadata (`craftRelease=`) for traceability. ## Example diff --git a/package.json b/package.json index 8ad901944..34a48baa7 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "pnpm": "10.27.0" }, "dependencies": { + "@vercel/client": "^18.2.5", "fastest-levenshtein": "^1.0.16", "ignore": "^7.0.5", "marked": "^17.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 167cb907c..d01737fdf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: .: dependencies: + '@vercel/client': + specifier: ^18.2.5 + version: 18.2.5(vite@7.3.5(@types/node@24.13.2)(tsx@4.21.0)) fastest-levenshtein: specifier: ^1.0.16 version: 1.0.16 @@ -421,6 +424,9 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} + '@bytecodealliance/preview2-shim@0.17.6': + resolution: {integrity: sha512-n3cM88gTen5980UOBAD6xDcNNL3ocTK8keab21bpx1ONdA+ARj7uD1qoFxOWCyKlkpSi195FH+GeAut7Oc6zZw==} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -833,9 +839,24 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@next/env@15.1.6': + resolution: {integrity: sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==} + '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@octokit/auth-token@5.1.2': resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} engines: {node: '>= 18'} @@ -1093,6 +1114,10 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 + '@renovatebot/pep440@4.2.1': + resolution: {integrity: sha512-2FK1hF93Fuf1laSdfiEmJvSJPVIDHEUTz68D3Fi9s0IZrrpaEcj6pTFBTbYvsgC5du4ogrtf5re7yMMvrKNgkw==} + engines: {node: ^20.9.0 || ^22.11.0 || ^24, pnpm: ^10.0.0} + '@rollup/rollup-android-arm-eabi@4.61.1': resolution: {integrity: sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==} cpu: [arm] @@ -1676,6 +1701,49 @@ packages: resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vercel/build-utils@14.0.1': + resolution: {integrity: sha512-EGOVxPX5T6yYfITMZACezM0rDHByCXEJ7X42jGytl5bAYi/DSJR0S3PpRPOiqYOh46ml9LmRJyZeq5V13HeGDg==} + + '@vercel/client@18.2.5': + resolution: {integrity: sha512-T1Qgj0U9IfFgXMgR3K+bihYZAh9wDyFdgo8pyt00rCrrtBo820WPsrSYH0xz/qMwOUanOAOk3dhsEnG6spCIFg==} + engines: {node: '>= 20'} + + '@vercel/error-utils@2.2.1': + resolution: {integrity: sha512-9DhP8jP7raLML4hGsBemxX5fXuQnu5xxMV+HjGygGbzEmVK/+KyJ3QP2Cw7PdF0uXdb9N0Qa4c3tRGH34ZX6vw==} + + '@vercel/microfrontends@1.2.2': + resolution: {integrity: sha512-QzcR5wVsz654XlCGT/HnxjgSkQMpeaEuj7bI1EeukwL/2D3kFkL5J68LMqA68HHSbbLvx32o7n5scSoB7a+3IQ==} + hasBin: true + peerDependencies: + '@sveltejs/kit': '>=1' + '@vercel/analytics': '>=1.5.0' + '@vercel/speed-insights': '>=1.2.0' + next: '>=13' + react: '>=17.0.0' + react-dom: '>=17.0.0' + vite: ^7.3.5 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + '@vercel/analytics': + optional: true + '@vercel/speed-insights': + optional: true + next: + optional: true + react: + optional: true + react-dom: + optional: true + vite: + optional: true + + '@vercel/python-analysis@0.13.1': + resolution: {integrity: sha512-ec4tii9I7i6eHfvsvG/eaNaKh0TxmxBsc904V7yjwZImeyFTEVvDSLQ/+510FB+wBG6gCvpyqBH5aAKP9llqsw==} + + '@vercel/routing-utils@6.4.1': + resolution: {integrity: sha512-d/DqMAbxV8ymZdPzVbmihFj9oK3IvskucNbRN18ik53JLFtMlfu8v1SuVTt+iRhnYXaeA/y4NYLcyOnkkNmq/Q==} + '@vitest/expect@4.1.8': resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} @@ -1735,6 +1803,9 @@ packages: ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -1774,15 +1845,25 @@ packages: resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} engines: {node: '>=4'} + async-retry@1.2.3: + resolution: {integrity: sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==} + async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async-sema@3.0.0: + resolution: {integrity: sha512-zyCMBDl4m71feawrxYcVbHxv/UUkqm4nKJiLu3+l9lfiQha6jQ/9dxhrXLnzzBXVFqCTDwiUkZOz9XFbdEGQsg==} + async@3.2.2: resolution: {integrity: sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==} asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + aws4@1.13.2: resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} @@ -1807,6 +1888,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bl@1.2.3: + resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1829,9 +1913,18 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1842,6 +1935,14 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1861,6 +1962,9 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -1872,6 +1976,9 @@ packages: resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} engines: {node: '>=8'} + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + cjs-module-lexer@1.4.3: resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} @@ -1910,12 +2017,23 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + consola@2.15.3: resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@0.4.0: + resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} + engines: {node: '>= 0.6'} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1935,6 +2053,10 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -1979,6 +2101,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.5.0: + resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} + es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} @@ -2066,6 +2191,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} @@ -2079,12 +2207,19 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-xml-builder@1.2.0: resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} @@ -2096,6 +2231,9 @@ packages: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2124,6 +2262,19 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2139,6 +2290,17 @@ packages: forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + + fs-extra@8.0.1: + resolution: {integrity: sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==} + engines: {node: '>=6 <7 || >=8'} + fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -2222,6 +2384,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -2230,6 +2395,9 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2249,6 +2417,10 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + http-proxy@1.18.1: + resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} + engines: {node: '>=8.0.0'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} @@ -2260,6 +2432,10 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2286,6 +2462,10 @@ packages: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-ci@2.0.0: resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} hasBin: true @@ -2317,10 +2497,20 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2331,6 +2521,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true @@ -2349,6 +2543,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2360,6 +2557,15 @@ packages: engines: {node: '>=6'} hasBin: true + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} @@ -2414,6 +2620,14 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2435,6 +2649,9 @@ packages: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@4.2.8: resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} engines: {node: '>=8'} @@ -2451,6 +2668,10 @@ packages: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -2459,6 +2680,9 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2564,6 +2788,15 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + + path-to-regexp@6.2.1: + resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2589,6 +2822,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + postcss@8.5.24: resolution: {integrity: sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==} engines: {node: ^10 || ^12 || >=14} @@ -2618,6 +2855,9 @@ packages: engines: {node: '>=14'} hasBin: true + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -2636,10 +2876,24 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pump@1.0.3: + resolution: {integrity: sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + querystring@0.2.1: + resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -2648,10 +2902,17 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-in-the-middle@8.0.1: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2667,15 +2928,29 @@ packages: resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} engines: {node: '>=14'} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.61.1: resolution: {integrity: sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} @@ -2688,6 +2963,10 @@ packages: engines: {node: '>=10'} hasBin: true + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2716,6 +2995,13 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sleep-promise@8.0.1: + resolution: {integrity: sha512-nfwyX+G1dsx2R1DMMKWLpNxuHMOCL7JIRBUw0fl7Z4nZ1YZK0apZuGY8MDexn0HDZzgbERgj/CrNtsYpo/B7eA==} + + smol-toml@1.5.2: + resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2758,6 +3044,9 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -2787,6 +3076,13 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tar-fs@1.16.3: + resolution: {integrity: sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==} + + tar-stream@1.6.2: + resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} + engines: {node: '>= 0.8.0'} + tar@7.5.19: resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} engines: {node: '>=18'} @@ -2821,6 +3117,10 @@ packages: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2846,6 +3146,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typescript-eslint@8.50.1: resolution: {integrity: sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2864,6 +3168,14 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + unplugin@1.0.1: resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} @@ -2980,6 +3292,10 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-typed-array@1.1.22: + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3044,6 +3360,9 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -3545,6 +3864,8 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@bytecodealliance/preview2-shim@0.17.6': {} + '@esbuild/aix-ppc64@0.27.7': optional: true @@ -3828,8 +4149,22 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@next/env@15.1.6': {} + '@nodable/entities@2.1.0': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@octokit/auth-token@5.1.2': {} '@octokit/core@6.1.6': @@ -4147,6 +4482,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@renovatebot/pep440@4.2.1': {} + '@rollup/rollup-android-arm-eabi@4.61.1': optional: true @@ -4878,6 +5215,72 @@ snapshots: '@typescript-eslint/types': 8.50.1 eslint-visitor-keys: 4.2.1 + '@vercel/build-utils@14.0.1': + dependencies: + '@vercel/python-analysis': 0.13.1 + cjs-module-lexer: 1.2.3 + es-module-lexer: 1.5.0 + + '@vercel/client@18.2.5(vite@7.3.5(@types/node@24.13.2)(tsx@4.21.0))': + dependencies: + '@vercel/build-utils': 14.0.1 + '@vercel/error-utils': 2.2.1 + '@vercel/microfrontends': 1.2.2(vite@7.3.5(@types/node@24.13.2)(tsx@4.21.0)) + '@vercel/routing-utils': 6.4.1 + async-retry: 1.2.3 + async-sema: 3.0.0 + fs-extra: 8.0.1 + ignore: 4.0.6 + minimatch: 10.2.6 + ms: 2.1.2 + querystring: 0.2.1 + sleep-promise: 8.0.1 + tar-fs: 1.16.3 + transitivePeerDependencies: + - '@sveltejs/kit' + - '@vercel/analytics' + - '@vercel/speed-insights' + - debug + - next + - react + - react-dom + - vite + + '@vercel/error-utils@2.2.1': {} + + '@vercel/microfrontends@1.2.2(vite@7.3.5(@types/node@24.13.2)(tsx@4.21.0))': + dependencies: + '@next/env': 15.1.6 + ajv: 8.20.0 + commander: 12.1.0 + cookie: 0.4.0 + fast-glob: 3.3.3 + http-proxy: 1.18.1 + jsonc-parser: 3.3.1 + nanoid: 3.3.16 + path-to-regexp: 6.2.1 + optionalDependencies: + vite: 7.3.5(@types/node@24.13.2)(tsx@4.21.0) + transitivePeerDependencies: + - debug + + '@vercel/python-analysis@0.13.1': + dependencies: + '@bytecodealliance/preview2-shim': 0.17.6 + '@renovatebot/pep440': 4.2.1 + fs-extra: 11.1.1 + js-yaml: 4.1.1 + minimatch: 10.2.6 + smol-toml: 1.5.2 + zod: 3.22.4 + + '@vercel/routing-utils@6.4.1': + dependencies: + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + optionalDependencies: + ajv: 6.14.0 + '@vitest/expect@4.1.8': dependencies: '@standard-schema/spec': 1.1.0 @@ -4948,6 +5351,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} @@ -4973,14 +5383,24 @@ snapshots: astral-regex@1.0.0: {} + async-retry@1.2.3: + dependencies: + retry: 0.12.0 + async-retry@1.3.3: dependencies: retry: 0.13.1 + async-sema@3.0.0: {} + async@3.2.2: {} asynckit@0.4.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + aws4@1.13.2: {} balanced-match@4.0.4: {} @@ -4995,6 +5415,11 @@ snapshots: binary-extensions@2.3.0: {} + bl@1.2.3: + dependencies: + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -5021,8 +5446,17 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-alloc-unsafe@1.1.0: {} + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + buffer-equal-constant-time@1.0.1: {} + buffer-fill@1.0.0: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5035,6 +5469,18 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} caniuse-lite@1.0.30001763: {} @@ -5058,12 +5504,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chownr@1.1.4: {} + chownr@3.0.0: {} ci-info@2.0.0: {} ci-info@4.3.1: {} + cjs-module-lexer@1.2.3: {} + cjs-module-lexer@1.4.3: {} cli-cursor@3.1.0: @@ -5096,10 +5546,16 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@12.1.0: {} + consola@2.15.3: {} convert-source-map@2.0.0: {} + cookie@0.4.0: {} + + core-util-is@1.0.3: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5116,6 +5572,12 @@ snapshots: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + delayed-stream@1.0.0: {} dotenv@16.6.1: {} @@ -5155,6 +5617,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.5.0: {} + es-module-lexer@2.1.0: {} es-object-atoms@1.1.1: @@ -5308,6 +5772,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + expect-type@1.3.0: {} extend@3.0.2: {} @@ -5316,10 +5782,20 @@ snapshots: fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-uri@3.1.5: {} + fast-xml-builder@1.2.0: dependencies: path-expression-matcher: 1.5.0 @@ -5335,6 +5811,10 @@ snapshots: fastest-levenshtein@1.0.16: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -5359,6 +5839,12 @@ snapshots: flatted@3.4.2: {} + follow-redirects@1.16.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -5383,6 +5869,20 @@ snapshots: forwarded-parse@2.1.2: {} + fs-constants@1.0.0: {} + + fs-extra@11.1.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@8.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -5490,6 +5990,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -5500,6 +6002,10 @@ snapshots: has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -5520,6 +6026,14 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy@1.18.1: + dependencies: + eventemitter3: 4.0.7 + follow-redirects: 1.16.0 + requires-port: 1.0.0 + transitivePeerDependencies: + - debug + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -5536,6 +6050,8 @@ snapshots: ieee754@1.2.1: {} + ignore@4.0.6: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -5560,6 +6076,8 @@ snapshots: dependencies: binary-extensions: 2.3.0 + is-callable@1.2.7: {} + is-ci@2.0.0: dependencies: ci-info: 2.0.0 @@ -5582,8 +6100,16 @@ snapshots: is-stream@2.0.1: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.22 + is-unicode-supported@0.1.0: {} + isarray@1.0.0: {} + + isarray@2.0.5: {} + isexe@2.0.0: {} jackspeak@4.1.1: @@ -5592,6 +6118,10 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + js-yaml@4.3.0: dependencies: argparse: 2.0.1 @@ -5606,12 +6136,26 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stringify-safe@5.0.1: {} json5@2.2.3: {} + jsonc-parser@3.3.1: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -5665,6 +6209,13 @@ snapshots: math-intrinsics@1.1.0: {} + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + mime-db@1.52.0: {} mime-types@2.1.35: @@ -5679,6 +6230,8 @@ snapshots: dependencies: brace-expansion: 5.0.8 + minimist@1.2.8: {} + minipass@4.2.8: {} minipass@7.1.2: {} @@ -5689,10 +6242,16 @@ snapshots: dependencies: minipass: 7.1.3 + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + mkdirp@1.0.4: {} module-details-from-path@1.0.4: {} + ms@2.1.2: {} + ms@2.1.3: {} mustache@3.0.1: {} @@ -5793,6 +6352,12 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 + path-to-regexp@6.1.0: {} + + path-to-regexp@6.2.1: {} + + path-to-regexp@6.3.0: {} + pathe@2.0.3: {} pg-int8@1.0.1: {} @@ -5813,6 +6378,8 @@ snapshots: picomatch@4.0.4: {} + possible-typed-array-names@1.1.0: {} + postcss@8.5.24: dependencies: nanoid: 3.3.16 @@ -5833,6 +6400,8 @@ snapshots: prettier@3.7.4: {} + process-nextick-args@2.0.1: {} + progress@2.0.3: {} prompts@2.4.1: @@ -5846,8 +6415,27 @@ snapshots: proxy-from-env@1.1.0: {} + pump@1.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} + querystring@0.2.1: {} + + queue-microtask@1.2.3: {} + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -5858,6 +6446,8 @@ snapshots: dependencies: picomatch: 2.3.2 + require-from-string@2.0.2: {} + require-in-the-middle@8.0.1: dependencies: debug: 4.4.3 @@ -5865,6 +6455,8 @@ snapshots: transitivePeerDependencies: - supports-color + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: @@ -5884,8 +6476,12 @@ snapshots: - encoding - supports-color + retry@0.12.0: {} + retry@0.13.1: {} + reusify@1.1.0: {} + rollup@4.61.1: dependencies: '@types/estree': 1.0.9 @@ -5917,12 +6513,27 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.1 fsevents: 2.3.3 + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} semver@6.3.1: {} semver@7.7.3: {} + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5949,6 +6560,10 @@ snapshots: sisteransi@1.0.5: {} + sleep-promise@8.0.1: {} + + smol-toml@1.5.2: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5995,6 +6610,10 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -6021,6 +6640,23 @@ snapshots: dependencies: has-flag: 4.0.0 + tar-fs@1.16.3: + dependencies: + chownr: 1.1.4 + mkdirp: 0.5.6 + pump: 1.0.3 + tar-stream: 1.6.2 + + tar-stream@1.6.2: + dependencies: + bl: 1.2.3 + buffer-alloc: 1.2.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + readable-stream: 2.3.8 + to-buffer: 1.2.2 + xtend: 4.0.2 + tar@7.5.19: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -6060,6 +6696,12 @@ snapshots: tmp@0.2.7: {} + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6084,6 +6726,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typescript-eslint@8.50.1(eslint@9.39.2)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) @@ -6101,6 +6749,10 @@ snapshots: universal-user-agent@7.0.3: {} + universalify@0.1.2: {} + + universalify@2.0.1: {} + unplugin@1.0.1: dependencies: acorn: 8.15.0 @@ -6178,6 +6830,16 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-typed-array@1.1.22: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -6234,4 +6896,6 @@ snapshots: yocto-queue@1.2.2: {} + zod@3.22.4: {} + zod@3.25.76: {} diff --git a/src/targets/__tests__/vercel.test.ts b/src/targets/__tests__/vercel.test.ts index 58b92146a..c1a2c6f27 100644 --- a/src/targets/__tests__/vercel.test.ts +++ b/src/targets/__tests__/vercel.test.ts @@ -1,5 +1,7 @@ import { vi } from 'vitest'; +import { createDeployment } from '@vercel/client'; + import { VercelTarget, targetSecrets } from '../vercel'; import { NoneArtifactProvider } from '../../artifact_providers/none'; import * as system from '../../utils/system'; @@ -12,13 +14,15 @@ const PROJECT_ID = 'prj_1234'; vi.mock('../../utils/helpers'); +vi.mock('@vercel/client', () => ({ + createDeployment: vi.fn(), +})); + vi.mock('../../utils/system', async importOriginal => { const actual = await importOriginal(); return { ...actual, - checkExecutableIsPresent: vi.fn(), - spawnProcess: vi.fn(async () => undefined), - extractZipArchive: vi.fn(async () => undefined), + extractZipArchiveWithFlattening: vi.fn(async () => undefined), }; }); @@ -30,6 +34,18 @@ vi.mock('../../utils/files', async importOriginal => { }; }); +function mockDeployment(url = 'my-app.vercel.app'): void { + (createDeployment as any).mockImplementation(async function* () { + yield { type: 'ready', payload: { url } }; + }); +} + +function mockDeploymentError(error: Error): void { + (createDeployment as any).mockImplementation(async function* () { + yield { type: 'error', payload: error }; + }); +} + function setTargetSecretsInEnv(): void { for (const secret of targetSecrets) { process.env[secret] = DEFAULT_SECRET_VALUE; @@ -57,7 +73,6 @@ function createVercelTarget( beforeEach(() => { setTargetSecretsInEnv(); - delete process.env.VERCEL_BIN; delete process.env.VERCEL_ORG_ID; delete process.env.VERCEL_PROJECT_ID; (isDryRun as any).mockReturnValue(false); @@ -95,7 +110,6 @@ describe('vercel target configuration', () => { expect(target.vercelConfig).toStrictEqual({ VERCEL_TOKEN: DEFAULT_SECRET_VALUE, prebuilt: true, - vercelCliPath: 'vercel', workingDir: undefined, orgId: undefined, projectId: undefined, @@ -113,36 +127,17 @@ describe('vercel target configuration', () => { test('allows overriding default options', () => { const target = createVercelTarget({ prebuilt: false, - vercelCliPath: '/custom/vercel', workingDir: 'subdir', }); expect(target.vercelConfig).toStrictEqual({ VERCEL_TOKEN: DEFAULT_SECRET_VALUE, prebuilt: false, - vercelCliPath: '/custom/vercel', workingDir: 'subdir', orgId: undefined, projectId: undefined, }); }); - - test('resolves vercel path from VERCEL_BIN env', () => { - process.env.VERCEL_BIN = '/env/vercel'; - const target = createVercelTarget({}); - expect(target.vercelConfig.vercelCliPath).toBe('/env/vercel'); - }); - - test('checks vercel is present in the constructor', () => { - createVercelTarget({}); - expect(system.checkExecutableIsPresent).toHaveBeenCalledWith('vercel'); - }); - - test('rejects config values that look like env-var expansions', () => { - expect(() => createVercelTarget({ workingDir: '${VERCEL_TOKEN}' })).toThrow( - /workingDir.*must not be an environment-variable expansion/, - ); - }); }); describe('publish', () => { @@ -158,55 +153,54 @@ describe('publish', () => { } test('deploys a prebuilt artifact to production with provenance', async () => { + mockDeployment(); const target = createVercelTarget({}); stubArtifacts(target, [artifact]); await target.publish(version, revision); - expect(system.extractZipArchive).toHaveBeenCalledWith( + expect(system.extractZipArchiveWithFlattening).toHaveBeenCalledWith( '/downloads/vercel.zip', TMP_DIR, ); - expect(system.spawnProcess).toHaveBeenCalledTimes(1); - const [bin, args, options] = (system.spawnProcess as any).mock.calls[0]; - expect(bin).toBe('vercel'); - expect(args).toEqual([ - 'deploy', - '--prod', - '--yes', - '--prebuilt', - '--meta', - `craftRelease=${version}`, - ]); - expect(options.cwd).toBe(TMP_DIR); - // Secret must be in env, not argv - expect(options.env.VERCEL_TOKEN).toBe(DEFAULT_SECRET_VALUE); - expect(args).not.toContain(DEFAULT_SECRET_VALUE); + expect(createDeployment).toHaveBeenCalledTimes(1); + const [clientOptions, deploymentOptions] = (createDeployment as any).mock + .calls[0]; + expect(clientOptions.token).toBe(DEFAULT_SECRET_VALUE); + expect(clientOptions.path).toBe(TMP_DIR); + expect(clientOptions.prebuilt).toBe(true); + expect(clientOptions.skipAutoDetectionConfirmation).toBe(true); + expect(deploymentOptions.target).toBe('production'); + expect(deploymentOptions.meta.craftRelease).toBe(version); }); - test('omits --prebuilt when prebuilt is false', async () => { + test('omits prebuilt when prebuilt is false', async () => { + mockDeployment(); const target = createVercelTarget({ prebuilt: false }); stubArtifacts(target, [artifact]); await target.publish(version, revision); - const [, args] = (system.spawnProcess as any).mock.calls[0]; - expect(args).not.toContain('--prebuilt'); + const [clientOptions] = (createDeployment as any).mock.calls[0]; + expect(clientOptions.prebuilt).toBe(false); }); test('does not forward org/project IDs when unset', async () => { + mockDeployment(); const target = createVercelTarget({}); stubArtifacts(target, [artifact]); await target.publish(version, revision); - const [, , options] = (system.spawnProcess as any).mock.calls[0]; - expect('VERCEL_ORG_ID' in options.env).toBe(false); - expect('VERCEL_PROJECT_ID' in options.env).toBe(false); + const [clientOptions, deploymentOptions] = (createDeployment as any).mock + .calls[0]; + expect(clientOptions.teamId).toBeUndefined(); + expect('name' in deploymentOptions).toBe(false); }); test('forwards org/project IDs when set', async () => { + mockDeployment(); process.env.VERCEL_ORG_ID = ORG_ID; process.env.VERCEL_PROJECT_ID = PROJECT_ID; const target = createVercelTarget({}); @@ -214,39 +208,43 @@ describe('publish', () => { await target.publish(version, revision); - const [, , options] = (system.spawnProcess as any).mock.calls[0]; - expect(options.env.VERCEL_ORG_ID).toBe(ORG_ID); - expect(options.env.VERCEL_PROJECT_ID).toBe(PROJECT_ID); + const [clientOptions, deploymentOptions] = (createDeployment as any).mock + .calls[0]; + expect(clientOptions.teamId).toBe(ORG_ID); + expect(deploymentOptions.name).toBe(PROJECT_ID); }); test('deploys from workingDir subdirectory when configured', async () => { + mockDeployment(); const target = createVercelTarget({ workingDir: 'dist' }); stubArtifacts(target, [artifact]); await target.publish(version, revision); - const [, , options] = (system.spawnProcess as any).mock.calls[0]; - expect(options.cwd).toBe(`${TMP_DIR}/dist`); + const [clientOptions] = (createDeployment as any).mock.calls[0]; + expect(clientOptions.path).toBe(`${TMP_DIR}/dist`); }); test('reports an error and does not deploy when no artifacts found', async () => { + mockDeployment(); const target = createVercelTarget({}); stubArtifacts(target, []); await expect(target.publish(version, revision)).rejects.toThrow( /no artifacts found/, ); - expect(system.spawnProcess).not.toHaveBeenCalled(); + expect(createDeployment).not.toHaveBeenCalled(); }); test('reports an error when more than one artifact found', async () => { + mockDeployment(); const target = createVercelTarget({}); stubArtifacts(target, [artifact, artifact]); await expect(target.publish(version, revision)).rejects.toThrow( /more than one Vercel archive/, ); - expect(system.spawnProcess).not.toHaveBeenCalled(); + expect(createDeployment).not.toHaveBeenCalled(); }); test('does not deploy in dry-run mode (including worktree mode)', async () => { @@ -258,7 +256,15 @@ describe('publish', () => { // Artifact is still extracted (local, safe), but the remote deploy is // skipped. - expect(system.extractZipArchive).toHaveBeenCalled(); - expect(system.spawnProcess).not.toHaveBeenCalled(); + expect(system.extractZipArchiveWithFlattening).toHaveBeenCalled(); + expect(createDeployment).not.toHaveBeenCalled(); + }); + + test('throws when the deploy stream yields an error event', async () => { + mockDeploymentError(new Error('boom')); + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await expect(target.publish(version, revision)).rejects.toThrow(/boom/); }); }); diff --git a/src/targets/vercel.ts b/src/targets/vercel.ts index 67ff358d5..6003bd769 100644 --- a/src/targets/vercel.ts +++ b/src/targets/vercel.ts @@ -1,31 +1,28 @@ import { join } from 'path'; +import { createDeployment } from '@vercel/client'; + import { GitHubGlobalConfig, TargetConfig, TypedTargetConfig, } from '../schemas/project_config'; import { checkEnvForPrerequisite } from '../utils/env'; -import { ConfigurationError, reportError } from '../utils/errors'; +import { reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; import { isDryRun } from '../utils/helpers'; import { logDryRun } from '../utils/dryRun'; -import { - checkExecutableIsPresent, - extractZipArchive, - resolveExecutable, - spawnProcess, -} from '../utils/system'; +import { extractZipArchiveWithFlattening } from '../utils/system'; import { BaseTarget } from './base'; import { BaseArtifactProvider } from '../artifact_providers/base'; /** - * Secrets required to authenticate with Vercel. + * Secrets required to authenticate with the Vercel API. * - * Only the token is a true secret. The org and project IDs are identifiers, not - * credentials, and are handled separately (see the `*_ID_ENV_VAR` constants - * below): they are optional and, when set, forwarded to the Vercel CLI through - * the environment. + * Only the token is a true secret. The org/team and project IDs are + * identifiers, not credentials, and are handled separately (see the + * `*_ID_ENV_VAR` constants below): they are optional and, when set, forwarded + * to the deploy call so it links to the right project non-interactively. * * Exported so tests (and documentation tooling) can reference the canonical * list of environment variables this target consumes. @@ -34,30 +31,14 @@ export const targetSecrets = ['VERCEL_TOKEN'] as const; type SecretsType = (typeof targetSecrets)[number]; /** - * Optional, non-secret identifiers forwarded to the Vercel CLI when present. - * They link the deployment to a specific org/project non-interactively, which - * is what CI needs (there is no interactive `vercel link` step). When unset, - * the CLI falls back to the `.vercel/project.json` inside the artifact. + * Optional, non-secret identifiers forwarded to the Vercel API when present. + * They link the deployment to a specific team/project non-interactively, which + * is what CI needs. When unset, the deploy falls back to the + * `.vercel/project.json` inside the artifact. */ const ORG_ID_ENV_VAR = 'VERCEL_ORG_ID'; const PROJECT_ID_ENV_VAR = 'VERCEL_PROJECT_ID'; -/** Vercel executable configuration */ -const VERCEL_CONFIG = { - name: 'vercel', - envVar: 'VERCEL_BIN', - errorHint: - 'Install the Vercel CLI (npm install -g vercel) or set VERCEL_BIN to its path', -} as const; - -/** - * Matches a string that is exactly an environment-variable expansion, e.g. - * `${VERCEL_TOKEN}`. `spawnProcess` expands args of this exact form against the - * environment (which includes the token), so any value flowing into the CLI - * argv must be rejected if it matches. - */ -const ENV_EXPANSION_REGEX = /^\$\{.*\}$/; - /** * Regex for the Vercel deploy archive. * @@ -69,7 +50,6 @@ const DEFAULT_DEPLOY_ARCHIVE_REGEX = /^(?:.+-)?vercel\.zip$/; /** Fields on the vercel target config accessed at runtime */ interface VercelConfigFields extends Record { prebuilt?: boolean; - vercelCliPath?: string; workingDir?: string; } @@ -77,23 +57,21 @@ interface VercelConfigFields extends Record { export interface VercelTargetConfig { /** * Whether the artifact contains a prebuilt `.vercel/output` (the result of - * `vercel build`). When true, the CLI is invoked with `--prebuilt` and skips + * `vercel build`). When true, the deploy is created with `prebuilt` and skips * the remote build step. Defaults to true: the docs website is built in CI * and the release just promotes the prebuilt output to production. */ prebuilt: boolean; - /** Resolved path/name of the vercel binary */ - vercelCliPath: string; /** Subdirectory within the extracted artifact to deploy from */ workingDir?: string; /** - * Optional Vercel org ID (an identifier, not a secret). Forwarded to the CLI - * through the environment when set. + * Optional Vercel org/team ID (an identifier, not a secret). Forwarded to the + * deploy as `teamId` when set. */ orgId?: string; /** * Optional Vercel project ID (an identifier, not a secret). Forwarded to the - * CLI through the environment when set. + * deploy as the project name when set. */ projectId?: string; } @@ -107,8 +85,8 @@ export type VercelTargetFullConfig = VercelTargetConfig & /** * Target responsible for deploying a prebuilt static site to Vercel. * - * Shells out to the `vercel` CLI to promote a release artifact to production - * (`vercel deploy --prod`). Intended for release-gated documentation sites: the + * Uses the Vercel deploy API (via `@vercel/client`) to promote a release + * artifact to production. Intended for release-gated documentation sites: the * artifact is built in CI and this target only publishes it, keeping the docs * in sync with the released version. */ @@ -128,7 +106,6 @@ export class VercelTarget extends BaseTarget { super(config, artifactProvider, githubRepo); this.githubRepo = githubRepo; this.vercelConfig = this.getVercelConfig(); - checkExecutableIsPresent(this.vercelConfig.vercelCliPath); } /** @@ -139,29 +116,10 @@ export class VercelTarget extends BaseTarget { public getVercelConfig(): VercelTargetFullConfig { const config = this.config as TypedTargetConfig; - // These config values are passed to the CLI as command-line arguments. - // spawnProcess() expands args of the exact form "${VAR}" using the - // environment -- which includes VERCEL_TOKEN. Reject such values so a - // config string can never be expanded into a secret. - if ( - typeof config.workingDir === 'string' && - ENV_EXPANSION_REGEX.test(config.workingDir) - ) { - throw new ConfigurationError( - `[vercel] "workingDir" must not be an environment-variable ` + - `expansion (got "${config.workingDir}")`, - ); - } - - const vercelCliPath = config.vercelCliPath - ? config.vercelCliPath - : resolveExecutable(VERCEL_CONFIG); - return { prebuilt: config.prebuilt ?? true, - vercelCliPath, workingDir: config.workingDir, - // Optional, non-secret identifiers. Forwarded to the CLI when present. + // Optional, non-secret identifiers. Forwarded to the deploy when present. orgId: process.env[ORG_ID_ENV_VAR] || undefined, projectId: process.env[PROJECT_ID_ENV_VAR] || undefined, ...this.getTargetSecrets(), @@ -187,23 +145,49 @@ export class VercelTarget extends BaseTarget { } /** - * Builds the vercel CLI argument list for a production deploy. + * Runs a Vercel production deploy of `deployDir` and waits for it to finish. + * + * Drives the `@vercel/client` event stream to completion: resolves with the + * live deployment URL on `ready`, and throws on the `error` event so a failed + * deploy fails the release. * - * @param version The version being released + * @param deployDir Directory to deploy from + * @param version The version being released (attached as deploy provenance) + * @returns the production deployment URL */ - private getVercelArgs(version: string): string[] { - // `--prod` promotes to production; `--yes` skips interactive prompts (CI). - const args = ['deploy', '--prod', '--yes']; - if (this.vercelConfig.prebuilt) { - args.push('--prebuilt'); + private async deploy(deployDir: string, version: string): Promise { + for await (const event of createDeployment( + { + token: this.vercelConfig.VERCEL_TOKEN, + path: deployDir, + prebuilt: this.vercelConfig.prebuilt, + teamId: this.vercelConfig.orgId, + skipAutoDetectionConfirmation: true, + }, + { + // `production` deploys to production; `craftRelease` ties the + // deployment back to the released version for traceability. + target: 'production', + meta: { craftRelease: version }, + // Link to the configured project non-interactively when set; otherwise + // the deploy falls back to the artifact's `.vercel/project.json`. + ...(this.vercelConfig.projectId + ? { name: this.vercelConfig.projectId } + : {}), + }, + )) { + if (event.type === 'ready') { + return event.payload.url as string; + } + if (event.type === 'error') { + throw event.payload; + } } - // Attach release provenance so the deployment is traceable to the version. - args.push('--meta', `craftRelease=${version}`); - return args; + throw new Error('Vercel deploy finished without a ready deployment'); } /** - * Deploys the release artifact to Vercel via the `vercel` CLI. + * Deploys the release artifact to Vercel via the Vercel deploy API. * * @param version New version to be released * @param revision Git commit SHA to be published @@ -232,44 +216,24 @@ export class VercelTarget extends BaseTarget { await withTempDir( async directory => { this.logger.info(`Extracting "${archivePath}" to "${directory}"...`); - await extractZipArchive(archivePath, directory); + await extractZipArchiveWithFlattening(archivePath, directory); const deployDir = this.vercelConfig.workingDir ? join(directory, this.vercelConfig.workingDir) : directory; - const args = this.getVercelArgs(version); - // A Vercel deploy is a remote, irreversible operation with no local // isolation. Unlike git/fs operations, it must NEVER run in dry-run - // mode -- including worktree mode, where spawnProcess would otherwise - // execute the command for real. Guard explicitly here. + // mode -- including worktree mode. Guard explicitly here, before any + // network calls. if (isDryRun()) { - logDryRun(`${this.vercelConfig.vercelCliPath} ${args.join(' ')}`); + logDryRun(`vercel deploy --prod (${deployDir})`); return; } - const env: NodeJS.ProcessEnv = { - ...process.env, - VERCEL_TOKEN: this.vercelConfig.VERCEL_TOKEN, - }; - // Org/project IDs are optional identifiers that link the deploy - // non-interactively: forward them only when set, otherwise let the CLI - // fall back to the artifact's `.vercel/project.json`. - if (this.vercelConfig.orgId) { - env.VERCEL_ORG_ID = this.vercelConfig.orgId; - } - if (this.vercelConfig.projectId) { - env.VERCEL_PROJECT_ID = this.vercelConfig.projectId; - } - this.logger.info('Deploying to Vercel...'); - await spawnProcess( - this.vercelConfig.vercelCliPath, - args, - { cwd: deployDir, env }, - { showStdout: true }, - ); + const url = await this.deploy(deployDir, version); + this.logger.info(`Vercel deploy live at https://${url}`); }, true, 'craft-vercel-', From 65a54b0ec274c049dddd4ddf92c4e72409e5b4b6 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Thu, 6 Aug 2026 16:21:14 +0000 Subject: [PATCH 4/9] fix(build): bundle jsonc-parser via its ESM entry The new @vercel/client dependency pulls in jsonc-parser (via @vercel/microfrontends), whose UMD main does runtime `require('./impl/format')`. esbuild can't follow those dynamic sibling requires, so the bundled `dist/craft` crashed at startup with "Cannot find module './impl/format'", failing the CLI smoke and prepare-dry-run e2e tests. Redirect the package to its ESM entry, whose static imports bundle cleanly. #skip-changelog --- build.mjs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/build.mjs b/build.mjs index 77773d007..c0ea59635 100644 --- a/build.mjs +++ b/build.mjs @@ -2,7 +2,37 @@ import { chmod, readFile, rename, stat, unlink, writeFile } from 'fs/promises'; import esbuild from 'esbuild'; import { sentryEsbuildPlugin } from '@sentry/esbuild-plugin'; -const plugins = []; +// jsonc-parser ships a UMD entry (its `main`) whose body does runtime +// `require("./impl/format")` etc. esbuild can't follow those dynamic sibling +// requires when bundling, so the built `dist/craft` fails at startup with +// "Cannot find module './impl/format'". Redirect the package to its ESM entry +// (`module`), which uses static imports esbuild can bundle. Pulled in +// transitively via @vercel/client → @vercel/microfrontends. +const jsoncParserEsmPlugin = { + name: 'jsonc-parser-esm', + setup(build) { + build.onResolve({ filter: /^jsonc-parser$/ }, async args => { + // Avoid recursing into our own resolve call below. + if (args.pluginData?.resolved) { + return; + } + const result = await build.resolve('jsonc-parser', { + importer: args.importer, + kind: args.kind, + resolveDir: args.resolveDir, + pluginData: { resolved: true }, + }); + if (result.errors.length > 0) { + return result; + } + return { + path: result.path.replace(/([\\/])lib[\\/]umd[\\/]/, '$1lib/esm/'), + }; + }); + }, +}; + +const plugins = [jsoncParserEsmPlugin]; // Only add Sentry plugin if auth token is available (production builds on master) if (process.env.SENTRY_AUTH_TOKEN) { From 77b5ddb71be6f6dc6641a25af0273aeee6cf5a16 Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 7 Aug 2026 12:49:46 +0000 Subject: [PATCH 5/9] fix(vercel): preserve .vercel layout, drop projectId name hack, align meta key, consume alias-assigned - switch extraction back to extractZipArchive (fixes prebuilt .vercel/output layout) - remove name: projectId (SDK reads .vercel/project.json from the artifact) - change meta key from craftRelease to release (BYK) - keep consuming the stream until alias-assigned (or return ready URL) - update dry-run log and tests to match - docs wording nits from review Addresses review threads on flattening, project linking, ready-vs-alias, and wording. --- AGENTS.md | 132 ++++-------------------- docs/src/content/docs/targets/vercel.md | 4 +- src/targets/__tests__/vercel.test.ts | 15 ++- src/targets/vercel.ts | 28 ++--- 4 files changed, 45 insertions(+), 134 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bb307b449..0141e32f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,119 +1,29 @@ -# AGENTS.md +# Jared (Outpost agent) -This file provides guidance for AI coding assistants working with the Craft codebase. +Autonomous GitHub coding agent. Work in `/workspace/repo`. -## Package Management +## Model tiers -- **Always use `pnpm`** for package management. Never use `npm` or `yarn`. -- Node.js version is managed by [Volta](https://volta.sh/) (currently v22.12.0). -- Install dependencies with `pnpm install --frozen-lockfile`. +The primary model is chosen per event (see `src/agents/models.ts`): heavy for +code-producing situations, cheaper for lightweight ones. -## Development Commands +| Role | Subagent | Model | +| --- | --- | --- | +| Triage / plan / review (heavy) | (primary Jared) | Claude Opus 4.8 | +| Triage / plan / review (light) | (primary Jared) | xAI Grok 4.3 | +| Explore | `explore` | OpenAI gpt-5-mini | +| Implement | `implement` | Moonshot kimi-k2.7-code | +| Ship (commit/push/PR) | `ship` | xAI Grok (`grok-build-0.1`) | -| Command | Description | -| ------------ | ------------------------------------------- | -| `pnpm build` | Build the project (outputs to `dist/craft`) | -| `pnpm test` | Run tests | -| `pnpm lint` | Run ESLint | -| `pnpm fix` | Auto-fix lint issues | +Pipeline: triage → explore → plan → implement → review → ship. +(`worker` is a deprecated alias of `implement`.) -To manually test changes: +Operators also talk to Jared directly from the Outpost dashboard. Those turns +(`New operator chat` / `Operator guidance:`) skip triage — treat the request as +the task and answer in the conversation. -```bash -pnpm build && ./dist/craft -``` +Long-term project knowledge for *this* Outpost repo lives in `.lore.md` when present. +For target repositories, read their `AGENTS.md` / `CONTRIBUTING.md` first. -## Code Style - -- **TypeScript** is used throughout the codebase. -- **Prettier** 3.x with single quotes and no arrow parens (configured in `.prettierrc.yml`). -- **ESLint** 9.x with flat config (`eslint.config.mjs`) using `typescript-eslint`. -- Unused variables prefixed with `_` are allowed (e.g., `_unusedParam`). - -## Project Structure - -``` -src/ -├── __mocks__/ # Test mocks -├── __tests__/ # Test files (*.test.ts) -├── artifact_providers/ # Artifact provider implementations -├── commands/ # CLI command implementations -├── schemas/ # Zod schemas and TypeScript types for config -├── status_providers/ # Status provider implementations -├── targets/ # Release target implementations -├── types/ # Shared TypeScript types -├── utils/ # Utility functions -├── config.ts # Configuration loading -├── index.ts # CLI entry point -└── logger.ts # Logging utilities -dist/ -└── craft # Single bundled executable (esbuild output) -``` - -## Testing - -- Tests use **Vitest**. -- Test files are located in `src/__tests__/` and follow the `*.test.ts` naming pattern. -- Run tests with `pnpm test`. -- Use `vi.fn()`, `vi.mock()`, `vi.spyOn()` for mocking (Vitest's mock API). - -## CI/CD - -- Main branch is `master`. -- CI runs tests on Node.js 20 and 22. -- Craft releases itself using its own tooling (dogfooding). - -## Configuration - -- Project configuration lives in `.craft.yml` at the repository root. -- The configuration schema is defined in `src/schemas/`. - -## Dry-Run Mode - -Craft supports a `--dry-run` flag that prevents destructive operations. This is implemented via a centralized abstraction layer. - -### How It Works - -Instead of checking `isDryRun()` manually in every function, destructive operations are wrapped with dry-run-aware proxies: - -- **Git operations**: Use `getGitClient()` from `src/utils/git.ts` or `createGitClient(directory)` for working with specific directories -- **GitHub API**: Use `getGitHubClient()` from `src/utils/githubApi.ts` -- **File writes**: Use `safeFs` from `src/utils/dryRun.ts` -- **Other actions**: Use `safeExec()` or `safeExecSync()` from `src/utils/dryRun.ts` - -### ESLint Enforcement - -ESLint rules prevent direct usage of raw APIs: - -- `no-restricted-imports`: Blocks direct `simple-git` imports -- `no-restricted-syntax`: Blocks `new Octokit()` instantiation - -If you're writing a wrapper module that needs raw access, use: - -```typescript -// eslint-disable-next-line no-restricted-imports -- This is the wrapper module -import simpleGit from 'simple-git'; -``` - -### Adding New Destructive Operations - -When adding new code that performs destructive operations: - -1. **Git**: Get the git client via `getGitClient()` or `createGitClient()` - mutating methods are automatically blocked -2. **GitHub API**: Get the client via `getGitHubClient()` - `create*`, `update*`, `delete*`, `upload*` methods are automatically blocked -3. **File writes**: Use `safeFs.writeFile()`, `safeFs.unlink()`, etc. instead of raw `fs` methods -4. **Other**: Wrap with `safeExec(action, description)` for custom operations - -### Special Cases - -Some operations need explicit `isDryRun()` checks: - -- Commands with their own `--dry-run` flag (e.g., `dart pub publish --dry-run` in pubDev target) -- Operations that need to return mock data in dry-run mode -- User experience optimizations (e.g., skipping sleep timers) - - -## Long-term Knowledge - -For long-term knowledge entries managed by [lore](https://github.com/BYK/loreai) (gotchas, patterns, decisions, architecture), see [`.lore.md`](.lore.md) in the project root. - +Skills are under `.agents/skills/`, generated from the canonical `skills/` tree +by `scripts/sync-skills.mjs`. Always load `repo-setup` before situation skills. diff --git a/docs/src/content/docs/targets/vercel.md b/docs/src/content/docs/targets/vercel.md index e136391ae..cb54fc644 100644 --- a/docs/src/content/docs/targets/vercel.md +++ b/docs/src/content/docs/targets/vercel.md @@ -7,7 +7,7 @@ Deploys a release artifact to [Vercel](https://vercel.com/) as a production depl The target extracts a ZIP artifact and deploys it via the Vercel deploy API (using [`@vercel/client`](https://www.npmjs.com/package/@vercel/client)) to promote it to production. It does not use or require the `vercel` CLI. -This target is release-gated: it runs as part of `craft publish`, so a deployment only happens on release and the deployed site stays in sync with the published version — the same guarantee the [`gh-pages`](./gh-pages/) target provides, but for Vercel-hosted sites. +It only runs as part of `craft publish`, so a deployment only happens on release and the deployed site stays in sync with the published version — the same guarantee the [`gh-pages`](./gh-pages/) target provides, but for Vercel-hosted sites. ## Configuration @@ -36,7 +36,7 @@ By default, this target: 2. Extracts its contents (preserving the archive layout, e.g. a top-level `.vercel/output`). 3. Deploys to production via the Vercel deploy API. -The version being released is attached to the deployment as metadata (`craftRelease=`) for traceability. +The version being released is attached to the deployment as metadata (`release=`) for traceability. ## Example diff --git a/src/targets/__tests__/vercel.test.ts b/src/targets/__tests__/vercel.test.ts index c1a2c6f27..6860089a1 100644 --- a/src/targets/__tests__/vercel.test.ts +++ b/src/targets/__tests__/vercel.test.ts @@ -22,7 +22,7 @@ vi.mock('../../utils/system', async importOriginal => { const actual = await importOriginal(); return { ...actual, - extractZipArchiveWithFlattening: vi.fn(async () => undefined), + extractZipArchive: vi.fn(async () => undefined), }; }); @@ -159,7 +159,7 @@ describe('publish', () => { await target.publish(version, revision); - expect(system.extractZipArchiveWithFlattening).toHaveBeenCalledWith( + expect(system.extractZipArchive).toHaveBeenCalledWith( '/downloads/vercel.zip', TMP_DIR, ); @@ -172,7 +172,7 @@ describe('publish', () => { expect(clientOptions.prebuilt).toBe(true); expect(clientOptions.skipAutoDetectionConfirmation).toBe(true); expect(deploymentOptions.target).toBe('production'); - expect(deploymentOptions.meta.craftRelease).toBe(version); + expect(deploymentOptions.meta.release).toBe(version); }); test('omits prebuilt when prebuilt is false', async () => { @@ -199,19 +199,16 @@ describe('publish', () => { expect('name' in deploymentOptions).toBe(false); }); - test('forwards org/project IDs when set', async () => { + test('forwards org ID when set', async () => { mockDeployment(); process.env.VERCEL_ORG_ID = ORG_ID; - process.env.VERCEL_PROJECT_ID = PROJECT_ID; const target = createVercelTarget({}); stubArtifacts(target, [artifact]); await target.publish(version, revision); - const [clientOptions, deploymentOptions] = (createDeployment as any).mock - .calls[0]; + const [clientOptions] = (createDeployment as any).mock.calls[0]; expect(clientOptions.teamId).toBe(ORG_ID); - expect(deploymentOptions.name).toBe(PROJECT_ID); }); test('deploys from workingDir subdirectory when configured', async () => { @@ -256,7 +253,7 @@ describe('publish', () => { // Artifact is still extracted (local, safe), but the remote deploy is // skipped. - expect(system.extractZipArchiveWithFlattening).toHaveBeenCalled(); + expect(system.extractZipArchive).toHaveBeenCalled(); expect(createDeployment).not.toHaveBeenCalled(); }); diff --git a/src/targets/vercel.ts b/src/targets/vercel.ts index 6003bd769..e5aebda1c 100644 --- a/src/targets/vercel.ts +++ b/src/targets/vercel.ts @@ -12,7 +12,7 @@ import { reportError } from '../utils/errors'; import { withTempDir } from '../utils/files'; import { isDryRun } from '../utils/helpers'; import { logDryRun } from '../utils/dryRun'; -import { extractZipArchiveWithFlattening } from '../utils/system'; +import { extractZipArchive } from '../utils/system'; import { BaseTarget } from './base'; import { BaseArtifactProvider } from '../artifact_providers/base'; @@ -156,6 +156,7 @@ export class VercelTarget extends BaseTarget { * @returns the production deployment URL */ private async deploy(deployDir: string, version: string): Promise { + let url: string | undefined; for await (const event of createDeployment( { token: this.vercelConfig.VERCEL_TOKEN, @@ -165,24 +166,27 @@ export class VercelTarget extends BaseTarget { skipAutoDetectionConfirmation: true, }, { - // `production` deploys to production; `craftRelease` ties the - // deployment back to the released version for traceability. + // `production` deploys to production; `release` ties the deployment + // back to the released version for traceability. target: 'production', - meta: { craftRelease: version }, - // Link to the configured project non-interactively when set; otherwise - // the deploy falls back to the artifact's `.vercel/project.json`. - ...(this.vercelConfig.projectId - ? { name: this.vercelConfig.projectId } - : {}), + meta: { release: version }, }, )) { if (event.type === 'ready') { - return event.payload.url as string; + url = event.payload.url as string; + } + if (event.type === 'alias-assigned') { + return url || (event.payload.url as string); } if (event.type === 'error') { throw event.payload; } } + if (url) { + // Production alias assignment is best-effort for some projects; if the + // stream ended after `ready` we still return the URL. + return url; + } throw new Error('Vercel deploy finished without a ready deployment'); } @@ -216,7 +220,7 @@ export class VercelTarget extends BaseTarget { await withTempDir( async directory => { this.logger.info(`Extracting "${archivePath}" to "${directory}"...`); - await extractZipArchiveWithFlattening(archivePath, directory); + await extractZipArchive(archivePath, directory); const deployDir = this.vercelConfig.workingDir ? join(directory, this.vercelConfig.workingDir) @@ -227,7 +231,7 @@ export class VercelTarget extends BaseTarget { // mode -- including worktree mode. Guard explicitly here, before any // network calls. if (isDryRun()) { - logDryRun(`vercel deploy --prod (${deployDir})`); + logDryRun(`@vercel/client createDeployment (${deployDir})`); return; } From de72ac64a2e33dfe3982ebbda45a1bc050327f1f Mon Sep 17 00:00:00 2001 From: "jared-outpost[bot]" Date: Fri, 7 Aug 2026 13:47:02 +0000 Subject: [PATCH 6/9] revert: restore AGENTS.md to master The container-harness AGENTS.md overlay was accidentally committed; restore the repository's own AGENTS.md so it's not part of this PR's diff. #skip-changelog --- AGENTS.md | 132 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0141e32f4..bb307b449 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,29 +1,119 @@ -# Jared (Outpost agent) +# AGENTS.md -Autonomous GitHub coding agent. Work in `/workspace/repo`. +This file provides guidance for AI coding assistants working with the Craft codebase. -## Model tiers +## Package Management -The primary model is chosen per event (see `src/agents/models.ts`): heavy for -code-producing situations, cheaper for lightweight ones. +- **Always use `pnpm`** for package management. Never use `npm` or `yarn`. +- Node.js version is managed by [Volta](https://volta.sh/) (currently v22.12.0). +- Install dependencies with `pnpm install --frozen-lockfile`. -| Role | Subagent | Model | -| --- | --- | --- | -| Triage / plan / review (heavy) | (primary Jared) | Claude Opus 4.8 | -| Triage / plan / review (light) | (primary Jared) | xAI Grok 4.3 | -| Explore | `explore` | OpenAI gpt-5-mini | -| Implement | `implement` | Moonshot kimi-k2.7-code | -| Ship (commit/push/PR) | `ship` | xAI Grok (`grok-build-0.1`) | +## Development Commands -Pipeline: triage → explore → plan → implement → review → ship. -(`worker` is a deprecated alias of `implement`.) +| Command | Description | +| ------------ | ------------------------------------------- | +| `pnpm build` | Build the project (outputs to `dist/craft`) | +| `pnpm test` | Run tests | +| `pnpm lint` | Run ESLint | +| `pnpm fix` | Auto-fix lint issues | -Operators also talk to Jared directly from the Outpost dashboard. Those turns -(`New operator chat` / `Operator guidance:`) skip triage — treat the request as -the task and answer in the conversation. +To manually test changes: -Long-term project knowledge for *this* Outpost repo lives in `.lore.md` when present. -For target repositories, read their `AGENTS.md` / `CONTRIBUTING.md` first. +```bash +pnpm build && ./dist/craft +``` -Skills are under `.agents/skills/`, generated from the canonical `skills/` tree -by `scripts/sync-skills.mjs`. Always load `repo-setup` before situation skills. +## Code Style + +- **TypeScript** is used throughout the codebase. +- **Prettier** 3.x with single quotes and no arrow parens (configured in `.prettierrc.yml`). +- **ESLint** 9.x with flat config (`eslint.config.mjs`) using `typescript-eslint`. +- Unused variables prefixed with `_` are allowed (e.g., `_unusedParam`). + +## Project Structure + +``` +src/ +├── __mocks__/ # Test mocks +├── __tests__/ # Test files (*.test.ts) +├── artifact_providers/ # Artifact provider implementations +├── commands/ # CLI command implementations +├── schemas/ # Zod schemas and TypeScript types for config +├── status_providers/ # Status provider implementations +├── targets/ # Release target implementations +├── types/ # Shared TypeScript types +├── utils/ # Utility functions +├── config.ts # Configuration loading +├── index.ts # CLI entry point +└── logger.ts # Logging utilities +dist/ +└── craft # Single bundled executable (esbuild output) +``` + +## Testing + +- Tests use **Vitest**. +- Test files are located in `src/__tests__/` and follow the `*.test.ts` naming pattern. +- Run tests with `pnpm test`. +- Use `vi.fn()`, `vi.mock()`, `vi.spyOn()` for mocking (Vitest's mock API). + +## CI/CD + +- Main branch is `master`. +- CI runs tests on Node.js 20 and 22. +- Craft releases itself using its own tooling (dogfooding). + +## Configuration + +- Project configuration lives in `.craft.yml` at the repository root. +- The configuration schema is defined in `src/schemas/`. + +## Dry-Run Mode + +Craft supports a `--dry-run` flag that prevents destructive operations. This is implemented via a centralized abstraction layer. + +### How It Works + +Instead of checking `isDryRun()` manually in every function, destructive operations are wrapped with dry-run-aware proxies: + +- **Git operations**: Use `getGitClient()` from `src/utils/git.ts` or `createGitClient(directory)` for working with specific directories +- **GitHub API**: Use `getGitHubClient()` from `src/utils/githubApi.ts` +- **File writes**: Use `safeFs` from `src/utils/dryRun.ts` +- **Other actions**: Use `safeExec()` or `safeExecSync()` from `src/utils/dryRun.ts` + +### ESLint Enforcement + +ESLint rules prevent direct usage of raw APIs: + +- `no-restricted-imports`: Blocks direct `simple-git` imports +- `no-restricted-syntax`: Blocks `new Octokit()` instantiation + +If you're writing a wrapper module that needs raw access, use: + +```typescript +// eslint-disable-next-line no-restricted-imports -- This is the wrapper module +import simpleGit from 'simple-git'; +``` + +### Adding New Destructive Operations + +When adding new code that performs destructive operations: + +1. **Git**: Get the git client via `getGitClient()` or `createGitClient()` - mutating methods are automatically blocked +2. **GitHub API**: Get the client via `getGitHubClient()` - `create*`, `update*`, `delete*`, `upload*` methods are automatically blocked +3. **File writes**: Use `safeFs.writeFile()`, `safeFs.unlink()`, etc. instead of raw `fs` methods +4. **Other**: Wrap with `safeExec(action, description)` for custom operations + +### Special Cases + +Some operations need explicit `isDryRun()` checks: + +- Commands with their own `--dry-run` flag (e.g., `dart pub publish --dry-run` in pubDev target) +- Operations that need to return mock data in dry-run mode +- User experience optimizations (e.g., skipping sleep timers) + + +## Long-term Knowledge + +For long-term knowledge entries managed by [lore](https://github.com/BYK/loreai) (gotchas, patterns, decisions, architecture), see [`.lore.md`](.lore.md) in the project root. + From 1726b49ff7b804f24409b27f44f0489eacec53ec Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 7 Aug 2026 14:38:52 +0000 Subject: [PATCH 7/9] fix(vercel): link via project ID, fix js-yaml override, address review feedback --- docs/src/content/docs/targets/vercel.md | 10 ++--- package.json | 3 +- pnpm-lock.yaml | 13 ++---- src/targets/__tests__/vercel.test.ts | 54 ++++++++++++++++++++++++- src/targets/vercel.ts | 32 +++++++++------ 5 files changed, 82 insertions(+), 30 deletions(-) diff --git a/docs/src/content/docs/targets/vercel.md b/docs/src/content/docs/targets/vercel.md index cb54fc644..51e170a53 100644 --- a/docs/src/content/docs/targets/vercel.md +++ b/docs/src/content/docs/targets/vercel.md @@ -7,8 +7,6 @@ Deploys a release artifact to [Vercel](https://vercel.com/) as a production depl The target extracts a ZIP artifact and deploys it via the Vercel deploy API (using [`@vercel/client`](https://www.npmjs.com/package/@vercel/client)) to promote it to production. It does not use or require the `vercel` CLI. -It only runs as part of `craft publish`, so a deployment only happens on release and the deployed site stays in sync with the published version — the same guarantee the [`gh-pages`](./gh-pages/) target provides, but for Vercel-hosted sites. - ## Configuration | Option | Description | @@ -21,11 +19,11 @@ It only runs as part of `craft publish`, so a deployment only happens on release | Name | Required | Description | |------|----------|-------------| | `VERCEL_TOKEN` | Yes | Vercel access token. Passed to the deploy API, never on the command line. | -| `VERCEL_ORG_ID` | No | Vercel organization/team ID. An identifier, not a secret. Forwarded to the deploy API as the team ID so the deployment links non-interactively; when unset, the deploy falls back to the `.vercel/project.json` inside the artifact. | -| `VERCEL_PROJECT_ID` | No | Vercel project ID. An identifier, not a secret. Forwarded to the deploy API to link the deployment to the project; when unset, the deploy falls back to the `.vercel/project.json` inside the artifact. | +| `VERCEL_ORG_ID` | No | Vercel organization/team ID. An identifier, not a secret. Forwarded to the deploy API as the team ID so the deployment links non-interactively. | +| `VERCEL_PROJECT_ID` | No | Vercel project ID. An identifier, not a secret. Forwarded to the deploy API as the project identifier so the deployment links to the intended project non-interactively. | :::note -For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (or ship a `.vercel/project.json` in the artifact) so the deploy API knows which project to deploy to. +For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` so the deploy API knows which project to deploy to. ::: ## Default Behavior @@ -59,4 +57,4 @@ targets: 1. Build the site in CI (e.g. `vercel build`) and create a `vercel.zip` artifact containing the prebuilt `.vercel/output` (or the source when `prebuilt: false`). 2. Configure the target in `.craft.yml`. -3. Set `VERCEL_TOKEN` in your environment, plus `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` (or include a `.vercel/project.json` in the artifact) so the deploy targets the right project. +3. Set `VERCEL_TOKEN` in your environment, plus `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` so the deploy targets the right project. diff --git a/package.json b/package.json index 34a48baa7..d3a26517c 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,8 @@ "form-data@>=4": "^4.0.6", "form-data@<3": "^2.5.6", "vite": "^7.3.5", - "@babel/core": "^7.29.6" + "@babel/core": "^7.29.6", + "js-yaml": "^4.3.0" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d01737fdf..30ea501d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,7 @@ overrides: form-data@<3: ^2.5.6 vite: ^7.3.5 '@babel/core': ^7.29.6 + js-yaml: ^4.3.0 importers: @@ -155,7 +156,7 @@ importers: specifier: ^2.0.0 version: 2.0.0 js-yaml: - specifier: 4.3.0 + specifier: ^4.3.0 version: 4.3.0 mkdirp: specifier: ^1.0.4 @@ -2521,10 +2522,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} hasBin: true @@ -5269,7 +5266,7 @@ snapshots: '@bytecodealliance/preview2-shim': 0.17.6 '@renovatebot/pep440': 4.2.1 fs-extra: 11.1.1 - js-yaml: 4.1.1 + js-yaml: 4.3.0 minimatch: 10.2.6 smol-toml: 1.5.2 zod: 3.22.4 @@ -6118,10 +6115,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - js-yaml@4.3.0: dependencies: argparse: 2.0.1 diff --git a/src/targets/__tests__/vercel.test.ts b/src/targets/__tests__/vercel.test.ts index 6860089a1..59c340260 100644 --- a/src/targets/__tests__/vercel.test.ts +++ b/src/targets/__tests__/vercel.test.ts @@ -40,6 +40,13 @@ function mockDeployment(url = 'my-app.vercel.app'): void { }); } +function mockDeploymentWithAlias(url = 'my-app.vercel.app'): void { + (createDeployment as any).mockImplementation(async function* () { + yield { type: 'ready', payload: { url } }; + yield { type: 'alias-assigned', payload: { url } }; + }); +} + function mockDeploymentError(error: Error): void { (createDeployment as any).mockImplementation(async function* () { yield { type: 'error', payload: error }; @@ -196,7 +203,7 @@ describe('publish', () => { const [clientOptions, deploymentOptions] = (createDeployment as any).mock .calls[0]; expect(clientOptions.teamId).toBeUndefined(); - expect('name' in deploymentOptions).toBe(false); + expect('project' in deploymentOptions).toBe(false); }); test('forwards org ID when set', async () => { @@ -211,6 +218,18 @@ describe('publish', () => { expect(clientOptions.teamId).toBe(ORG_ID); }); + test('links the deployment to the project when VERCEL_PROJECT_ID is set', async () => { + mockDeployment(); + process.env.VERCEL_PROJECT_ID = PROJECT_ID; + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + const [, deploymentOptions] = (createDeployment as any).mock.calls[0]; + expect(deploymentOptions.project).toBe(PROJECT_ID); + }); + test('deploys from workingDir subdirectory when configured', async () => { mockDeployment(); const target = createVercelTarget({ workingDir: 'dist' }); @@ -264,4 +283,37 @@ describe('publish', () => { await expect(target.publish(version, revision)).rejects.toThrow(/boom/); }); + + test('succeeds when the deploy stream yields alias-assigned after ready', async () => { + mockDeploymentWithAlias(); + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + expect(createDeployment).toHaveBeenCalledTimes(1); + }); + + test('succeeds with a ready deployment when no alias-assigned event arrives', async () => { + mockDeployment(); + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await target.publish(version, revision); + + expect(createDeployment).toHaveBeenCalledTimes(1); + }); + + test('throws when the stream ends without a ready deployment', async () => { + (createDeployment as any).mockImplementation(async function* () { + // Stream ends without ready/alias-assigned/error events. + yield { type: 'created', payload: { url: 'x.vercel.app' } }; + }); + const target = createVercelTarget({}); + stubArtifacts(target, [artifact]); + + await expect(target.publish(version, revision)).rejects.toThrow( + /without a ready deployment/, + ); + }); }); diff --git a/src/targets/vercel.ts b/src/targets/vercel.ts index e5aebda1c..f51dae612 100644 --- a/src/targets/vercel.ts +++ b/src/targets/vercel.ts @@ -33,8 +33,8 @@ type SecretsType = (typeof targetSecrets)[number]; /** * Optional, non-secret identifiers forwarded to the Vercel API when present. * They link the deployment to a specific team/project non-interactively, which - * is what CI needs. When unset, the deploy falls back to the - * `.vercel/project.json` inside the artifact. + * is what CI needs. Without a project ID, the deployment name is derived from + * the deploy directory, which would not reliably target the intended project. */ const ORG_ID_ENV_VAR = 'VERCEL_ORG_ID'; const PROJECT_ID_ENV_VAR = 'VERCEL_PROJECT_ID'; @@ -71,7 +71,8 @@ export interface VercelTargetConfig { orgId?: string; /** * Optional Vercel project ID (an identifier, not a secret). Forwarded to the - * deploy as the project name when set. + * deploy as the `project` identifier so the deployment links to the intended + * project non-interactively. */ projectId?: string; } @@ -147,9 +148,10 @@ export class VercelTarget extends BaseTarget { /** * Runs a Vercel production deploy of `deployDir` and waits for it to finish. * - * Drives the `@vercel/client` event stream to completion: resolves with the - * live deployment URL on `ready`, and throws on the `error` event so a failed - * deploy fails the release. + * Drives the `@vercel/client` event stream to completion: resolves on the + * `alias-assigned` event (the production promotion is done), falls back to + * the `ready` deployment URL when the stream ends without an alias event, and + * throws on the `error` event so a failed deploy fails the release. * * @param deployDir Directory to deploy from * @param version The version being released (attached as deploy provenance) @@ -157,6 +159,17 @@ export class VercelTarget extends BaseTarget { */ private async deploy(deployDir: string, version: string): Promise { let url: string | undefined; + const deploymentOptions: Record = { + // `production` deploys to production; `release` ties the deployment + // back to the released version for traceability. + target: 'production', + meta: { release: version }, + }; + if (this.vercelConfig.projectId) { + // `project` (the project ID) overrides `name` and reliably links the + // deployment to the configured project non-interactively. + deploymentOptions.project = this.vercelConfig.projectId; + } for await (const event of createDeployment( { token: this.vercelConfig.VERCEL_TOKEN, @@ -165,12 +178,7 @@ export class VercelTarget extends BaseTarget { teamId: this.vercelConfig.orgId, skipAutoDetectionConfirmation: true, }, - { - // `production` deploys to production; `release` ties the deployment - // back to the released version for traceability. - target: 'production', - meta: { release: version }, - }, + deploymentOptions, )) { if (event.type === 'ready') { url = event.payload.url as string; From fca56e3e2bd54cd59e803db40ac7d4bdd68d8dba Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 7 Aug 2026 15:21:20 +0000 Subject: [PATCH 8/9] fix(deps): override path-to-regexp to 6.3.0 to address GHSA-9wv6-86v2-598j --- package.json | 3 ++- pnpm-lock.yaml | 15 +++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index d3a26517c..4e1496d9b 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,8 @@ "form-data@<3": "^2.5.6", "vite": "^7.3.5", "@babel/core": "^7.29.6", - "js-yaml": "^4.3.0" + "js-yaml": "^4.3.0", + "path-to-regexp@<6.3.0": "^6.3.0" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30ea501d2..4189cece4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,7 @@ overrides: vite: ^7.3.5 '@babel/core': ^7.29.6 js-yaml: ^4.3.0 + path-to-regexp@<6.3.0: ^6.3.0 importers: @@ -2785,12 +2786,6 @@ packages: resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} - path-to-regexp@6.1.0: - resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} - - path-to-regexp@6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -5255,7 +5250,7 @@ snapshots: http-proxy: 1.18.1 jsonc-parser: 3.3.1 nanoid: 3.3.16 - path-to-regexp: 6.2.1 + path-to-regexp: 6.3.0 optionalDependencies: vite: 7.3.5(@types/node@24.13.2)(tsx@4.21.0) transitivePeerDependencies: @@ -5273,7 +5268,7 @@ snapshots: '@vercel/routing-utils@6.4.1': dependencies: - path-to-regexp: 6.1.0 + path-to-regexp: 6.3.0 path-to-regexp-updated: path-to-regexp@6.3.0 optionalDependencies: ajv: 6.14.0 @@ -6345,10 +6340,6 @@ snapshots: lru-cache: 11.2.2 minipass: 7.1.2 - path-to-regexp@6.1.0: {} - - path-to-regexp@6.2.1: {} - path-to-regexp@6.3.0: {} pathe@2.0.3: {} From 51cab289873990824bcfc9c17e7309c430ea2059 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Fri, 7 Aug 2026 15:35:21 +0000 Subject: [PATCH 9/9] fix(deps): override tar-fs to 1.16.6 to address GHSA-pq67-2wwv-3xjx and related advisories --- package.json | 3 ++- pnpm-lock.yaml | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4e1496d9b..9a8d029a4 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,8 @@ "vite": "^7.3.5", "@babel/core": "^7.29.6", "js-yaml": "^4.3.0", - "path-to-regexp@<6.3.0": "^6.3.0" + "path-to-regexp@<6.3.0": "^6.3.0", + "tar-fs@<1.16.4": "1.16.6" } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4189cece4..233b7caf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,7 @@ overrides: '@babel/core': ^7.29.6 js-yaml: ^4.3.0 path-to-regexp@<6.3.0: ^6.3.0 + tar-fs@<1.16.4: 1.16.6 importers: @@ -3068,8 +3069,8 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - tar-fs@1.16.3: - resolution: {integrity: sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==} + tar-fs@1.16.6: + resolution: {integrity: sha512-JkOgFt3FxM/2v2CNpAVHqMW2QASjc/Hxo7IGfNd3MHaDYSW/sBFiS7YVmmhmr8x6vwN1VFQDQGdT2MWpmIuVKA==} tar-stream@1.6.2: resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} @@ -5227,7 +5228,7 @@ snapshots: ms: 2.1.2 querystring: 0.2.1 sleep-promise: 8.0.1 - tar-fs: 1.16.3 + tar-fs: 1.16.6 transitivePeerDependencies: - '@sveltejs/kit' - '@vercel/analytics' @@ -6624,7 +6625,7 @@ snapshots: dependencies: has-flag: 4.0.0 - tar-fs@1.16.3: + tar-fs@1.16.6: dependencies: chownr: 1.1.4 mkdirp: 0.5.6