diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index 2cb0705..5c04fef 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -384,17 +384,22 @@ prisma-cli --version --json `prisma-cli version` is the richer environment report; `prisma-cli --version` is the terse one-liner. Both report the same `cli.version`. Use the flag for quick checks, the subcommand for support tickets and bug reports. -## `prisma-cli init --framework --entry --http-port --region --name --link --no-link --project --install --no-install` +## `prisma-cli init --framework --entry --http-port --region --name --link --no-link --project --install --no-install --format ` Purpose: -- write a committed `prisma.compute.ts` for the app in this directory +- write a committed `prisma.compute.ts` (or, with `--format json`, a dependency-free `prisma.compute.json`) for the app in this directory Behavior: - init is the config formalizer: `app deploy` works with zero config, and init writes down what deploy would infer so the setup is committed, reviewable, and stable for teammates and CI - writing the config requires no auth and no network; linking is the only remote step - fails with `INIT_CONFIG_EXISTS` when a compute config already exists in the invocation directory or any ancestor up to the repository or workspace root; the error names the existing file, and init never overwrites or merges — editing a committed config is the user's editor's job +- `--format ` selects the config serialization; `ts` (alias `typescript`) is the default and writes `prisma.compute.ts`, `json` writes `prisma.compute.json` via the shared SDK serializer, a dependency-free static document (a `$schema` reference will be emitted once the schema is hosted; the loader already accepts and strips one); both formats pin exactly the same resolved values, and the CLI's global `--json` output flag is unrelated to the config format +- with `--format json`, the types install step never runs: the JSON format exists to be dependency-free, so `--install` is a usage error; without `--install`, the result reports the step as skipped with no install hint +- with `--format json` and the custom framework, init fails with a usage error: custom needs `build.outputDirectory` and `build.entrypoint`, which init does not collect, and strict JSON cannot carry the commented build stub the TypeScript format uses; the fix is the TypeScript format or a hand-written `build` object +- graduation path: an explicit `--format ts` with an existing `prisma.compute.json` converts it in place; the JSON config is loaded and validated, the equivalent `prisma.compute.ts` wrapping the same object in `defineComputeConfig` is written next to it, the JSON file is then deleted (a failed delete rolls the write back, and a failed rollback fails with `INIT_CONVERT_INCOMPLETE` so two config files never coexist silently), and the types install and link steps run as usual (`--install`/`--no-install`, `--link`/`--no-link`/`--project`) and act on the discovered config directory, not the invocation directory, so a conversion run from a nested app dir installs types and writes the project pin where the config lives; config values are transported, never re-resolved, so the value-resolution flags (`--framework`, `--entry`, `--http-port`, `--name`, `--region`) are usage errors during conversion, and the conversion is reported with `converted: true` +- conversion is one-way and explicit: plain `init` with any existing config still fails with `INIT_CONFIG_EXISTS`, and `--format json` with an existing TypeScript config fails with `INIT_CONVERT_UNSUPPORTED` because TypeScript configs may contain logic that a static JSON file cannot express; a fully static config can be rewritten by hand - detects the framework from the same registry and signals `app deploy` uses; explicit `--framework` wins over detection - `--entry` sets the source entrypoint for entrypoint frameworks (Bun, Hono); `--http-port` overrides the framework default port; `--name` overrides the app name inferred from `package.json#name` or the directory name - previews the resolved values with per-value source annotations (`detected`, `framework default`, `package.json`, `flag`) before writing; in interactive mode the preview is followed by an optional adjust step for framework and HTTP port, and `--yes` accepts the preview as shown @@ -415,7 +420,7 @@ Behavior: - link failures and cancellations after the config is written downgrade to warnings and `nextSteps`; the config write stands and init exits 0 - `nextSteps` includes the deploy command, plus the project link command when the directory is still unlinked - user-facing command hints in init output (next steps, link hints, error recovery commands) use the package runner detected from the project, such as `pnpm dlx @prisma/cli@latest project link` or `npx -y @prisma/cli@latest app deploy`, matching the `agent` group's convention -- in `--json`, `result` includes `configPath`, the written `app` values, per-value `settings` sources, and `link` state; `--json` never prompts +- in `--json`, `result` includes `configPath`, `format` (`typescript` or `json`), `converted` (true only for the `--format ts` conversion path), the written `app` values (null when a conversion transported a config that does not pin a single fully-resolved app), per-value `settings` sources, and `link` state; `--json` never prompts Examples: @@ -424,6 +429,8 @@ prisma-cli init prisma-cli init --framework hono --entry src/index.ts prisma-cli init --name api --http-port 8080 --no-link prisma-cli init --project proj_123 +prisma-cli init --format json +prisma-cli init --format ts prisma-cli init --json ``` diff --git a/docs/product/error-conventions.md b/docs/product/error-conventions.md index ef9fa9f..7939fd3 100644 --- a/docs/product/error-conventions.md +++ b/docs/product/error-conventions.md @@ -188,6 +188,8 @@ These codes are the minimum stable set for the MVP: - `BUILD_SETTINGS_UNSUPPORTED` - `FRAMEWORK_NOT_DETECTED` - `INIT_CONFIG_EXISTS` +- `INIT_CONVERT_UNSUPPORTED` +- `INIT_CONVERT_INCOMPLETE` - `INIT_DETECTION_FAILED` - `DEPLOYMENT_NOT_FOUND` - `NO_DEPLOYMENTS` @@ -261,6 +263,8 @@ Recommended meanings: - `BUILD_SETTINGS_UNSUPPORTED`: a compute config `build` block targets a framework whose SDK strategy does not consume committed build settings - `FRAMEWORK_NOT_DETECTED`: app deploy could not detect a supported Beta framework and no explicit framework/build type was provided - `INIT_CONFIG_EXISTS`: a compute config already exists in this directory or an ancestor; init never overwrites or merges +- `INIT_CONVERT_UNSUPPORTED`: `init --format json` found an existing TypeScript config; TypeScript configs may contain logic, so converting them to JSON automatically would be lossy and the rewrite is manual +- `INIT_CONVERT_INCOMPLETE`: a conversion wrote prisma.compute.ts but could not delete prisma.compute.json, and rolling back the write also failed; both files exist and one must be deleted by hand - `INIT_DETECTION_FAILED`: no supported framework detected and no --framework passed; `meta.frameworks` lists the valid values - `DEPLOYMENT_NOT_FOUND`: requested deployment id does not exist - `NO_DEPLOYMENTS`: command resolved a branch or app but found no deployments diff --git a/packages/cli/package.json b/packages/cli/package.json index 3623423..4a84bbf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "@clack/prompts": "^1.5.0", - "@prisma/compute-sdk": "0.31.0", + "@prisma/compute-sdk": "0.33.0", "@prisma/credentials-store": "^7.8.0", "@prisma/management-api-sdk": "^1.46.0", "better-result": "^2.9.2", diff --git a/packages/cli/src/commands/init/index.ts b/packages/cli/src/commands/init/index.ts index 5e5a342..647229f 100644 --- a/packages/cli/src/commands/init/index.ts +++ b/packages/cli/src/commands/init/index.ts @@ -43,7 +43,13 @@ export function createInitCommand(runtime: CliRuntime): Command { .addOption( new Option("--install", "Install @prisma/compute-sdk for config types"), ) - .addOption(new Option("--no-install", "Skip the types install step")); + .addOption(new Option("--no-install", "Skip the types install step")) + .addOption( + new Option( + "--format ", + "Config file format: ts (default) writes prisma.compute.ts, json writes dependency-free prisma.compute.json; --format ts converts an existing prisma.compute.json", + ), + ); addGlobalFlags(command); command.action(async (options) => { @@ -56,6 +62,7 @@ export function createInitCommand(runtime: CliRuntime): Command { link?: boolean; project?: string; install?: boolean; + format?: string; }; await runCommand( @@ -72,6 +79,7 @@ export function createInitCommand(runtime: CliRuntime): Command { link: flags.link, project: flags.project, install: flags.install, + format: flags.format, }), { renderHuman: (context, descriptor, result) => diff --git a/packages/cli/src/controllers/init.ts b/packages/cli/src/controllers/init.ts index b492bac..80c4498 100644 --- a/packages/cli/src/controllers/init.ts +++ b/packages/cli/src/controllers/init.ts @@ -1,7 +1,8 @@ -import { writeFile } from "node:fs/promises"; +import { readFile, rm, writeFile } from "node:fs/promises"; import path from "node:path"; import { COMPUTE_CONFIG_FILENAME, + COMPUTE_CONFIG_JSON_FILENAME, COMPUTE_REGIONS, type ComputeConfig, type ComputeFramework, @@ -12,7 +13,10 @@ import { findComputeConfigDir, frameworkByKey, frameworkFromAlias, + type LoadedComputeConfig, + normalizeComputeConfig, serializeComputeConfig, + serializeComputeConfigJson, } from "@prisma/compute-sdk/config"; import { execa } from "execa"; @@ -39,6 +43,7 @@ import { } from "../shell/prompt"; import { type CommandContext, canPrompt } from "../shell/runtime"; import type { + InitConfigFormat, InitLinkState, InitResult, InitSettingRow, @@ -56,6 +61,7 @@ export interface InitFlags { link?: boolean; project?: string; install?: boolean; + format?: string; } interface ResolvedInitFramework { @@ -74,7 +80,37 @@ export async function runInit( // bunx, npx -y), matching the agent group's convention. const formatCommand = resolvePrismaCliPackageCommandFormatterSync(cwd); - await requireNoExistingComputeConfig(cwd, signal); + const format = parseInitFormat(flags.format, formatCommand); + if (format.value === "json" && flags.install === true) { + throw usageError( + "--install does not apply to the JSON config format", + `${COMPUTE_CONFIG_JSON_FILENAME} is a dependency-free static config; the ${COMPUTE_SDK_PACKAGE} devDependency exists only for ${COMPUTE_CONFIG_FILENAME} editor types.`, + "Drop --install, or use the TypeScript format.", + [formatCommand(["init", "--format", "json"])], + "app", + ); + } + + const existingConfig = await findExistingComputeConfig(cwd, signal); + if (existingConfig) { + const solePath = + existingConfig.candidates.length === 1 + ? existingConfig.candidates[0] + : undefined; + const soleIsJson = + solePath !== undefined && path.extname(solePath) === ".json"; + // Conversion must be explicit: plain init refuses every existing config. + if (soleIsJson && format.value === "typescript" && format.explicit) { + rejectConversionResolutionFlags(flags, formatCommand); + return runInitConversion(context, flags, solePath, formatCommand); + } + if (solePath && !soleIsJson && format.value === "json") { + throw initConvertUnsupportedError(solePath); + } + throw initConfigExistsError( + existingConfig.candidates[0] ?? existingConfig.directory, + ); + } const region = parseInitRegion(flags.region, formatCommand); let framework = await resolveInitFramework(context, flags, formatCommand); @@ -89,6 +125,20 @@ export async function runInit( framework = adjusted.framework; httpPort = adjusted.httpPort; + // The custom framework needs build.outputDirectory and build.entrypoint, + // which init does not collect. The TypeScript format carries a commented + // build stub to fill in; strict JSON cannot hold comments, so refuse here + // instead of writing a config that deploy would reject. + if (format.value === "json" && framework.key === "custom") { + throw usageError( + "Custom framework requires the TypeScript config format", + "The custom framework needs build.outputDirectory and build.entrypoint, which init does not collect; the TypeScript format includes a commented build stub to complete, and strict JSON cannot carry it.", + `Rerun without --format json and fill in the build stub, or write ${COMPUTE_CONFIG_JSON_FILENAME} by hand with a build object.`, + [formatCommand(["init", "--framework", "custom"])], + "app", + ); + } + // Entry resolves against the FINAL framework so an interactive framework // switch cannot leave a stale (or missing) entry in the written config. const entry = await resolveInitEntry(cwd, framework, flags.entry, signal); @@ -123,10 +173,19 @@ export async function runInit( }, }; - const configPath = path.join(cwd, COMPUTE_CONFIG_FILENAME); - let source = serializeComputeConfig(config); - if (framework.key === "custom") { - source += CUSTOM_BUILD_STUB; + const configFilename = + format.value === "json" + ? COMPUTE_CONFIG_JSON_FILENAME + : COMPUTE_CONFIG_FILENAME; + const configPath = path.join(cwd, configFilename); + let source: string; + if (format.value === "json") { + source = serializeComputeConfigJson(config); + } else { + source = serializeComputeConfig(config); + if (framework.key === "custom") { + source += CUSTOM_BUILD_STUB; + } } signal.throwIfAborted(); @@ -141,9 +200,18 @@ export async function runInit( } const warnings: string[] = []; - const types = await resolveInitTypes(context, flags, { - onWarning: (message) => warnings.push(message), - }); + // The JSON format exists to be dependency-free, so the types install step + // never runs for it; validation happens when commands load the config. + const types: InitTypesState = + format.value === "json" + ? { + status: "skipped", + package: COMPUTE_SDK_PACKAGE, + installCommand: null, + } + : await resolveInitTypes(context, flags, { + onWarning: (message) => warnings.push(message), + }); const link = await resolveInitLink(context, flags, { onWarning: (message) => warnings.push(message), formatCommand, @@ -155,7 +223,9 @@ export async function runInit( return { command: "init", result: { - configPath: COMPUTE_CONFIG_FILENAME, + configPath: configFilename, + format: format.value, + converted: false, directory: formatInitDirectory(cwd), app: { name: name.value, @@ -347,17 +417,292 @@ const CUSTOM_BUILD_STUB = ` // }, `; -async function requireNoExistingComputeConfig( +/** + * Nearest existing compute config, searching from `cwd` up to the source + * root. Init routes on this: refuse, convert, or proceed fresh. + */ +async function findExistingComputeConfig( cwd: string, signal: AbortSignal, -): Promise { +): Promise<{ directory: string; candidates: string[] } | null> { const configDir = await findComputeConfigDir(cwd, signal); if (!configDir) { + return null; + } + + return { + directory: configDir, + candidates: await findComputeConfigCandidates(configDir, signal), + }; +} + +function parseInitFormat( + value: string | undefined, + formatCommand: PrismaCliPackageCommandFormatter, +): { value: InitConfigFormat; explicit: boolean } { + if (value === undefined) { + return { value: "typescript", explicit: false }; + } + + const normalized = value.trim().toLowerCase(); + if (normalized === "ts" || normalized === "typescript") { + return { value: "typescript", explicit: true }; + } + if (normalized === "json") { + return { value: "json", explicit: true }; + } + + throw usageError( + "Unknown config format", + `"${value}" is not a supported config format.`, + "Pass --format ts or --format json.", + [formatCommand(["init", "--format", "json"])], + "app", + ); +} + +/** + * Conversion transports the existing config's values; it never re-resolves + * settings. Refusing resolution flags beats silently ignoring them. + */ +function rejectConversionResolutionFlags( + flags: InitFlags, + formatCommand: PrismaCliPackageCommandFormatter, +): void { + const passed = [ + flags.framework !== undefined ? "--framework" : null, + flags.entry !== undefined ? "--entry" : null, + flags.httpPort !== undefined ? "--http-port" : null, + flags.name !== undefined ? "--name" : null, + flags.region !== undefined ? "--region" : null, + ].filter((flag): flag is string => flag !== null); + if (passed.length === 0) { return; } + throw usageError( + `${passed.join(", ")} ${passed.length === 1 ? "does" : "do"} not apply when converting an existing config`, + `--format ts with an existing ${COMPUTE_CONFIG_JSON_FILENAME} converts it as-is; settings are transported, never re-resolved.`, + `Convert first, then edit ${COMPUTE_CONFIG_FILENAME} directly.`, + [formatCommand(["init", "--format", "ts"])], + "app", + ); +} + +function initConvertIncompleteError( + jsonConfigPath: string, + tsConfigPath: string, +): CliError { + return new CliError({ + code: "INIT_CONVERT_INCOMPLETE", + domain: "app", + summary: "Conversion left two config files behind", + why: `${path.basename(tsConfigPath)} was written but ${path.basename(jsonConfigPath)} could not be deleted, and rolling back the write also failed. Commands refuse to load a directory with two config files.`, + fix: `Delete one file by hand: keep ${path.basename(tsConfigPath)} to finish the conversion, or keep ${path.basename(jsonConfigPath)} to undo it.`, + exitCode: 1, + nextSteps: [], + meta: { jsonConfigPath, tsConfigPath }, + }); +} + +function initConvertUnsupportedError(existingPath: string): CliError { + return new CliError({ + code: "INIT_CONVERT_UNSUPPORTED", + domain: "app", + summary: "TypeScript configs do not convert to JSON", + why: `${existingPath} may contain imports, expressions, or comments that the static ${COMPUTE_CONFIG_JSON_FILENAME} format cannot express, so an automatic conversion would be lossy.`, + fix: `If the config is fully static, rewrite it by hand as ${COMPUTE_CONFIG_JSON_FILENAME} and delete ${path.basename(existingPath)}.`, + exitCode: 1, + nextSteps: [], + meta: { existingConfigPath: existingPath }, + }); +} + +/** + * The graduation path: an explicit `--format ts` with an existing + * `prisma.compute.json` rewrites the same config as `prisma.compute.ts` and + * deletes the JSON file, so a static config can grow into a programmatic one. + * The values are transported, never re-resolved. + */ +async function runInitConversion( + context: CommandContext, + flags: InitFlags, + jsonConfigPath: string, + formatCommand: PrismaCliPackageCommandFormatter, +): Promise> { + const cwd = context.runtime.cwd; + const signal = context.runtime.signal; + + let parsed: unknown; + try { + parsed = JSON.parse(await readFile(jsonConfigPath, "utf8")); + } catch (error) { + if (signal.aborted) { + throw error; + } + throw initConvertInvalidError(jsonConfigPath, [ + error instanceof Error + ? (error.message.split("\n")[0] as string) + : String(error), + ]); + } + + // "$schema" is editor tooling metadata, not config; the TypeScript format + // carries types through its import instead. + const config = stripJsonSchemaKey(parsed); + const normalized = normalizeComputeConfig(config, jsonConfigPath); + if (normalized.isErr()) { + throw initConvertInvalidError(jsonConfigPath, normalized.error.issues); + } + const loaded = normalized.value; + + const tsConfigPath = path.join(loaded.configDir, COMPUTE_CONFIG_FILENAME); + const source = serializeComputeConfig(config as ComputeConfig); + + signal.throwIfAborted(); + try { + // wx: fail instead of clobbering a config that appeared since discovery. + await writeFile(tsConfigPath, source, { encoding: "utf8", flag: "wx" }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "EEXIST") { + throw initConfigExistsError(tsConfigPath); + } + throw error; + } + try { + await rm(jsonConfigPath); + } catch (error) { + // Two coexisting config files are a hard loader error, so a failed + // delete rolls the write back and leaves the JSON config untouched. + try { + await rm(tsConfigPath, { force: true }); + } catch { + throw initConvertIncompleteError(jsonConfigPath, tsConfigPath); + } + throw error; + } + + const settings = conversionSettings(loaded); + renderInitSettingsPreview(context, settings); + + const warnings: string[] = []; + // Conversion transports the config's values but its side-effect steps + // behave exactly like fresh init: install flags feed resolveInitTypes, + // and link flags (--link/--no-link/--project) resolve via resolveInitLink + // instead of being silently ignored. Both act on the config's home, not + // the invocation directory: discovery may have found the config in an + // ancestor, and the types dependency and project pin belong where the + // config lives (fresh init has no such split; it writes at cwd). + const stepContext = withRuntimeCwd(context, loaded.configDir); + const types = await resolveInitTypes(stepContext, flags, { + onWarning: (message) => warnings.push(message), + }); + const link = await resolveInitLink(stepContext, flags, { + onWarning: (message) => warnings.push(message), + formatCommand, + }); + + const unlinked = link.status !== "already-linked" && link.status !== "linked"; + const typesMissing = + types.status !== "installed" && types.status !== "already-installed"; + return { + command: "init", + result: { + configPath: path.relative(cwd, tsConfigPath) || COMPUTE_CONFIG_FILENAME, + format: "typescript", + converted: true, + directory: formatInitDirectory(loaded.configDir), + app: conversionApp(loaded), + settings, + types, + link, + }, + warnings, + nextSteps: [ + ...(typesMissing && types.installCommand ? [types.installCommand] : []), + formatCommand(["app", "deploy"]), + ...(unlinked ? [formatCommand(["project", "link"])] : []), + ], + }; +} + +/** The same command context, acting from `cwd` instead of the invocation directory. */ +function withRuntimeCwd(context: CommandContext, cwd: string): CommandContext { + if (path.resolve(context.runtime.cwd) === path.resolve(cwd)) { + return context; + } + return { ...context, runtime: { ...context.runtime, cwd } }; +} - const candidates = await findComputeConfigCandidates(configDir, signal); - throw initConfigExistsError(candidates[0] ?? configDir); +function stripJsonSchemaKey(parsed: unknown): unknown { + if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return parsed; + } + const { $schema: _schema, ...config } = parsed as Record; + return config; +} + +function initConvertInvalidError( + jsonConfigPath: string, + issues: string[], +): CliError { + return new CliError({ + code: "COMPUTE_CONFIG_INVALID", + domain: "app", + summary: `Invalid ${path.basename(jsonConfigPath)}`, + why: issues.join(" "), + fix: `Fix ${path.basename(jsonConfigPath)} and rerun the conversion.`, + where: jsonConfigPath, + meta: { configPath: jsonConfigPath, issues }, + exitCode: 2, + nextSteps: [], + }); +} + +/** Preview rows for a conversion; every value is sourced from the JSON file. */ +function conversionSettings(loaded: LoadedComputeConfig): InitSettingRow[] { + const target = loaded.kind === "single" ? loaded.targets[0] : undefined; + if (!target) { + return []; + } + + const source = COMPUTE_CONFIG_JSON_FILENAME; + return [ + ...(target.name ? [{ key: "app", value: target.name, source }] : []), + ...(target.framework + ? [ + { + key: "framework", + value: frameworkByKey(target.framework).displayName, + source, + }, + ] + : []), + ...(target.entry ? [{ key: "entry", value: target.entry, source }] : []), + ...(target.httpPort !== null + ? [{ key: "http port", value: String(target.httpPort), source }] + : []), + ...(target.region ? [{ key: "region", value: target.region, source }] : []), + ]; +} + +/** + * App identity for the conversion result. Configs written by init pin all of + * name, framework, and httpPort; hand-written configs that omit any of them + * (or define multiple apps) report null instead of a partial identity. + */ +function conversionApp(loaded: LoadedComputeConfig): InitResult["app"] { + const target = loaded.kind === "single" ? loaded.targets[0] : undefined; + if (!target?.name || !target.framework || target.httpPort === null) { + return null; + } + + return { + name: target.name, + framework: target.framework, + httpPort: target.httpPort, + ...(target.entry ? { entry: target.entry } : {}), + ...(target.region ? { region: target.region } : {}), + }; } function initConfigExistsError(existingPath: string): CliError { diff --git a/packages/cli/src/presenters/init.ts b/packages/cli/src/presenters/init.ts index 492f34c..037b10f 100644 --- a/packages/cli/src/presenters/init.ts +++ b/packages/cli/src/presenters/init.ts @@ -1,3 +1,5 @@ +import { COMPUTE_CONFIG_JSON_FILENAME } from "@prisma/compute-sdk/config"; + import { resolvePrismaCliPackageCommandFormatterSync } from "../lib/agent/cli-command"; import type { CommandDescriptor } from "../shell/command-meta"; import type { CommandContext } from "../shell/runtime"; @@ -14,7 +16,13 @@ export function renderInit( context.runtime.cwd, ); const lines = [ - renderSummaryLine(ui, "success", `Wrote ${result.configPath}`), + renderSummaryLine( + ui, + "success", + result.converted + ? `Converted ${COMPUTE_CONFIG_JSON_FILENAME} to ${result.configPath}` + : `Wrote ${result.configPath}`, + ), ]; // Failed steps surface through the runner's success-warning rendering, so diff --git a/packages/cli/src/shell/command-meta.ts b/packages/cli/src/shell/command-meta.ts index 1c0f50a..069f213 100644 --- a/packages/cli/src/shell/command-meta.ts +++ b/packages/cli/src/shell/command-meta.ts @@ -47,11 +47,12 @@ const DESCRIPTORS: CommandDescriptor[] = [ { id: "init", path: ["prisma", "init"], - description: "Write a committed prisma.compute.ts for this app", + description: "Write a committed compute config for this app", examples: (runtime) => agentCommandExamples(runtime, [ ["init"], ["init", "--framework", "hono", "--entry", "src/index.ts"], + ["init", "--format", "json"], ["init", "--no-link"], ]), }, diff --git a/packages/cli/src/types/init.ts b/packages/cli/src/types/init.ts index 8ffe741..dad5e80 100644 --- a/packages/cli/src/types/init.ts +++ b/packages/cli/src/types/init.ts @@ -1,3 +1,6 @@ +/** Serialization the compute config was written in. */ +export type InitConfigFormat = "typescript" | "json"; + export interface InitSettingRow { key: string; value: string; @@ -35,14 +38,21 @@ export interface InitTypesState { export interface InitResult { configPath: string; + format: InitConfigFormat; + /** True when init converted an existing prisma.compute.json to TypeScript. */ + converted: boolean; directory: string; + /** + * App identity pinned by the written config. Null when a conversion + * transported a config that does not pin a single fully-resolved app. + */ app: { name: string; framework: string; httpPort: number; entry?: string; region?: string; - }; + } | null; settings: InitSettingRow[]; types: InitTypesState; link: InitLinkState; diff --git a/packages/cli/tests/init.test.ts b/packages/cli/tests/init.test.ts index d53a709..8e3725c 100644 --- a/packages/cli/tests/init.test.ts +++ b/packages/cli/tests/init.test.ts @@ -1,5 +1,9 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; +import { + COMPUTE_CONFIG_JSON_SCHEMA_URL, + loadComputeConfig, +} from "@prisma/compute-sdk/config"; import stripAnsi from "strip-ansi"; import { describe, expect, it } from "vitest"; @@ -31,6 +35,10 @@ async function readConfig(cwd: string): Promise { return readFile(path.join(cwd, "prisma.compute.ts"), "utf8"); } +async function readJsonConfig(cwd: string): Promise { + return readFile(path.join(cwd, "prisma.compute.json"), "utf8"); +} + describe("init", () => { it("writes a config for an explicit framework without auth or prompts", async () => { const cwd = await createTempCwd(); @@ -412,6 +420,620 @@ describe("init types install", () => { }); }); +describe("init config format", () => { + it("writes prisma.compute.json with --format json that round-trips through the loader", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writePackageJson(cwd, { name: "billing-api" }); + + const result = await executeCli({ + argv: [ + "init", + "--framework", + "hono", + "--entry", + "src/index.ts", + "--no-link", + "--format", + "json", + "--json", + ], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload).toMatchObject({ + ok: true, + command: "init", + result: { + configPath: "prisma.compute.json", + format: "json", + converted: false, + app: { + name: "billing-api", + framework: "hono", + entry: "src/index.ts", + }, + // The JSON format is dependency-free by design, so the types install + // step never runs and no install hint is emitted. + types: { + status: "skipped", + package: "@prisma/compute-sdk", + installCommand: null, + }, + }, + }); + expect(payload.nextSteps).toEqual([ + "npx -y @prisma/cli@latest app deploy", + "npx -y @prisma/cli@latest project link", + ]); + + const written = JSON.parse(await readJsonConfig(cwd)); + // No $schema until the schema URL actually resolves; the loader accepts + // it either way, so hand-added references keep working. + expect(written).not.toHaveProperty("$schema"); + expect(written.app).toMatchObject({ + name: "billing-api", + framework: "hono", + entry: "src/index.ts", + }); + + const loaded = await loadComputeConfig(cwd); + expect(loaded.isOk()).toBe(true); + expect(loaded.isOk() && loaded.value?.targets[0]).toMatchObject({ + name: "billing-api", + framework: "hono", + entry: "src/index.ts", + httpPort: payload.result.app.httpPort, + }); + await expect(readConfig(cwd)).rejects.toMatchObject({ code: "ENOENT" }); + }); + + it("writes prisma.compute.ts with an explicit --format ts", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writePackageJson(cwd, { name: "api" }); + + const result = await executeCli({ + argv: [ + "init", + "--framework", + "hono", + "--no-link", + "--format", + "ts", + "--json", + ], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload.result).toMatchObject({ + configPath: "prisma.compute.ts", + format: "typescript", + converted: false, + }); + await expect(readConfig(cwd)).resolves.toContain("defineComputeConfig"); + }); + + it("rejects --install with --format json as a usage error", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writePackageJson(cwd, { name: "api" }); + + const result = await executeCli({ + argv: [ + "init", + "--framework", + "hono", + "--install", + "--format", + "json", + "--json", + ], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(2); + expect(payload.error.code).toBe("USAGE_ERROR"); + await expect(readJsonConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("rejects unknown --format values as a usage error", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + + const result = await executeCli({ + argv: ["init", "--framework", "hono", "--format", "yaml", "--json"], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(2); + expect(payload.error.code).toBe("USAGE_ERROR"); + }); + + it("rejects --format json for the custom framework and writes nothing", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + + const result = await executeCli({ + argv: [ + "init", + "--framework", + "custom", + "--no-link", + "--format", + "json", + "--json", + ], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(2); + expect(payload.error.code).toBe("USAGE_ERROR"); + expect(payload.error.summary).toContain("TypeScript config format"); + await expect(readJsonConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("fails with INIT_CONVERT_UNSUPPORTED when a TypeScript config exists and --format json is passed", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writeFile( + path.join(cwd, "prisma.compute.ts"), + 'export default { app: { framework: "hono" } };\n', + ); + + const result = await executeCli({ + argv: ["init", "--framework", "hono", "--format", "json", "--json"], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(1); + expect(payload.error.code).toBe("INIT_CONVERT_UNSUPPORTED"); + expect(payload.error.fix).toContain("rewrite it by hand"); + expect(payload.error.meta.existingConfigPath).toContain( + "prisma.compute.ts", + ); + await expect(readJsonConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("refuses plain init when prisma.compute.json exists", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify({ app: { framework: "hono" } })}\n`, + ); + + // Conversion must be explicit via --format ts; plain init and a repeated + // --format json both refuse. + for (const argv of [ + ["init", "--framework", "hono", "--json"], + ["init", "--framework", "hono", "--format", "json", "--json"], + ]) { + const result = await executeCli({ argv, cwd, stateDir, fixturePath }); + const payload = JSON.parse(result.stdout); + expect(result.exitCode).toBe(1); + expect(payload.error.code).toBe("INIT_CONFIG_EXISTS"); + expect(payload.error.meta.existingConfigPath).toContain( + "prisma.compute.json", + ); + } + }); + + it("converts prisma.compute.json to prisma.compute.ts with --format ts", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify( + { + $schema: COMPUTE_CONFIG_JSON_SCHEMA_URL, + app: { + name: "billing-api", + framework: "hono", + entry: "src/index.ts", + httpPort: 8080, + region: "us-east-1", + }, + }, + null, + 2, + )}\n`, + ); + + const result = await executeCli({ + argv: ["init", "--format", "ts", "--json"], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload.result).toMatchObject({ + configPath: "prisma.compute.ts", + format: "typescript", + converted: true, + app: { + name: "billing-api", + framework: "hono", + entry: "src/index.ts", + httpPort: 8080, + region: "us-east-1", + }, + }); + expect(payload.result.settings).toContainEqual({ + key: "framework", + value: "Hono", + source: "prisma.compute.json", + }); + + const config = await readConfig(cwd); + expect(config).toContain("defineComputeConfig"); + expect(config).toContain('name: "billing-api"'); + expect(config).toContain('framework: "hono"'); + expect(config).toContain('entry: "src/index.ts"'); + expect(config).toContain("httpPort: 8080"); + expect(config).toContain('region: "us-east-1"'); + expect(config).not.toContain("$schema"); + await expect(readJsonConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + + const loaded = await loadComputeConfig(cwd); + expect(loaded.isOk()).toBe(true); + expect(loaded.isOk() && loaded.value?.targets[0]).toMatchObject({ + name: "billing-api", + framework: "hono", + entry: "src/index.ts", + httpPort: 8080, + region: "us-east-1", + }); + }); + + it("preserves env, build, root, and the project region through conversion", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify({ + region: "eu-central-1", + app: { + name: "web", + framework: "nextjs", + root: "apps/web", + httpPort: 3000, + env: { file: ".env.production", vars: { NODE_ENV: "production" } }, + build: { command: "pnpm build", outputDirectory: ".next" }, + }, + })}\n`, + ); + + const result = await executeCli({ + argv: ["init", "--format", "ts", "--json"], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload.result.converted).toBe(true); + + const config = await readConfig(cwd); + expect(config).toContain('region: "eu-central-1"'); + expect(config).toContain('root: "apps/web"'); + expect(config).toContain('file: ".env.production"'); + expect(config).toContain('NODE_ENV: "production"'); + expect(config).toContain('command: "pnpm build"'); + expect(config).toContain('outputDirectory: ".next"'); + + const loaded = await loadComputeConfig(cwd); + expect(loaded.isOk() && loaded.value?.targets[0]).toMatchObject({ + name: "web", + framework: "nextjs", + root: "apps/web", + region: "eu-central-1", + envInputs: [".env.production", "NODE_ENV=production"], + build: { + command: "pnpm build", + outputDirectory: ".next", + entrypoint: undefined, + }, + }); + }); + + it("converts a multi-app config, including a null build command", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify({ + apps: { + web: { + framework: "nextjs", + root: "apps/web", + build: { command: null }, + }, + api: { + framework: "hono", + root: "apps/api", + entry: "src/index.ts", + httpPort: 8080, + }, + }, + })}\n`, + ); + + const result = await executeCli({ + argv: ["init", "--format", "ts", "--json"], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload.result.converted).toBe(true); + // Multi-app configs carry no single app identity. + expect(payload.result.app).toBeNull(); + + const config = await readConfig(cwd); + expect(config).toContain("command: null"); + await expect(readJsonConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + + const loaded = await loadComputeConfig(cwd); + expect(loaded.isOk() && loaded.value?.kind).toBe("multi"); + expect(loaded.isOk() && loaded.value?.targets).toEqual([ + expect.objectContaining({ + key: "web", + framework: "nextjs", + build: expect.objectContaining({ command: null }), + }), + expect.objectContaining({ + key: "api", + framework: "hono", + entry: "src/index.ts", + httpPort: 8080, + }), + ]); + }); + + it("rejects resolution flags during conversion instead of ignoring them", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + const jsonSource = `${JSON.stringify({ + app: { name: "api", framework: "hono", httpPort: 8080 }, + })}\n`; + await writeFile(path.join(cwd, "prisma.compute.json"), jsonSource); + + const result = await executeCli({ + argv: [ + "init", + "--format", + "ts", + "--framework", + "nextjs", + "--http-port", + "3000", + "--json", + ], + cwd, + stateDir, + fixturePath, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(2); + expect(payload.error.code).toBe("USAGE_ERROR"); + expect(payload.error.summary).toContain("--framework"); + expect(payload.error.summary).toContain("--http-port"); + + // Nothing on disk changed: no TS config, JSON untouched. + await expect(readConfig(cwd)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readJsonConfig(cwd)).toBe(jsonSource); + }); + + it("runs the types install step when converting with --install", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writePackageJson(cwd, { name: "api" }); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify({ app: { framework: "hono", httpPort: 8080 } })}\n`, + ); + + const result = await executeCli({ + argv: ["init", "--format", "ts", "--install", "--json"], + cwd, + stateDir, + fixturePath, + env: { + PRISMA_CLI_INIT_INSTALL_COMMAND: JSON.stringify([ + "node", + "-e", + "process.exit(0)", + ]), + }, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload.result.converted).toBe(true); + // A hand-written config without a pinned name reports no app identity. + expect(payload.result.app).toBeNull(); + expect(payload.result.types.status).toBe("installed"); + }); + + it("honors link flags when converting, like fresh init", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writePackageJson(cwd, { name: "api" }); + await login(cwd, stateDir); + const jsonConfig = `${JSON.stringify({ app: { framework: "hono", httpPort: 8080 } })}\n`; + await writeFile(path.join(cwd, "prisma.compute.json"), jsonConfig); + + const linked = await executeCli({ + argv: [ + "init", + "--format", + "ts", + "--project", + "proj_123", + "--no-install", + "--json", + ], + cwd, + stateDir, + fixturePath, + }); + const linkedPayload = JSON.parse(linked.stdout); + + expect(linked.exitCode).toBe(0); + expect(linkedPayload.result.converted).toBe(true); + expect(linkedPayload.result.link).toMatchObject({ + status: "linked", + project: { id: "proj_123" }, + }); + // Linked conversions do not suggest linking again. + expect(linkedPayload.nextSteps).not.toContainEqual( + expect.stringContaining("project link"), + ); + + // --no-link stays an explicit skip. + const cwd2 = await createTempCwd(); + await writeFile(path.join(cwd2, "prisma.compute.json"), jsonConfig); + const skipped = await executeCli({ + argv: ["init", "--format", "ts", "--no-link", "--no-install", "--json"], + cwd: cwd2, + stateDir, + fixturePath, + }); + const skippedPayload = JSON.parse(skipped.stdout); + expect(skipped.exitCode).toBe(0); + expect(skippedPayload.result.link.status).toBe("skipped"); + }); + + it("runs conversion side effects in the config directory, not the invocation directory", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await mkdir(path.join(cwd, ".git"), { recursive: true }); + await writePackageJson(cwd, { name: "root-app" }); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify({ app: { framework: "hono", httpPort: 8080 } })}\n`, + ); + const nested = path.join(cwd, "apps", "api"); + await mkdir(nested, { recursive: true }); + await writePackageJson(nested, { name: "api" }); + await login(cwd, stateDir); + + const result = await executeCli({ + argv: [ + "init", + "--format", + "ts", + "--install", + "--project", + "proj_123", + "--json", + ], + cwd: nested, + stateDir, + fixturePath, + env: { + // The fake installer records its working directory on disk. + PRISMA_CLI_INIT_INSTALL_COMMAND: JSON.stringify([ + "node", + "-e", + "require('fs').writeFileSync('install-cwd.txt','ok')", + ]), + }, + }); + const payload = JSON.parse(result.stdout); + + expect(result.exitCode).toBe(0); + expect(payload.result.configPath).toBe( + path.join("..", "..", "prisma.compute.ts"), + ); + expect(payload.result.types.status).toBe("installed"); + expect(payload.result.link).toMatchObject({ + status: "linked", + project: { id: "proj_123" }, + }); + + // The install ran in the config's directory, and the project pin was + // written there too; the nested invocation directory got neither. + await expect( + readFile(path.join(cwd, "install-cwd.txt"), "utf8"), + ).resolves.toBe("ok"); + await expect( + readFile(path.join(nested, "install-cwd.txt"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + readFile(path.join(cwd, ".prisma/local.json"), "utf8"), + ).resolves.toContain("proj_123"); + await expect( + readFile(path.join(nested, ".prisma/local.json"), "utf8"), + ).rejects.toMatchObject({ code: "ENOENT" }); + + // The conversion itself happened at the config's home. + await expect(readConfig(cwd)).resolves.toContain("defineComputeConfig"); + await expect(readJsonConfig(cwd)).rejects.toMatchObject({ + code: "ENOENT", + }); + }); + + it("prints the human conversion summary", async () => { + const cwd = await createTempCwd(); + const stateDir = path.join(cwd, ".state"); + await writeFile( + path.join(cwd, "prisma.compute.json"), + `${JSON.stringify({ app: { framework: "hono", httpPort: 8080 } })}\n`, + ); + + const result = await executeCli({ + argv: ["init", "--format", "ts", "--no-install"], + cwd, + stateDir, + fixturePath, + }); + const stderr = stripAnsi(result.stderr); + + expect(result.exitCode).toBe(0); + expect(stderr).toContain( + "✔ Converted prisma.compute.json to prisma.compute.ts", + ); + }); +}); + describe("init edge cases", () => { it("rejects --entry for frameworks that derive their entrypoint", async () => { const cwd = await createTempCwd(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66aba11..c85a1d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 '@prisma/compute-sdk': - specifier: 0.31.0 - version: 0.31.0(@prisma/management-api-sdk@1.46.0)(rollup@4.62.2) + specifier: 0.33.0 + version: 0.33.0(@prisma/management-api-sdk@1.46.0)(rollup@4.62.2) '@prisma/credentials-store': specifier: ^7.8.0 version: 7.8.0 @@ -551,8 +551,8 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} - '@prisma/compute-sdk@0.31.0': - resolution: {integrity: sha512-V2av8c6qVrWqYug6qL82qBnyep+mFq0FP3MANSGalISoSUHwiQbXIWVoT7HnM6kaXYa6d1aU4Sb+1iR8GnQ6DA==} + '@prisma/compute-sdk@0.33.0': + resolution: {integrity: sha512-lk+WOfh0km/HbqcOsFtGYUTkQjTzESZcR8q7FPX1xtuyp7ccoFNTGKhDqM5VJw6GLeQgCeZlkdAZXBtHRfN7Vw==} engines: {node: '>=18.0.0'} peerDependencies: '@prisma/management-api-sdk': ^1.44.0 @@ -934,8 +934,8 @@ packages: bare-abort-controller: optional: true - bare-fs@4.7.2: - resolution: {integrity: sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==} + bare-fs@4.7.3: + resolution: {integrity: sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==} engines: {bare: '>=1.16.0'} peerDependencies: bare-buffer: '*' @@ -943,8 +943,8 @@ packages: bare-buffer: optional: true - bare-os@3.9.1: - resolution: {integrity: sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==} + bare-os@3.9.3: + resolution: {integrity: sha512-fF4Q7QsyKVF5Rj0qvI8BgUNjqzC2JvQlpTaPLjVJVxYVUX5Zr9un+y3w1HmA4nNKdFmRBT8z/WmrjvXzXVerKQ==} engines: {bare: '>=1.14.0'} bare-path@3.0.1: @@ -976,8 +976,8 @@ packages: birpc@4.0.0: resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} - brace-expansion@5.0.6: - resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + brace-expansion@5.0.7: + resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==} engines: {node: 18 || 20 || >=22} bundle-name@4.1.0: @@ -1356,11 +1356,6 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} - hasBin: true - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1407,8 +1402,8 @@ packages: tar-stream@3.2.0: resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} - tar@7.5.16: - resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} + tar@7.5.19: + resolution: {integrity: sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==} engines: {node: '>=18'} teex@1.0.1: @@ -1922,8 +1917,8 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0 nopt: 8.1.0 - semver: 7.8.5 - tar: 7.5.16 + semver: 7.8.1 + tar: 7.5.19 transitivePeerDependencies: - encoding - supports-color @@ -1937,7 +1932,7 @@ snapshots: '@oxc-project/types@0.127.0': {} - '@prisma/compute-sdk@0.31.0(@prisma/management-api-sdk@1.46.0)(rollup@4.62.2)': + '@prisma/compute-sdk@0.33.0(@prisma/management-api-sdk@1.46.0)(rollup@4.62.2)': dependencies: '@prisma/management-api-sdk': 1.46.0 '@vercel/nft': 1.10.2(rollup@4.62.2) @@ -2221,7 +2216,7 @@ snapshots: bare-events@2.9.1: {} - bare-fs@4.7.2: + bare-fs@4.7.3: dependencies: bare-events: 2.9.1 bare-path: 3.0.1 @@ -2232,11 +2227,11 @@ snapshots: - bare-abort-controller - react-native-b4a - bare-os@3.9.1: {} + bare-os@3.9.3: {} bare-path@3.0.1: dependencies: - bare-os: 3.9.1 + bare-os: 3.9.3 bare-stream@2.13.3(bare-events@2.9.1): dependencies: @@ -2260,7 +2255,7 @@ snapshots: birpc@4.0.0: {} - brace-expansion@5.0.6: + brace-expansion@5.0.7: dependencies: balanced-match: 4.0.4 @@ -2495,7 +2490,7 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.6 + brace-expansion: 5.0.7 minipass@7.1.3: {} @@ -2654,8 +2649,6 @@ snapshots: semver@7.8.1: {} - semver@7.8.5: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2697,7 +2690,7 @@ snapshots: tar-stream@3.2.0: dependencies: b4a: 1.8.1 - bare-fs: 4.7.2 + bare-fs: 4.7.3 fast-fifo: 1.3.2 streamx: 2.28.0 transitivePeerDependencies: @@ -2705,7 +2698,7 @@ snapshots: - bare-buffer - react-native-b4a - tar@7.5.16: + tar@7.5.19: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0