@codewithdan/zingit 0.16.0 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +13 -13
- package/CHANGELOG.md +28 -0
- package/README.md +17 -17
- package/bin/cli.js +2 -2
- package/client/dist/zingit-client.js +46 -46
- package/package.json +6 -4
- package/server/dist/agents/base.d.ts +2 -2
- package/server/dist/agents/base.js +13 -13
- package/server/dist/agents/claude.js +1 -1
- package/server/dist/agents/codex.js +1 -1
- package/server/dist/agents/copilot.js +1 -1
- package/server/dist/handlers/messageHandlers.js +9 -9
- package/server/dist/services/git-manager.d.ts +5 -5
- package/server/dist/services/git-manager.js +10 -10
- package/server/dist/services/index.d.ts +1 -1
- package/server/dist/types.d.ts +2 -2
- package/server/dist/validation/payload.d.ts +1 -1
- package/server/dist/validation/payload.js +25 -25
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codewithdan/zingit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI-powered UI
|
|
3
|
+
"version": "0.17.1",
|
|
4
|
+
"description": "AI-powered UI marker tool - point, mark, and let AI fix it",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"client/dist",
|
|
16
16
|
"README.md",
|
|
17
17
|
"AGENTS.md",
|
|
18
|
+
"CHANGELOG.md",
|
|
18
19
|
"LICENSE"
|
|
19
20
|
],
|
|
20
21
|
"main": "client/dist/zingit-client.js",
|
|
@@ -29,11 +30,11 @@
|
|
|
29
30
|
"test": "cd client && npm run test",
|
|
30
31
|
"test:watch": "cd client && npm run test:watch",
|
|
31
32
|
"test:ui": "cd client && npm run test:ui",
|
|
32
|
-
"release": "
|
|
33
|
+
"release": "standard-version && npm run build && npm publish --access public"
|
|
33
34
|
},
|
|
34
35
|
"keywords": [
|
|
35
36
|
"ui",
|
|
36
|
-
"
|
|
37
|
+
"marker",
|
|
37
38
|
"ai",
|
|
38
39
|
"agent",
|
|
39
40
|
"claude",
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"@types/ws": "^8.18.1",
|
|
70
71
|
"concurrently": "^9.1.2",
|
|
71
72
|
"gh-pages": "^6.3.0",
|
|
73
|
+
"standard-version": "^9.5.0",
|
|
72
74
|
"tsx": "^4.21.0",
|
|
73
75
|
"typescript": "^5.9.3"
|
|
74
76
|
}
|
|
@@ -11,8 +11,8 @@ export declare abstract class BaseAgent implements Agent {
|
|
|
11
11
|
*/
|
|
12
12
|
protected formatPromptWithImageMetadata(prompt: string, images?: ImageContent[]): string;
|
|
13
13
|
/**
|
|
14
|
-
* Extract images from batch data
|
|
15
|
-
* Returns an array of ImageContent objects for
|
|
14
|
+
* Extract images from batch data markers
|
|
15
|
+
* Returns an array of ImageContent objects for markers that have screenshots
|
|
16
16
|
*/
|
|
17
17
|
extractImages(data: BatchData): ImageContent[];
|
|
18
18
|
formatPrompt(data: BatchData, projectDir: string): string;
|
|
@@ -18,12 +18,12 @@ export class BaseAgent {
|
|
|
18
18
|
return `${header}---\n\n${prompt}`;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
* Extract images from batch data
|
|
22
|
-
* Returns an array of ImageContent objects for
|
|
21
|
+
* Extract images from batch data markers
|
|
22
|
+
* Returns an array of ImageContent objects for markers that have screenshots
|
|
23
23
|
*/
|
|
24
24
|
extractImages(data) {
|
|
25
25
|
const images = [];
|
|
26
|
-
data.
|
|
26
|
+
data.markers.forEach((ann, i) => {
|
|
27
27
|
if (ann.screenshot) {
|
|
28
28
|
let base64Data = ann.screenshot;
|
|
29
29
|
let mediaType = 'image/png'; // Default
|
|
@@ -48,13 +48,13 @@ export class BaseAgent {
|
|
|
48
48
|
// 3. Check padding is correct
|
|
49
49
|
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
50
50
|
if (!base64Data || !base64Regex.test(base64Data) || base64Data.length % 4 !== 0) {
|
|
51
|
-
console.warn(`ZingIt: Invalid base64 data in
|
|
52
|
-
return; // Skip this
|
|
51
|
+
console.warn(`ZingIt: Invalid base64 data in marker ${i + 1}, skipping screenshot`);
|
|
52
|
+
return; // Skip this marker's screenshot
|
|
53
53
|
}
|
|
54
54
|
// Check image size limit (base64 is ~33% larger than binary)
|
|
55
55
|
const estimatedBinarySize = Math.ceil(base64Data.length * 0.75);
|
|
56
56
|
if (estimatedBinarySize > MAX_IMAGE_SIZE_BYTES) {
|
|
57
|
-
console.warn(`ZingIt: Image in
|
|
57
|
+
console.warn(`ZingIt: Image in marker ${i + 1} exceeds ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024}MB limit, skipping`);
|
|
58
58
|
return; // Skip oversized image
|
|
59
59
|
}
|
|
60
60
|
// Validate that base64 can be decoded (catches corrupted data)
|
|
@@ -62,13 +62,13 @@ export class BaseAgent {
|
|
|
62
62
|
Buffer.from(base64Data, 'base64');
|
|
63
63
|
}
|
|
64
64
|
catch (err) {
|
|
65
|
-
console.warn(`ZingIt: Failed to decode base64 in
|
|
66
|
-
return; // Skip this
|
|
65
|
+
console.warn(`ZingIt: Failed to decode base64 in marker ${i + 1}, skipping screenshot:`, err);
|
|
66
|
+
return; // Skip this marker's screenshot
|
|
67
67
|
}
|
|
68
68
|
images.push({
|
|
69
69
|
base64: base64Data,
|
|
70
70
|
mediaType,
|
|
71
|
-
label: `Screenshot of
|
|
71
|
+
label: `Screenshot of Marker ${i + 1}: ${ann.identifier}`
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
74
|
});
|
|
@@ -81,10 +81,10 @@ Page: ${data.pageTitle}
|
|
|
81
81
|
URL: ${data.pageUrl}
|
|
82
82
|
|
|
83
83
|
`;
|
|
84
|
-
data.
|
|
84
|
+
data.markers.forEach((ann, i) => {
|
|
85
85
|
prompt += `---
|
|
86
86
|
|
|
87
|
-
##
|
|
87
|
+
## Marker ${i + 1}: ${ann.identifier}
|
|
88
88
|
|
|
89
89
|
**Requested Change:** ${ann.notes}
|
|
90
90
|
|
|
@@ -108,8 +108,8 @@ ${ann.parentContext ? `**Parent Path:** \`${ann.parentContext}\`` : ''}
|
|
|
108
108
|
|
|
109
109
|
`;
|
|
110
110
|
});
|
|
111
|
-
// Check if any
|
|
112
|
-
const hasScreenshots = data.
|
|
111
|
+
// Check if any markers have screenshots
|
|
112
|
+
const hasScreenshots = data.markers.some(ann => ann.screenshot);
|
|
113
113
|
prompt += `
|
|
114
114
|
CRITICAL INSTRUCTIONS:
|
|
115
115
|
1. CAREFULLY identify the CORRECT element to modify:
|
|
@@ -84,7 +84,7 @@ CRITICAL EFFICIENCY RULES:
|
|
|
84
84
|
2. Make the requested change immediately - don't explore or explain unless there's ambiguity
|
|
85
85
|
3. For simple changes (text, styles, attributes), be concise - just do it and confirm
|
|
86
86
|
4. Only search/explore if the selector doesn't match or you need to understand complex context
|
|
87
|
-
5. Avoid explaining what
|
|
87
|
+
5. Avoid explaining what markers are or describing the codebase unnecessarily
|
|
88
88
|
|
|
89
89
|
WHEN TO BE BRIEF (90% of cases):
|
|
90
90
|
- Text changes: Find, change, confirm (1-2 sentences)
|
|
@@ -79,7 +79,7 @@ export class CodexAgent extends BaseAgent {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
// Add system instructions and main prompt
|
|
82
|
-
const systemInstructions = `You are a UI debugging assistant. When given
|
|
82
|
+
const systemInstructions = `You are a UI debugging assistant. When given markers about UI elements, search for the corresponding code using the selectors and HTML context provided, then make the requested changes.
|
|
83
83
|
|
|
84
84
|
When screenshots are provided, use them to:
|
|
85
85
|
- Better understand the visual context and styling of the elements
|
|
@@ -49,7 +49,7 @@ export class CopilotAgent extends BaseAgent {
|
|
|
49
49
|
<context>
|
|
50
50
|
You are a UI debugging assistant working in the project directory: ${projectDir}
|
|
51
51
|
|
|
52
|
-
When given
|
|
52
|
+
When given markers about UI elements:
|
|
53
53
|
1. Search for the corresponding code using the selectors and HTML context provided
|
|
54
54
|
2. Make the requested changes in the project at ${projectDir}
|
|
55
55
|
3. Be thorough in finding the right files and making precise edits
|
|
@@ -116,7 +116,7 @@ export async function handleBatch(ws, state, msg, deps) {
|
|
|
116
116
|
try {
|
|
117
117
|
console.log('[Batch] Creating checkpoint...');
|
|
118
118
|
const checkpoint = await state.gitManager.createCheckpoint({
|
|
119
|
-
|
|
119
|
+
markers: batchData.markers,
|
|
120
120
|
pageUrl: batchData.pageUrl,
|
|
121
121
|
pageTitle: batchData.pageTitle,
|
|
122
122
|
agentName: state.agentName,
|
|
@@ -128,7 +128,7 @@ export async function handleBatch(ws, state, msg, deps) {
|
|
|
128
128
|
checkpoint: {
|
|
129
129
|
id: checkpoint.id,
|
|
130
130
|
timestamp: checkpoint.timestamp,
|
|
131
|
-
|
|
131
|
+
markers: checkpoint.markers,
|
|
132
132
|
filesModified: 0,
|
|
133
133
|
linesChanged: 0,
|
|
134
134
|
agentName: checkpoint.agentName,
|
|
@@ -169,12 +169,12 @@ export async function handleBatch(ws, state, msg, deps) {
|
|
|
169
169
|
else {
|
|
170
170
|
console.log('[Batch] Reusing existing session');
|
|
171
171
|
}
|
|
172
|
-
// Log user's
|
|
173
|
-
console.log('[Batch]
|
|
174
|
-
if (batchData.
|
|
175
|
-
batchData.
|
|
176
|
-
const notePreview =
|
|
177
|
-
console.log(`[Batch]
|
|
172
|
+
// Log user's markers before formatting
|
|
173
|
+
console.log('[Batch] Marker count:', batchData.markers?.length || 0);
|
|
174
|
+
if (batchData.markers && batchData.markers.length > 0) {
|
|
175
|
+
batchData.markers.forEach((m, idx) => {
|
|
176
|
+
const notePreview = m.notes?.substring(0, 200) || '(no notes)';
|
|
177
|
+
console.log(`[Batch] Marker ${idx + 1}: ${notePreview}`);
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
if (batchData.pageUrl) {
|
|
@@ -229,7 +229,7 @@ export async function handleMessage(ws, state, msg, deps) {
|
|
|
229
229
|
return;
|
|
230
230
|
}
|
|
231
231
|
console.log('[Message] ===== Request started =====');
|
|
232
|
-
// Create session if it doesn't exist (allows direct messaging without
|
|
232
|
+
// Create session if it doesn't exist (allows direct messaging without markers)
|
|
233
233
|
if (!state.session) {
|
|
234
234
|
if (!state.agent) {
|
|
235
235
|
console.warn('[ZingIt] No agent selected for message');
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Marker } from '../types.js';
|
|
2
2
|
export interface Checkpoint {
|
|
3
3
|
id: string;
|
|
4
4
|
timestamp: string;
|
|
5
5
|
commitHash: string;
|
|
6
6
|
branchName: string;
|
|
7
|
-
|
|
7
|
+
markers: MarkerSummary[];
|
|
8
8
|
pageUrl: string;
|
|
9
9
|
pageTitle: string;
|
|
10
10
|
agentName: string;
|
|
@@ -12,7 +12,7 @@ export interface Checkpoint {
|
|
|
12
12
|
filesModified: number;
|
|
13
13
|
linesChanged: number;
|
|
14
14
|
}
|
|
15
|
-
export interface
|
|
15
|
+
export interface MarkerSummary {
|
|
16
16
|
id: string;
|
|
17
17
|
identifier: string;
|
|
18
18
|
notes: string;
|
|
@@ -32,7 +32,7 @@ export interface ChangeHistory {
|
|
|
32
32
|
export interface CheckpointInfo {
|
|
33
33
|
id: string;
|
|
34
34
|
timestamp: string;
|
|
35
|
-
|
|
35
|
+
markers: MarkerSummary[];
|
|
36
36
|
filesModified: number;
|
|
37
37
|
linesChanged: number;
|
|
38
38
|
agentName: string;
|
|
@@ -62,7 +62,7 @@ export declare class GitManager {
|
|
|
62
62
|
* Create a checkpoint before AI modifications
|
|
63
63
|
*/
|
|
64
64
|
createCheckpoint(metadata: {
|
|
65
|
-
|
|
65
|
+
markers: Marker[];
|
|
66
66
|
pageUrl: string;
|
|
67
67
|
pageTitle: string;
|
|
68
68
|
agentName: string;
|
|
@@ -114,10 +114,10 @@ export class GitManager {
|
|
|
114
114
|
timestamp: new Date().toISOString(),
|
|
115
115
|
commitHash: commitHash.trim(),
|
|
116
116
|
branchName: status.branch,
|
|
117
|
-
|
|
118
|
-
id:
|
|
119
|
-
identifier:
|
|
120
|
-
notes:
|
|
117
|
+
markers: metadata.markers.map((m) => ({
|
|
118
|
+
id: m.id,
|
|
119
|
+
identifier: m.identifier,
|
|
120
|
+
notes: m.notes,
|
|
121
121
|
})),
|
|
122
122
|
pageUrl: metadata.pageUrl,
|
|
123
123
|
pageTitle: metadata.pageTitle,
|
|
@@ -145,7 +145,7 @@ export class GitManager {
|
|
|
145
145
|
// Get list of changed files since checkpoint
|
|
146
146
|
let diffStat = '';
|
|
147
147
|
try {
|
|
148
|
-
const result = await
|
|
148
|
+
const result = await execFileAsync('git', ['diff', '--name-status', checkpoint.commitHash], {
|
|
149
149
|
cwd: this.projectDir,
|
|
150
150
|
});
|
|
151
151
|
diffStat = result.stdout;
|
|
@@ -166,7 +166,7 @@ export class GitManager {
|
|
|
166
166
|
let linesAdded = 0;
|
|
167
167
|
let linesRemoved = 0;
|
|
168
168
|
try {
|
|
169
|
-
const { stdout: numstat } = await
|
|
169
|
+
const { stdout: numstat } = await execFileAsync('git', ['diff', '--numstat', checkpoint.commitHash, '--', filePath], { cwd: this.projectDir });
|
|
170
170
|
const parts = numstat.trim().split('\t');
|
|
171
171
|
linesAdded = parseInt(parts[0]) || 0;
|
|
172
172
|
linesRemoved = parseInt(parts[1]) || 0;
|
|
@@ -188,7 +188,7 @@ export class GitManager {
|
|
|
188
188
|
if (fileChanges.length > 0) {
|
|
189
189
|
try {
|
|
190
190
|
await execFileAsync('git', ['add', '-A'], { cwd: this.projectDir });
|
|
191
|
-
const identifiers = checkpoint.
|
|
191
|
+
const identifiers = checkpoint.markers.map((a) => sanitizeForGit(a.identifier)).join(', ');
|
|
192
192
|
const commitMsg = `[ZingIt] ${identifiers}`;
|
|
193
193
|
// Use execFile with array args to avoid shell injection
|
|
194
194
|
await execFileAsync('git', ['commit', '-m', commitMsg], { cwd: this.projectDir });
|
|
@@ -222,7 +222,7 @@ export class GitManager {
|
|
|
222
222
|
throw new GitManagerError('Checkpoint is not in applied state', 'INVALID_CHECKPOINT_STATE');
|
|
223
223
|
}
|
|
224
224
|
// Reset to the checkpoint's original commit
|
|
225
|
-
await
|
|
225
|
+
await execFileAsync('git', ['reset', '--hard', checkpoint.commitHash], { cwd: this.projectDir });
|
|
226
226
|
// Get files that were reverted
|
|
227
227
|
const filesReverted = [];
|
|
228
228
|
// We could compute this but for now just return empty - the checkpoint has the info
|
|
@@ -245,7 +245,7 @@ export class GitManager {
|
|
|
245
245
|
throw new GitManagerError(`Checkpoint not found: ${checkpointId}`, 'CHECKPOINT_NOT_FOUND');
|
|
246
246
|
}
|
|
247
247
|
// Reset to that commit
|
|
248
|
-
await
|
|
248
|
+
await execFileAsync('git', ['reset', '--hard', checkpoint.commitHash], { cwd: this.projectDir });
|
|
249
249
|
// Mark all checkpoints after this one as reverted
|
|
250
250
|
const checkpointIndex = history.checkpoints.findIndex((c) => c.id === checkpointId);
|
|
251
251
|
for (let i = checkpointIndex + 1; i < history.checkpoints.length; i++) {
|
|
@@ -266,7 +266,7 @@ export class GitManager {
|
|
|
266
266
|
return history.checkpoints.map((cp, index) => ({
|
|
267
267
|
id: cp.id,
|
|
268
268
|
timestamp: cp.timestamp,
|
|
269
|
-
|
|
269
|
+
markers: cp.markers,
|
|
270
270
|
filesModified: cp.filesModified,
|
|
271
271
|
linesChanged: cp.linesChanged,
|
|
272
272
|
agentName: cp.agentName,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { GitManager, GitManagerError } from './git-manager.js';
|
|
2
|
-
export type { Checkpoint, CheckpointInfo, ChangeHistory, FileChange,
|
|
2
|
+
export type { Checkpoint, CheckpointInfo, ChangeHistory, FileChange, MarkerSummary } from './git-manager.js';
|
package/server/dist/types.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface AgentSession {
|
|
|
29
29
|
destroy(): Promise<void>;
|
|
30
30
|
getSessionId?(): string | null;
|
|
31
31
|
}
|
|
32
|
-
export interface
|
|
32
|
+
export interface Marker {
|
|
33
33
|
id: string;
|
|
34
34
|
selector: string;
|
|
35
35
|
identifier: string;
|
|
@@ -46,7 +46,7 @@ export interface Annotation {
|
|
|
46
46
|
export interface BatchData {
|
|
47
47
|
pageUrl: string;
|
|
48
48
|
pageTitle: string;
|
|
49
|
-
|
|
49
|
+
markers: Marker[];
|
|
50
50
|
projectDir?: string;
|
|
51
51
|
}
|
|
52
52
|
export type WSIncomingType = 'batch' | 'message' | 'reset' | 'stop' | 'get_agents' | 'select_agent' | 'get_history' | 'undo' | 'revert_to' | 'clear_history';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { BatchData } from '../types.js';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const MAX_MARKERS = 50;
|
|
3
3
|
export declare const MAX_HTML_LENGTH = 50000;
|
|
4
4
|
export declare const MAX_NOTES_LENGTH = 5000;
|
|
5
5
|
export declare const MAX_SELECTOR_LENGTH = 1000;
|
|
@@ -2,33 +2,33 @@
|
|
|
2
2
|
// ============================================
|
|
3
3
|
// Payload Validation
|
|
4
4
|
// ============================================
|
|
5
|
-
export const
|
|
5
|
+
export const MAX_MARKERS = 50;
|
|
6
6
|
export const MAX_HTML_LENGTH = 50000;
|
|
7
7
|
export const MAX_NOTES_LENGTH = 5000;
|
|
8
8
|
export const MAX_SELECTOR_LENGTH = 1000;
|
|
9
9
|
export const MAX_SCREENSHOT_SIZE = 5000000; // ~5MB base64 (matches Claude API limit)
|
|
10
10
|
const VALID_STATUSES = ['pending', 'processing', 'completed'];
|
|
11
|
-
function
|
|
12
|
-
if (!
|
|
13
|
-
return { valid: false, error: `
|
|
11
|
+
function validateMarker(marker, index) {
|
|
12
|
+
if (!marker.id || typeof marker.id !== 'string') {
|
|
13
|
+
return { valid: false, error: `Marker ${index}: missing or invalid id` };
|
|
14
14
|
}
|
|
15
|
-
if (!
|
|
16
|
-
return { valid: false, error: `
|
|
15
|
+
if (!marker.identifier || typeof marker.identifier !== 'string') {
|
|
16
|
+
return { valid: false, error: `Marker ${index}: missing or invalid identifier` };
|
|
17
17
|
}
|
|
18
|
-
if (
|
|
19
|
-
return { valid: false, error: `
|
|
18
|
+
if (marker.status && !VALID_STATUSES.includes(marker.status)) {
|
|
19
|
+
return { valid: false, error: `Marker ${index}: invalid status '${marker.status}'` };
|
|
20
20
|
}
|
|
21
|
-
if (
|
|
22
|
-
return { valid: false, error: `
|
|
21
|
+
if (marker.selector && marker.selector.length > MAX_SELECTOR_LENGTH) {
|
|
22
|
+
return { valid: false, error: `Marker ${index}: selector too long (max ${MAX_SELECTOR_LENGTH})` };
|
|
23
23
|
}
|
|
24
|
-
if (
|
|
25
|
-
return { valid: false, error: `
|
|
24
|
+
if (marker.html && marker.html.length > MAX_HTML_LENGTH) {
|
|
25
|
+
return { valid: false, error: `Marker ${index}: html too long (max ${MAX_HTML_LENGTH})` };
|
|
26
26
|
}
|
|
27
|
-
if (
|
|
28
|
-
return { valid: false, error: `
|
|
27
|
+
if (marker.notes && marker.notes.length > MAX_NOTES_LENGTH) {
|
|
28
|
+
return { valid: false, error: `Marker ${index}: notes too long (max ${MAX_NOTES_LENGTH})` };
|
|
29
29
|
}
|
|
30
|
-
if (
|
|
31
|
-
return { valid: false, error: `
|
|
30
|
+
if (marker.screenshot && marker.screenshot.length > MAX_SCREENSHOT_SIZE) {
|
|
31
|
+
return { valid: false, error: `Marker ${index}: screenshot too large (max ${MAX_SCREENSHOT_SIZE / 1000}KB)` };
|
|
32
32
|
}
|
|
33
33
|
return { valid: true };
|
|
34
34
|
}
|
|
@@ -36,18 +36,18 @@ export function validateBatchData(data) {
|
|
|
36
36
|
if (!data) {
|
|
37
37
|
return { valid: false, error: 'Missing batch data' };
|
|
38
38
|
}
|
|
39
|
-
if (!data.
|
|
40
|
-
return { valid: false, error: 'Missing or invalid
|
|
39
|
+
if (!data.markers || !Array.isArray(data.markers)) {
|
|
40
|
+
return { valid: false, error: 'Missing or invalid markers array' };
|
|
41
41
|
}
|
|
42
|
-
if (data.
|
|
43
|
-
return { valid: false, error: 'No
|
|
42
|
+
if (data.markers.length === 0) {
|
|
43
|
+
return { valid: false, error: 'No markers provided' };
|
|
44
44
|
}
|
|
45
|
-
if (data.
|
|
46
|
-
return { valid: false, error: `Too many
|
|
45
|
+
if (data.markers.length > MAX_MARKERS) {
|
|
46
|
+
return { valid: false, error: `Too many markers (max ${MAX_MARKERS})` };
|
|
47
47
|
}
|
|
48
|
-
// Validate each
|
|
49
|
-
for (let i = 0; i < data.
|
|
50
|
-
const result =
|
|
48
|
+
// Validate each marker
|
|
49
|
+
for (let i = 0; i < data.markers.length; i++) {
|
|
50
|
+
const result = validateMarker(data.markers[i], i);
|
|
51
51
|
if (!result.valid) {
|
|
52
52
|
return { valid: false, error: result.error };
|
|
53
53
|
}
|