Compare commits

..
6 Commits
14 changed files with 147 additions and 179 deletions
+9 -113
View File
@@ -1,121 +1,17 @@
# AGENTS.md # Agents.md
## Overview ## Apps
Privacy-first video editor, with a focus on simplicity and ease of use. - Web
- Desktop
## Lib vs Utils ## Rust
- `lib/` - domain logic (specific to this app) Shared code between apps live in `rust/`, not in `packages/`
- `utils/` - small helper utils (generic, could be copy-pasted into any other app)
## Core Editor System ## Web
The editor uses a **singleton EditorCore** that manages all editor state through specialized managers. ### React
### Architecture - Read components before using them. They may already apply classes, which affects what you need to pass and how to override them.
```
EditorCore (singleton)
├── playback: PlaybackManager
├── timeline: TimelineManager
├── scene: SceneManager
├── project: ProjectManager
├── media: MediaManager
└── renderer: RendererManager
```
### When to Use What
#### In React Components
**Always use the `useEditor()` hook:**
```typescript
import { useEditor } from '@/hooks/use-editor';
function MyComponent() {
const editor = useEditor();
const tracks = editor.timeline.getTracks();
// Call methods
editor.timeline.addTrack({ type: 'media' });
// Display data (auto re-renders on changes)
return <div>{tracks.length} tracks</div>;
}
```
The hook:
- Returns the singleton instance
- Subscribes to all manager changes
- Automatically re-renders when state changes
#### Outside React Components
**Use `EditorCore.getInstance()` directly:**
```typescript
// In utilities, event handlers, or non-React code
import { EditorCore } from "@/core";
const editor = EditorCore.getInstance();
await editor.export({ format: "mp4", quality: "high" });
```
## Actions System
Actions are the trigger layer for user-initiated operations. The single source of truth is `@/lib/actions/definitions.ts`.
**To add a new action:**
1. Add it to `ACTIONS` in `@/lib/actions/definitions.ts`:
```typescript
export const ACTIONS = {
"my-action": {
description: "What the action does",
category: "editing",
defaultShortcuts: ["ctrl+m"],
},
// ...
};
```
2. Add handler in `@/hooks/use-editor-actions.ts`:
```typescript
useActionHandler(
"my-action",
() => {
// implementation
},
undefined,
);
```
**In components, use `invokeAction()` for user-triggered operations:**
```typescript
import { invokeAction } from '@/lib/actions';
// Good - uses action system
const handleSplit = () => invokeAction("split-selected");
// Avoid - bypasses UX layer (toasts, validation feedback)
const handleSplit = () => editor.timeline.splitElements({ ... });
```
Direct `editor.xxx()` calls are for internal use (commands, tests, complex multi-step operations).
## Commands System
Commands handle undo/redo. They live in `@/lib/commands/` organized by domain (timeline, media, scene).
Each command extends `Command` from `@/lib/commands/base-command` and implements:
- `execute()` - saves current state, then does the mutation
- `undo()` - restores the saved state
Actions and commands work together: actions are "what triggered this", commands are "how to do it (and undo it)".
+3
View File
@@ -0,0 +1,3 @@
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig();
+1 -1
View File
@@ -24,7 +24,7 @@
"@huggingface/transformers": "^3.8.1", "@huggingface/transformers": "^3.8.1",
"@opencut/env": "workspace:*", "@opencut/env": "workspace:*",
"@opencut/ui": "workspace:*", "@opencut/ui": "workspace:*",
"@opennextjs/cloudflare": "^1.17.3", "@opennextjs/cloudflare": "^1.18.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
@@ -42,7 +42,6 @@ export function SoundsView() {
<div className="px-3 pt-4 pb-0"> <div className="px-3 pt-4 pb-0">
<TabsList> <TabsList>
<TabsTrigger value="sound-effects">Sound effects</TabsTrigger> <TabsTrigger value="sound-effects">Sound effects</TabsTrigger>
<TabsTrigger value="songs">Songs</TabsTrigger>
<TabsTrigger value="saved">Saved</TabsTrigger> <TabsTrigger value="saved">Saved</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@@ -59,12 +58,6 @@ export function SoundsView() {
> >
<SavedSoundsView /> <SavedSoundsView />
</TabsContent> </TabsContent>
<TabsContent
value="songs"
className="mt-0 flex min-h-0 flex-1 flex-col p-5 pt-0"
>
<SongsView />
</TabsContent>
</Tabs> </Tabs>
</div> </div>
); );
@@ -485,10 +478,6 @@ function SavedSoundsView() {
); );
} }
function SongsView() {
return <div>Songs</div>;
}
interface AudioItemProps { interface AudioItemProps {
sound: SoundEffect; sound: SoundEffect;
isPlaying: boolean; isPlaying: boolean;
@@ -1,7 +1,11 @@
"use client"; "use client";
import type { ParamDefinition, NumberParamDefinition } from "@/lib/params"; import type { ParamDefinition, NumberParamDefinition } from "@/lib/params";
import { formatNumberForDisplay, snapToStep } from "@/utils/math"; import {
formatNumberForDisplay,
getFractionDigitsForStep,
snapToStep,
} from "@/utils/math";
import { SectionField } from "@/components/section"; import { SectionField } from "@/components/section";
import { NumberField } from "@/components/ui/number-field"; import { NumberField } from "@/components/ui/number-field";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
@@ -156,7 +160,7 @@ function NumberParamField({
onPreview(clamped / displayMultiplier); onPreview(clamped / displayMultiplier);
}; };
const maxFractionDigits = step >= 1 ? 0 : Math.round(-Math.log10(step)); const maxFractionDigits = getFractionDigitsForStep({ step });
const draft = usePropertyDraft({ const draft = usePropertyDraft({
displayValue: formatNumberForDisplay({ displayValue: formatNumberForDisplay({
@@ -1,7 +1,13 @@
import { NumberField } from "@/components/ui/number-field"; import { NumberField } from "@/components/ui/number-field";
import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants"; import { VOLUME_DB_MAX, VOLUME_DB_MIN } from "@/lib/timeline/audio-constants";
import { DEFAULTS } from "@/lib/timeline/defaults"; import { DEFAULTS } from "@/lib/timeline/defaults";
import { clamp, isNearlyEqual } from "@/utils/math"; import {
clamp,
formatNumberForDisplay,
getFractionDigitsForStep,
isNearlyEqual,
snapToStep,
} from "@/utils/math";
import type { AudioElement, VideoElement } from "@/lib/timeline"; import type { AudioElement, VideoElement } from "@/lib/timeline";
import { resolveNumberAtTime } from "@/lib/animation"; import { resolveNumberAtTime } from "@/lib/animation";
import { useElementPlayhead } from "../hooks/use-element-playhead"; import { useElementPlayhead } from "../hooks/use-element-playhead";
@@ -18,6 +24,9 @@ import {
SectionTitle, SectionTitle,
} from "@/components/section"; } from "@/components/section";
const VOLUME_STEP = 0.1;
const VOLUME_FRACTION_DIGITS = getFractionDigitsForStep({ step: VOLUME_STEP });
export function AudioTab({ export function AudioTab({
element, element,
trackId, trackId,
@@ -42,17 +51,24 @@ export function AudioTab({
propertyPath: "volume", propertyPath: "volume",
localTime, localTime,
isPlayheadWithinElementRange, isPlayheadWithinElementRange,
displayValue: resolvedVolume.toFixed(1), displayValue: formatNumberForDisplay({
value: resolvedVolume,
fractionDigits: VOLUME_FRACTION_DIGITS,
}),
parse: (input) => { parse: (input) => {
const parsed = parseFloat(input); const parsed = parseFloat(input);
if (Number.isNaN(parsed)) { if (Number.isNaN(parsed)) {
return null; return null;
} }
return clamp({ value: parsed, min: VOLUME_DB_MIN, max: VOLUME_DB_MAX }); return clamp({
value: snapToStep({ value: parsed, step: VOLUME_STEP }),
min: VOLUME_DB_MIN,
max: VOLUME_DB_MAX,
});
}, },
valueAtPlayhead: resolvedVolume, valueAtPlayhead: resolvedVolume,
step: 0.1, step: VOLUME_STEP,
buildBaseUpdates: ({ value }) => ({ buildBaseUpdates: ({ value }) => ({
volume: value, volume: value,
}), }),
@@ -38,7 +38,12 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { clamp, formatNumberForDisplay, snapToStep } from "@/utils/math"; import {
clamp,
formatNumberForDisplay,
getFractionDigitsForStep,
snapToStep,
} from "@/utils/math";
import { import {
Section, Section,
SectionContent, SectionContent,
@@ -599,7 +604,7 @@ function MaskNumberField({
const max = isPercent ? 100 : param.max; const max = isPercent ? 100 : param.max;
const step = isPercent ? 1 : param.step; const step = isPercent ? 1 : param.step;
const displayValue = value * displayMultiplier; const displayValue = value * displayMultiplier;
const maxFractionDigits = step >= 1 ? 0 : Math.round(-Math.log10(step)); const maxFractionDigits = getFractionDigitsForStep({ step });
const clampDisplay = (nextDisplayValue: number) => const clampDisplay = (nextDisplayValue: number) =>
max !== undefined max !== undefined
@@ -22,16 +22,28 @@ import {
SectionTitle, SectionTitle,
} from "@/components/section"; } from "@/components/section";
import { usePropertyDraft } from "../hooks/use-property-draft"; import { usePropertyDraft } from "../hooks/use-property-draft";
import {
formatNumberForDisplay,
getFractionDigitsForStep,
snapToStep,
} from "@/utils/math";
const SPEED_STEP = 0.01;
const SPEED_FRACTION_DIGITS = getFractionDigitsForStep({ step: SPEED_STEP });
function rateToDisplay({ rate }: { rate: number }): string { function rateToDisplay({ rate }: { rate: number }): string {
return rate.toFixed(2); return formatNumberForDisplay({
value: rate,
fractionDigits: SPEED_FRACTION_DIGITS,
});
} }
function parseSpeedInput({ input }: { input: string }): number | null { function parseSpeedInput({ input }: { input: string }): number | null {
const parsed = parseFloat(input); const parsed = parseFloat(input);
if (Number.isNaN(parsed)) return null; if (Number.isNaN(parsed)) return null;
const rounded = Math.round(parsed * 100) / 100; return clampRetimeRate({
return clampRetimeRate({ rate: rounded }); rate: snapToStep({ value: parsed, step: SPEED_STEP }),
});
} }
function buildRetime({ function buildRetime({
@@ -56,24 +68,21 @@ export function SpeedTab({
const rate = clampRetimeRate({ const rate = clampRetimeRate({
rate: element.retime?.rate ?? DEFAULT_RETIME_RATE, rate: element.retime?.rate ?? DEFAULT_RETIME_RATE,
}); });
const maintainPitchAvailable = canMaintainPitch({ rate }); const isPitchPreserveAvailable = canMaintainPitch({ rate });
const maintainPitch = element.retime?.maintainPitch ?? false; const maintainPitch = element.retime?.maintainPitch ?? false;
const pendingRateRef = useRef(rate); const pendingRateRef = useRef(rate);
const applyRetime = ({ const commitRetime = ({
rate: nextRate, rate: nextRate,
maintainPitch: nextMaintainPitch, maintainPitch: nextMaintainPitch,
pushHistory = true,
}: { }: {
rate: number; rate: number;
maintainPitch: boolean; maintainPitch: boolean;
pushHistory?: boolean;
}) => { }) => {
editor.timeline.updateElementRetime({ editor.timeline.updateElementRetime({
trackId, trackId,
elementId: element.id, elementId: element.id,
retime: buildRetime({ rate: nextRate, maintainPitch: nextMaintainPitch }), retime: buildRetime({ rate: nextRate, maintainPitch: nextMaintainPitch }),
pushHistory,
}); });
}; };
@@ -82,10 +91,20 @@ export function SpeedTab({
parse: (input) => parseSpeedInput({ input }), parse: (input) => parseSpeedInput({ input }),
onPreview: (nextRate) => { onPreview: (nextRate) => {
pendingRateRef.current = nextRate; pendingRateRef.current = nextRate;
applyRetime({ rate: nextRate, maintainPitch, pushHistory: false }); editor.timeline.previewElements({
updates: [
{
trackId,
elementId: element.id,
updates: {
retime: buildRetime({ rate: nextRate, maintainPitch }),
},
},
],
});
}, },
onCommit: () => { onCommit: () => {
applyRetime({ rate: pendingRateRef.current, maintainPitch }); commitRetime({ rate: pendingRateRef.current, maintainPitch });
}, },
}); });
@@ -115,20 +134,20 @@ export function SpeedTab({
onScrub={speedDraft.scrubTo} onScrub={speedDraft.scrubTo}
onScrubEnd={speedDraft.commitScrub} onScrubEnd={speedDraft.commitScrub}
onReset={() => onReset={() =>
applyRetime({ rate: DEFAULT_RETIME_RATE, maintainPitch }) commitRetime({ rate: DEFAULT_RETIME_RATE, maintainPitch })
} }
isDefault={rate === DEFAULT_RETIME_RATE} isDefault={rate === DEFAULT_RETIME_RATE}
/> />
</SectionField> </SectionField>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm">Change pitch</span> <span className="text-sm">Change pitch</span>
<Switch <Switch
checked={!maintainPitch} checked={!maintainPitch}
disabled={!maintainPitchAvailable} disabled={!isPitchPreserveAvailable}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
applyRetime({ rate, maintainPitch: !checked }) commitRetime({ rate, maintainPitch: !checked })
} }
/> />
</div> </div>
</SectionFields> </SectionFields>
</SectionContent> </SectionContent>
@@ -16,7 +16,7 @@ import {
import { ColorPicker } from "@/components/ui/color-picker"; import { ColorPicker } from "@/components/ui/color-picker";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { uppercase } from "@/utils/string"; import { uppercase } from "@/utils/string";
import { clamp } from "@/utils/math"; import { clamp, formatNumberForDisplay } from "@/utils/math";
import { useEditor } from "@/hooks/use-editor"; import { useEditor } from "@/hooks/use-editor";
import { DEFAULT_COLOR } from "@/constants/project-constants"; import { DEFAULT_COLOR } from "@/constants/project-constants";
import { import {
@@ -251,7 +251,10 @@ function SpacingSection({
}); });
const lineHeight = usePropertyDraft({ const lineHeight = usePropertyDraft({
displayValue: (element.lineHeight ?? DEFAULTS.text.lineHeight).toFixed(1), displayValue: formatNumberForDisplay({
value: element.lineHeight ?? DEFAULTS.text.lineHeight,
fractionDigits: 1,
}),
parse: (input) => { parse: (input) => {
const parsed = parseFloat(input); const parsed = parseFloat(input);
return Number.isNaN(parsed) return Number.isNaN(parsed)
@@ -27,7 +27,7 @@ import { ColorPicker } from "@/components/ui/color-picker";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { uppercase } from "@/utils/string"; import { uppercase } from "@/utils/string";
import { clamp } from "@/utils/math"; import { clamp, formatNumberForDisplay } from "@/utils/math";
import { import {
timelineTimeToPixels, timelineTimeToPixels,
timelineTimeToSnappedPixels, timelineTimeToSnappedPixels,
@@ -188,7 +188,7 @@ function TimelineBookmark({
left: `${bookmarkLeft}px`, left: `${bookmarkLeft}px`,
width: `${bookmarkWidth}px`, width: `${bookmarkWidth}px`,
}} }}
aria-label={`Bookmark at ${time.toFixed(1)}s`} aria-label={`Bookmark at ${formatNumberForDisplay({ value: time, fractionDigits: 1 })}s`}
type="button" type="button"
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onClick={handleClick} onClick={handleClick}
@@ -1,18 +1,18 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
export async function proxy() { export async function middleware() {
return NextResponse.next(); return NextResponse.next();
} }
export const config = { export const config = {
matcher: [ matcher: [
/* /*
* Match all request paths except for the ones starting with: * Match all request paths except for the ones starting with:
* - api (API routes) * - api (API routes)
* - _next/static (static files) * - _next/static (static files)
* - _next/image (image optimization files) * - _next/image (image optimization files)
* - favicon.ico (favicon file) * - favicon.ico (favicon file)
*/ */
"/((?!api|_next/static|_next/image|favicon.ico).*)", "/((?!api|_next/static|_next/image|favicon.ico).*)",
], ],
}; };
+3 -1
View File
@@ -1,3 +1,5 @@
import { formatNumberForDisplay } from "@/utils/math";
const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; const BYTE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const;
export const STORAGE_HEADROOM_RESERVE_BYTES = 50 * 1024 * 1024; export const STORAGE_HEADROOM_RESERVE_BYTES = 50 * 1024 * 1024;
@@ -37,7 +39,7 @@ export function formatStorageBytes({ bytes }: { bytes: number }): string {
} }
const precision = value >= 10 || unitIndex === 0 ? 0 : 1; const precision = value >= 10 || unitIndex === 0 ? 0 : 1;
return `${value.toFixed(precision)} ${BYTE_UNITS[unitIndex]}`; return `${formatNumberForDisplay({ value, fractionDigits: precision })} ${BYTE_UNITS[unitIndex]}`;
} }
export async function readStorageQuotaStatus(): Promise<StorageQuotaStatus> { export async function readStorageQuotaStatus(): Promise<StorageQuotaStatus> {
+33 -2
View File
@@ -22,7 +22,7 @@ export function clampRound({
return Math.round(clamp({ value, min, max })); return Math.round(clamp({ value, min, max }));
} }
function getFractionDigitsForStep({ step }: { step: number }): number { export function getFractionDigitsForStep({ step }: { step: number }): number {
const normalizedStep = step.toString().toLowerCase(); const normalizedStep = step.toString().toLowerCase();
if (normalizedStep.includes("e-")) { if (normalizedStep.includes("e-")) {
return Number(normalizedStep.split("e-")[1] ?? 0); return Number(normalizedStep.split("e-")[1] ?? 0);
@@ -59,12 +59,43 @@ export function isNearlyEqual({
export function formatNumberForDisplay({ export function formatNumberForDisplay({
value, value,
fractionDigits,
minFractionDigits = 0,
maxFractionDigits = 6, maxFractionDigits = 6,
}: { }: {
value: number; value: number;
fractionDigits?: number;
minFractionDigits?: number;
maxFractionDigits?: number; maxFractionDigits?: number;
}): string { }): string {
return Number(value.toFixed(maxFractionDigits)).toString(); const resolvedMaxFractionDigits = Math.max(
0,
fractionDigits ?? maxFractionDigits,
);
const resolvedMinFractionDigits = Math.min(
Math.max(0, fractionDigits ?? minFractionDigits),
resolvedMaxFractionDigits,
);
const fixedValue = value.toFixed(resolvedMaxFractionDigits);
if (resolvedMaxFractionDigits === 0) {
return Number(fixedValue) === 0 ? "0" : fixedValue;
}
const [integerPart, fractionPart = ""] = fixedValue.split(".");
const normalizedIntegerPart = Number(fixedValue) === 0 ? "0" : integerPart;
let trimmedFractionPart = fractionPart;
while (
trimmedFractionPart.length > resolvedMinFractionDigits &&
trimmedFractionPart.endsWith("0")
) {
trimmedFractionPart = trimmedFractionPart.slice(0, -1);
}
return trimmedFractionPart
? `${normalizedIntegerPart}.${trimmedFractionPart}`
: normalizedIntegerPart;
} }
export function evaluateMathExpression({ export function evaluateMathExpression({
+2 -2
View File
@@ -26,7 +26,7 @@
"@huggingface/transformers": "^3.8.1", "@huggingface/transformers": "^3.8.1",
"@opencut/env": "workspace:*", "@opencut/env": "workspace:*",
"@opencut/ui": "workspace:*", "@opencut/ui": "workspace:*",
"@opennextjs/cloudflare": "^1.17.3", "@opennextjs/cloudflare": "^1.18.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
@@ -517,7 +517,7 @@
"@opennextjs/aws": ["@opennextjs/aws@3.9.16", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": "~15.0.8 || ~15.1.12 || ~15.2.9 || ~15.3.9 || ~15.4.11 || ~15.5.10 || ~16.0.11 || ^16.1.5" }, "bin": { "open-next": "dist/index.js" } }, "sha512-jQQStCysIllNCPqz5W2KSguXpr+ETlOcD8SyNu+h9zwpRVYk4uEPQge+ErG3avI5xsT8vKA7EGLYG59dhj/B6Q=="], "@opennextjs/aws": ["@opennextjs/aws@3.9.16", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@aws-sdk/client-cloudfront": "3.984.0", "@aws-sdk/client-dynamodb": "3.984.0", "@aws-sdk/client-lambda": "3.984.0", "@aws-sdk/client-s3": "3.984.0", "@aws-sdk/client-sqs": "3.984.0", "@node-minify/core": "^8.0.6", "@node-minify/terser": "^8.0.6", "@tsconfig/node18": "^1.0.3", "aws4fetch": "^1.0.20", "chalk": "^5.6.2", "cookie": "^1.0.2", "esbuild": "0.25.4", "express": "^5.1.0", "path-to-regexp": "^6.3.0", "urlpattern-polyfill": "^10.1.0", "yaml": "^2.8.1" }, "peerDependencies": { "next": "~15.0.8 || ~15.1.12 || ~15.2.9 || ~15.3.9 || ~15.4.11 || ~15.5.10 || ~16.0.11 || ^16.1.5" }, "bin": { "open-next": "dist/index.js" } }, "sha512-jQQStCysIllNCPqz5W2KSguXpr+ETlOcD8SyNu+h9zwpRVYk4uEPQge+ErG3avI5xsT8vKA7EGLYG59dhj/B6Q=="],
"@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.17.3", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.16", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "~15.0.8 || ~15.1.12 || ~15.2.9 || ~15.3.9 || ~15.4.11 || ~15.5.10 || ~16.0.11 || ^16.1.5", "wrangler": "^4.65.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-BQT8R/CEqrZS2sYydJ6uqv3U+yZAxbTeDUfgqQIlOnhu3wDPsB/QuuUBBseWOZiOdZ8gLegK6F2SLKia74Nv6g=="], "@opennextjs/cloudflare": ["@opennextjs/cloudflare@1.18.0", "", { "dependencies": { "@ast-grep/napi": "^0.40.5", "@dotenvx/dotenvx": "1.31.0", "@opennextjs/aws": "3.9.16", "cloudflare": "^4.4.1", "comment-json": "^4.5.1", "enquirer": "^2.4.1", "glob": "^12.0.0", "ts-tqdm": "^0.8.6", "yargs": "^18.0.0" }, "peerDependencies": { "next": "~15.0.8 || ~15.1.12 || ~15.2.9 || ~15.3.9 || ~15.4.11 || ~15.5.10 || ~16.0.11 || ^16.1.5", "wrangler": "^4.65.0" }, "bin": { "opennextjs-cloudflare": "dist/cli/index.js" } }, "sha512-JM236YHnKzroFAZqst1t28ZGOShvnkVUDtjrp7TJ/W2P3RLo4b6npJ8VEXOn6frs6lsUfR5rvsKYLYb7h1GIJQ=="],
"@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],