@cluesmith/codev 1.2.1 → 1.2.3

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.
@@ -52,6 +52,28 @@ const langMap = {
52
52
  };
53
53
  const lang = langMap[ext] || ext;
54
54
  const isMarkdown = ext === 'md';
55
+ // Image detection
56
+ const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];
57
+ const isImage = imageExtensions.includes(ext);
58
+ // Video detection
59
+ const videoExtensions = ['webm', 'mp4', 'mov', 'avi'];
60
+ const isVideo = videoExtensions.includes(ext);
61
+ // MIME type mapping for images
62
+ const imageMimeTypes = {
63
+ png: 'image/png',
64
+ jpg: 'image/jpeg',
65
+ jpeg: 'image/jpeg',
66
+ gif: 'image/gif',
67
+ webp: 'image/webp',
68
+ svg: 'image/svg+xml'
69
+ };
70
+ // MIME type mapping for videos
71
+ const videoMimeTypes = {
72
+ webm: 'video/webm',
73
+ mp4: 'video/mp4',
74
+ mov: 'video/quicktime',
75
+ avi: 'video/x-msvideo'
76
+ };
55
77
  // Create server
56
78
  const server = http.createServer((req, res) => {
57
79
  // CORS headers
@@ -67,18 +89,34 @@ const server = http.createServer((req, res) => {
67
89
  if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
68
90
  try {
69
91
  let template = fs.readFileSync(templatePath, 'utf-8');
70
- const fileContent = fs.readFileSync(fullFilePath, 'utf-8');
92
+ // Get file stats for images
93
+ const fileStats = fs.statSync(fullFilePath);
94
+ const fileSize = fileStats.size;
71
95
  // Replace placeholders
72
96
  template = template.replace(/\{\{BUILDER_ID\}\}/g, '');
73
97
  template = template.replace(/\{\{FILE_PATH\}\}/g, fullFilePath);
74
98
  template = template.replace(/\{\{FILE\}\}/g, displayPath);
75
99
  template = template.replace(/\{\{LANG\}\}/g, lang);
76
100
  template = template.replace(/\{\{IS_MARKDOWN\}\}/g, String(isMarkdown));
77
- // Inject file content
78
- // JSON.stringify escapes quotes but not </script> which would break HTML parsing
79
- // Replace </script> with <\/script> (valid JS, doesn't match HTML closing tag)
80
- const escapedContent = JSON.stringify(fileContent).replace(/<\/script>/gi, '<\\/script>');
81
- template = template.replace('// FILE_CONTENT will be injected by the server', `init(${escapedContent});`);
101
+ template = template.replace(/\{\{IS_IMAGE\}\}/g, String(isImage));
102
+ template = template.replace(/\{\{IS_VIDEO\}\}/g, String(isVideo));
103
+ template = template.replace(/\{\{FILE_SIZE\}\}/g, String(fileSize));
104
+ if (isImage) {
105
+ // For images, don't inject file content - it will be loaded via /api/image
106
+ template = template.replace('// FILE_CONTENT will be injected by the server', `initImage(${fileSize});`);
107
+ }
108
+ else if (isVideo) {
109
+ // For videos, don't inject file content - it will be loaded via /api/video
110
+ template = template.replace('// FILE_CONTENT will be injected by the server', `initVideo(${fileSize});`);
111
+ }
112
+ else {
113
+ // For text files, inject content as before
114
+ const fileContent = fs.readFileSync(fullFilePath, 'utf-8');
115
+ // JSON.stringify escapes quotes but not </script> which would break HTML parsing
116
+ // Replace </script> with <\/script> (valid JS, doesn't match HTML closing tag)
117
+ const escapedContent = JSON.stringify(fileContent).replace(/<\/script>/gi, '<\\/script>');
118
+ template = template.replace('// FILE_CONTENT will be injected by the server', `init(${escapedContent});`);
119
+ }
82
120
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
83
121
  res.end(template);
84
122
  }
@@ -102,6 +140,54 @@ const server = http.createServer((req, res) => {
102
140
  }
103
141
  return;
104
142
  }
143
+ // Handle image content (GET /api/image)
144
+ // Use startsWith to allow query params like ?t=123 for cache busting
145
+ if (req.method === 'GET' && req.url?.startsWith('/api/image')) {
146
+ if (!isImage) {
147
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
148
+ res.end('Not an image file');
149
+ return;
150
+ }
151
+ try {
152
+ const imageData = fs.readFileSync(fullFilePath);
153
+ const mimeType = imageMimeTypes[ext] || 'application/octet-stream';
154
+ res.writeHead(200, {
155
+ 'Content-Type': mimeType,
156
+ 'Content-Length': imageData.length,
157
+ 'Cache-Control': 'no-cache' // Don't cache, allow reload to work
158
+ });
159
+ res.end(imageData);
160
+ }
161
+ catch (err) {
162
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
163
+ res.end('Error reading image: ' + err.message);
164
+ }
165
+ return;
166
+ }
167
+ // Handle video content (GET /api/video)
168
+ // Use startsWith to allow query params like ?t=123 for cache busting
169
+ if (req.method === 'GET' && req.url?.startsWith('/api/video')) {
170
+ if (!isVideo) {
171
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
172
+ res.end('Not a video file');
173
+ return;
174
+ }
175
+ try {
176
+ const videoData = fs.readFileSync(fullFilePath);
177
+ const mimeType = videoMimeTypes[ext] || 'application/octet-stream';
178
+ res.writeHead(200, {
179
+ 'Content-Type': mimeType,
180
+ 'Content-Length': videoData.length,
181
+ 'Cache-Control': 'no-cache' // Don't cache, allow reload to work
182
+ });
183
+ res.end(videoData);
184
+ }
185
+ catch (err) {
186
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
187
+ res.end('Error reading video: ' + err.message);
188
+ }
189
+ return;
190
+ }
105
191
  // Handle file save
106
192
  if (req.method === 'POST' && req.url === '/save') {
107
193
  let body = '';
@@ -1 +1 @@
1
- {"version":3,"file":"open-server.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/open-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"open-server.js","sourceRoot":"","sources":["../../../src/agent-farm/servers/open-server.ts"],"names":[],"mappings":";AAEA;;;;;GAKG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;AAE3C,kBAAkB;AAClB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;AAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AAEzB,IAAI,CAAC,QAAQ,EAAE,CAAC;IACd,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;IACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,QAAQ;AACR,MAAM,YAAY,GAAG,QAAQ,CAAC;AAC9B,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAE5C;;;GAGG;AACH,SAAS,gBAAgB;IACvB,MAAM,QAAQ,GAAG,WAAW,CAAC;IAE7B,2DAA2D;IAC3D,iEAAiE;IACjE,8DAA8D;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,qBAAqB,EAAE,QAAQ,CAAC,CAAC;IACzE,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAE3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAC;AAExC,uBAAuB;AACvB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,8BAA8B;AAC9B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AAC1D,MAAM,OAAO,GAA2B;IACtC,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY;IACxE,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IACtC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,KAAK;IAC1C,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;CACxC,CAAC;AACF,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;AACjC,MAAM,UAAU,GAAG,GAAG,KAAK,IAAI,CAAC;AAEhC,kBAAkB;AAClB,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;AACrE,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAE9C,kBAAkB;AAClB,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;AACtD,MAAM,OAAO,GAAG,eAAe,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAE9C,+BAA+B;AAC/B,MAAM,cAAc,GAA2B;IAC7C,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;CACrB,CAAC;AAEF,+BAA+B;AAC/B,MAAM,cAAc,GAA2B;IAC7C,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,iBAAiB;CACvB,CAAC;AAEF,gBAAgB;AAChB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC5C,eAAe;IACf,GAAG,CAAC,SAAS,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;IAClD,GAAG,CAAC,SAAS,CAAC,8BAA8B,EAAE,oBAAoB,CAAC,CAAC;IACpE,GAAG,CAAC,SAAS,CAAC,8BAA8B,EAAE,cAAc,CAAC,CAAC;IAE9D,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,GAAG,EAAE,CAAC;QACV,OAAO;IACT,CAAC;IAED,0BAA0B;IAC1B,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,aAAa,CAAC,EAAE,CAAC;QAC3E,IAAI,CAAC;YACH,IAAI,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAEtD,4BAA4B;YAC5B,MAAM,SAAS,GAAG,EAAE,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC;YAC5C,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC;YAEhC,uBAAuB;YACvB,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,CAAC;YACvD,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,oBAAoB,EAAE,YAAY,CAAC,CAAC;YAChE,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,WAAW,CAAC,CAAC;YAC1D,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;YACnD,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;YACxE,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YAClE,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;YAClE,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAEpE,IAAI,OAAO,EAAE,CAAC;gBACZ,2EAA2E;gBAC3E,QAAQ,GAAG,QAAQ,CAAC,OAAO,CACzB,gDAAgD,EAChD,aAAa,QAAQ,IAAI,CAC1B,CAAC;YACJ,CAAC;iBAAM,IAAI,OAAO,EAAE,CAAC;gBACnB,2EAA2E;gBAC3E,QAAQ,GAAG,QAAQ,CAAC,OAAO,CACzB,gDAAgD,EAChD,aAAa,QAAQ,IAAI,CAC1B,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,2CAA2C;gBAC3C,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;gBAC3D,iFAAiF;gBACjF,+EAA+E;gBAC/E,MAAM,cAAc,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;gBAC1F,QAAQ,GAAG,QAAQ,CAAC,OAAO,CACzB,gDAAgD,EAChD,QAAQ,cAAc,IAAI,CAC3B,CAAC;YACJ,CAAC;YAED,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,0BAA0B,EAAE,CAAC,CAAC;YACnE,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,wBAAwB,GAAI,GAAa,CAAC,OAAO,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO;IACT,CAAC;IAED,iCAAiC;IACjC,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzD,IAAI,CAAC;YACH,yBAAyB;YACzB,MAAM,WAAW,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAC3D,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE,CAAC,CAAC;YACpE,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACvB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,sBAAsB,GAAI,GAAa,CAAC,OAAO,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO;IACT,CAAC;IAED,wCAAwC;IACxC,qEAAqE;IACrE,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;YAC7B,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;YACnE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;gBACjB,cAAc,EAAE,QAAQ;gBACxB,gBAAgB,EAAE,SAAS,CAAC,MAAM;gBAClC,eAAe,EAAE,UAAU,CAAE,oCAAoC;aAClE,CAAC,CAAC;YACH,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,uBAAuB,GAAI,GAAa,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO;IACT,CAAC;IAED,wCAAwC;IACxC,qEAAqE;IACrE,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,IAAI,CAAC;YACH,MAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,CAAC;YAChD,MAAM,QAAQ,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;YACnE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;gBACjB,cAAc,EAAE,QAAQ;gBACxB,gBAAgB,EAAE,SAAS,CAAC,MAAM;gBAClC,eAAe,EAAE,UAAU,CAAE,oCAAoC;aAClE,CAAC,CAAC;YACH,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;YACrD,GAAG,CAAC,GAAG,CAAC,uBAAuB,GAAI,GAAa,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO;IACT,CAAC;IAED,mBAAmB;IACnB,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;QACjD,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC5D,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;YACjB,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAE3C,8CAA8C;gBAC9C,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;oBAC1B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;oBACrD,GAAG,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;oBACzC,OAAO;gBACT,CAAC;gBAED,EAAE,CAAC,aAAa,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;gBACjD,OAAO,CAAC,GAAG,CAAC,UAAU,YAAY,EAAE,CAAC,CAAC;gBAEtC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YAC7C,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;gBACrD,GAAG,CAAC,GAAG,CAAC,qBAAqB,GAAI,GAAa,CAAC,OAAO,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,0BAA0B;IAC1B,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC,CAAC;IACrD,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;IACvB,OAAO,CAAC,GAAG,CAAC,iCAAiC,IAAI,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,SAAS,YAAY,EAAE,CAAC,CAAC;AACvC,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cluesmith/codev",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Codev CLI - AI-assisted software development framework",
5
5
  "type": "module",
6
6
  "bin": {
@@ -284,12 +284,75 @@
284
284
  .annotation-item:hover { background: #2a2a2a; }
285
285
  .annotation-item .line { color: #3b82f6; font-size: 12px; }
286
286
  .annotation-item .text { font-size: 13px; margin-top: 4px; }
287
+
288
+ /* Image Viewer */
289
+ #image-viewer {
290
+ display: none;
291
+ flex-direction: column;
292
+ height: calc(100vh - 80px);
293
+ padding: 15px;
294
+ }
295
+ .image-controls {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 8px;
299
+ padding: 10px 0;
300
+ flex-shrink: 0;
301
+ }
302
+ .image-controls .btn {
303
+ padding: 6px 12px;
304
+ border-radius: 4px;
305
+ border: none;
306
+ cursor: pointer;
307
+ font-size: 12px;
308
+ background: #444;
309
+ color: #fff;
310
+ }
311
+ .image-controls .btn:hover { opacity: 0.9; }
312
+ .image-controls .btn.active {
313
+ background: #3b82f6;
314
+ }
315
+ #image-info {
316
+ color: #888;
317
+ font-size: 12px;
318
+ margin-left: auto;
319
+ }
320
+ .image-container {
321
+ flex: 1;
322
+ overflow: auto;
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: center;
326
+ background: #222;
327
+ border-radius: 4px;
328
+ }
329
+ .image-container.zoom-fit img {
330
+ max-width: 100%;
331
+ max-height: 100%;
332
+ width: auto;
333
+ height: auto;
334
+ object-fit: contain;
335
+ }
336
+ .image-container.zoom-100 img,
337
+ .image-container.zoom-custom img {
338
+ max-width: none;
339
+ max-height: none;
340
+ }
341
+ #image-display {
342
+ transition: transform 0.1s ease;
343
+ }
344
+ #image-error {
345
+ color: #ef4444;
346
+ text-align: center;
347
+ padding: 40px;
348
+ }
287
349
  </style>
288
350
  </head>
289
- <body data-builder="{{BUILDER_ID}}" data-file="{{FILE_PATH}}" data-lang="{{LANG}}">
351
+ <body data-builder="{{BUILDER_ID}}" data-file="{{FILE_PATH}}" data-lang="{{LANG}}" data-is-image="{{IS_IMAGE}}" data-is-video="{{IS_VIDEO}}" data-file-size="{{FILE_SIZE}}">
290
352
  <div class="header">
291
353
  <h1 class="path">{{FILE_PATH}}</h1>
292
- <div class="subtitle">Click on a line number to leave an annotation.</div>
354
+ <div class="subtitle" id="subtitle">Click on a line number to leave an annotation.</div>
355
+ <div id="image-header-info" style="display: none; color: #888; font-size: 12px; margin-top: 4px;"></div>
293
356
  <div class="actions">
294
357
  <button id="reloadBtn" class="btn btn-secondary" onclick="reloadFile()" title="Reload from disk" aria-label="Reload file from disk">↻</button>
295
358
  <button id="togglePreviewBtn" class="btn btn-secondary" style="display: none;" title="Toggle Preview (Cmd+Shift+P)">
@@ -309,6 +372,32 @@
309
372
  <!-- Markdown preview mode -->
310
373
  <div id="preview-container" style="display: none; padding: 20px; overflow: auto; height: calc(100vh - 80px);"></div>
311
374
 
375
+ <!-- Image viewer mode -->
376
+ <div id="image-viewer">
377
+ <div class="image-controls">
378
+ <button id="zoomFitBtn" class="btn active" onclick="zoomFit()">Fit</button>
379
+ <button id="zoom100Btn" class="btn" onclick="zoom100()">100%</button>
380
+ <button id="zoomInBtn" class="btn" onclick="zoomIn()">+</button>
381
+ <button id="zoomOutBtn" class="btn" onclick="zoomOut()">−</button>
382
+ <span id="zoom-level"></span>
383
+ <span id="image-info"></span>
384
+ </div>
385
+ <div class="image-container zoom-fit" id="image-container">
386
+ <img id="image-display" alt="Image preview" />
387
+ <div id="image-error" style="display: none;"></div>
388
+ </div>
389
+ </div>
390
+
391
+ <!-- Video viewer mode -->
392
+ <div id="video-viewer" style="display: none; padding: 15px; height: calc(100vh - 80px);">
393
+ <div style="display: flex; align-items: center; justify-content: center; height: 100%; background: #222; border-radius: 4px;">
394
+ <video id="video-display" controls style="max-width: 100%; max-height: 100%;">
395
+ Your browser does not support the video tag.
396
+ </video>
397
+ <div id="video-error" style="display: none; color: #ef4444; text-align: center; padding: 40px;"></div>
398
+ </div>
399
+ </div>
400
+
312
401
  <!-- Editor mode -->
313
402
  <textarea id="editor" spellcheck="false"></textarea>
314
403
 
@@ -362,6 +451,16 @@
362
451
  const isMarkdownFile = {{IS_MARKDOWN}};
363
452
  let isPreviewMode = false;
364
453
 
454
+ // Image viewer state
455
+ const isImageFile = {{IS_IMAGE}};
456
+ let currentZoomMode = 'fit'; // 'fit', '100', 'custom'
457
+ let currentZoomLevel = 1.0;
458
+ let imageNaturalWidth = 0;
459
+ let imageNaturalHeight = 0;
460
+
461
+ // Video viewer state
462
+ const isVideoFile = {{IS_VIDEO}};
463
+
365
464
  // Comment patterns by file extension
366
465
  const COMMENT_PATTERNS = {
367
466
  js: { prefix: '// REVIEW', regex: /^(\s*)\/\/\s*REVIEW(\(@\w+\))?:\s*(.*)$/ },
@@ -403,6 +502,181 @@
403
502
  }
404
503
  }
405
504
 
505
+ // Initialize image viewer
506
+ function initImage(fileSize) {
507
+ // Hide code view elements
508
+ document.getElementById('viewMode').style.display = 'none';
509
+ document.getElementById('editBtn').style.display = 'none';
510
+ document.getElementById('togglePreviewBtn').style.display = 'none';
511
+
512
+ // Update subtitle
513
+ document.querySelector('.subtitle').textContent = 'Image viewer - use zoom controls to adjust view.';
514
+
515
+ // Show image viewer
516
+ const imageViewer = document.getElementById('image-viewer');
517
+ imageViewer.style.display = 'flex';
518
+
519
+ // Load image
520
+ const img = document.getElementById('image-display');
521
+ const imageError = document.getElementById('image-error');
522
+
523
+ img.onload = function() {
524
+ imageNaturalWidth = img.naturalWidth;
525
+ imageNaturalHeight = img.naturalHeight;
526
+ updateImageInfo(fileSize);
527
+ imageError.style.display = 'none';
528
+ img.style.display = 'block';
529
+ };
530
+
531
+ img.onerror = function() {
532
+ imageError.textContent = 'Failed to load image. The file may be corrupted.';
533
+ imageError.style.display = 'block';
534
+ img.style.display = 'none';
535
+ };
536
+
537
+ // Add cache-busting query param to allow reload
538
+ img.src = '/api/image?t=' + Date.now();
539
+ }
540
+
541
+ // Initialize video viewer
542
+ function initVideo(fileSize) {
543
+ // Hide code view elements
544
+ document.getElementById('viewMode').style.display = 'none';
545
+ document.getElementById('editBtn').style.display = 'none';
546
+ document.getElementById('togglePreviewBtn').style.display = 'none';
547
+
548
+ // Update subtitle
549
+ const sizeStr = formatFileSize(fileSize);
550
+ document.querySelector('.subtitle').textContent = 'Video player · ' + sizeStr;
551
+
552
+ // Show video viewer
553
+ const videoViewer = document.getElementById('video-viewer');
554
+ videoViewer.style.display = 'block';
555
+
556
+ // Load video
557
+ const video = document.getElementById('video-display');
558
+ const videoError = document.getElementById('video-error');
559
+
560
+ video.onloadedmetadata = function() {
561
+ videoError.style.display = 'none';
562
+ video.style.display = 'block';
563
+ };
564
+
565
+ video.onerror = function() {
566
+ videoError.textContent = 'Failed to load video. The file may be corrupted or in an unsupported format.';
567
+ videoError.style.display = 'block';
568
+ video.style.display = 'none';
569
+ };
570
+
571
+ // Add cache-busting query param to allow reload
572
+ video.src = '/api/video?t=' + Date.now();
573
+ }
574
+
575
+ // Update image info display (in both header and controls)
576
+ function updateImageInfo(fileSize) {
577
+ const sizeStr = formatFileSize(fileSize);
578
+ const infoText = `${imageNaturalWidth} × ${imageNaturalHeight} px · ${sizeStr}`;
579
+
580
+ // Update controls area
581
+ const info = document.getElementById('image-info');
582
+ info.textContent = infoText;
583
+
584
+ // Update header area
585
+ const headerInfo = document.getElementById('image-header-info');
586
+ headerInfo.textContent = infoText;
587
+ headerInfo.style.display = 'block';
588
+ }
589
+
590
+ // Format file size in human-readable form
591
+ function formatFileSize(bytes) {
592
+ if (bytes < 1024) return bytes + ' B';
593
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
594
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
595
+ }
596
+
597
+ // Update zoom level display
598
+ function updateZoomLevelDisplay() {
599
+ const zoomLevelEl = document.getElementById('zoom-level');
600
+ if (currentZoomMode === 'fit') {
601
+ zoomLevelEl.textContent = '';
602
+ } else {
603
+ zoomLevelEl.textContent = Math.round(currentZoomLevel * 100) + '%';
604
+ }
605
+ }
606
+
607
+ // Update active button state
608
+ function updateZoomButtons() {
609
+ document.getElementById('zoomFitBtn').classList.toggle('active', currentZoomMode === 'fit');
610
+ document.getElementById('zoom100Btn').classList.toggle('active', currentZoomMode === '100');
611
+ }
612
+
613
+ // Zoom fit - image fits within container
614
+ function zoomFit() {
615
+ const container = document.getElementById('image-container');
616
+ const img = document.getElementById('image-display');
617
+
618
+ container.className = 'image-container zoom-fit';
619
+ img.style.transform = '';
620
+ img.style.width = '';
621
+ img.style.height = '';
622
+
623
+ currentZoomMode = 'fit';
624
+ currentZoomLevel = 1.0;
625
+ updateZoomButtons();
626
+ updateZoomLevelDisplay();
627
+ }
628
+
629
+ // Zoom 100% - actual pixels
630
+ function zoom100() {
631
+ const container = document.getElementById('image-container');
632
+ const img = document.getElementById('image-display');
633
+
634
+ container.className = 'image-container zoom-100';
635
+ img.style.transform = '';
636
+ img.style.width = imageNaturalWidth + 'px';
637
+ img.style.height = imageNaturalHeight + 'px';
638
+
639
+ currentZoomMode = '100';
640
+ currentZoomLevel = 1.0;
641
+ updateZoomButtons();
642
+ updateZoomLevelDisplay();
643
+ }
644
+
645
+ // Zoom in by 25%
646
+ function zoomIn() {
647
+ if (currentZoomMode === 'fit') {
648
+ // Switch to custom zoom starting at 100%
649
+ currentZoomLevel = 1.0;
650
+ }
651
+ currentZoomLevel = Math.min(currentZoomLevel * 1.25, 10.0); // Max 1000%
652
+ applyCustomZoom();
653
+ }
654
+
655
+ // Zoom out by 20%
656
+ function zoomOut() {
657
+ if (currentZoomMode === 'fit') {
658
+ // Switch to custom zoom starting at 100%
659
+ currentZoomLevel = 1.0;
660
+ }
661
+ currentZoomLevel = Math.max(currentZoomLevel * 0.8, 0.1); // Min 10%
662
+ applyCustomZoom();
663
+ }
664
+
665
+ // Apply custom zoom level
666
+ function applyCustomZoom() {
667
+ const container = document.getElementById('image-container');
668
+ const img = document.getElementById('image-display');
669
+
670
+ container.className = 'image-container zoom-custom';
671
+ img.style.transform = '';
672
+ img.style.width = (imageNaturalWidth * currentZoomLevel) + 'px';
673
+ img.style.height = (imageNaturalHeight * currentZoomLevel) + 'px';
674
+
675
+ currentZoomMode = 'custom';
676
+ updateZoomButtons();
677
+ updateZoomLevelDisplay();
678
+ }
679
+
406
680
  // Toggle between annotated view and preview mode
407
681
  function togglePreviewMode() {
408
682
  // Don't allow preview toggle while in textarea edit mode