repoimage 0.2.0

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.
Files changed (71) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/AGENTS.md +28 -0
  3. package/PROJECT-AGENTS.md +55 -0
  4. package/README.md +153 -0
  5. package/TODO.md +132 -0
  6. package/client/index.html +12 -0
  7. package/client/package.json +23 -0
  8. package/client/src/App.tsx +599 -0
  9. package/client/src/components/FsBrowser.tsx +210 -0
  10. package/client/src/components/Settings.tsx +81 -0
  11. package/client/src/index.css +797 -0
  12. package/client/src/lib/api.ts +69 -0
  13. package/client/src/lib/collect.ts +204 -0
  14. package/client/src/lib/format.ts +96 -0
  15. package/client/src/lib/session.ts +58 -0
  16. package/client/src/main.tsx +10 -0
  17. package/client/src/vite-env.d.ts +1 -0
  18. package/client/tsconfig.json +18 -0
  19. package/client/vite.config.ts +27 -0
  20. package/docs/README.md +28 -0
  21. package/docs/api/overview.md +65 -0
  22. package/docs/api/scan.md +188 -0
  23. package/docs/architecture.md +155 -0
  24. package/docs/design/invariants.md +19 -0
  25. package/docs/design/role-system.md +50 -0
  26. package/docs/development/README.md +94 -0
  27. package/docs/features/README.md +21 -0
  28. package/docs/features/compression-score.md +75 -0
  29. package/docs/features/exclusions.md +63 -0
  30. package/docs/features/session.md +64 -0
  31. package/package.json +37 -0
  32. package/server/dist/cli.d.ts +3 -0
  33. package/server/dist/cli.d.ts.map +1 -0
  34. package/server/dist/cli.js +54 -0
  35. package/server/dist/cli.js.map +1 -0
  36. package/server/dist/fs-list.d.ts +3 -0
  37. package/server/dist/fs-list.d.ts.map +1 -0
  38. package/server/dist/fs-list.js +73 -0
  39. package/server/dist/fs-list.js.map +1 -0
  40. package/server/dist/paths.d.ts +3 -0
  41. package/server/dist/paths.d.ts.map +1 -0
  42. package/server/dist/paths.js +12 -0
  43. package/server/dist/paths.js.map +1 -0
  44. package/server/dist/scan.d.ts +16 -0
  45. package/server/dist/scan.d.ts.map +1 -0
  46. package/server/dist/scan.js +158 -0
  47. package/server/dist/scan.js.map +1 -0
  48. package/server/dist/server.d.ts +6 -0
  49. package/server/dist/server.d.ts.map +1 -0
  50. package/server/dist/server.js +313 -0
  51. package/server/dist/server.js.map +1 -0
  52. package/server/package.json +22 -0
  53. package/server/src/cli.ts +63 -0
  54. package/server/src/fs-list.ts +70 -0
  55. package/server/src/paths.ts +6 -0
  56. package/server/src/scan.ts +203 -0
  57. package/server/src/server.ts +356 -0
  58. package/server/tsconfig.json +9 -0
  59. package/shared/package.json +10 -0
  60. package/shared/src/constants.ts +37 -0
  61. package/shared/src/index.ts +4 -0
  62. package/shared/src/role-guess.ts +103 -0
  63. package/shared/src/schema.ts +18 -0
  64. package/shared/src/types.ts +36 -0
  65. package/shared/tsconfig.json +9 -0
  66. package/test/cli.test.js +56 -0
  67. package/test/fs-list.test.js +39 -0
  68. package/test/role-guess.test.js +50 -0
  69. package/test/scan.test.js +150 -0
  70. package/test/server.test.js +308 -0
  71. package/tsconfig.base.json +14 -0
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRepoimageServer = createRepoimageServer;
7
+ const http_1 = __importDefault(require("http"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const shared_1 = require("@repoimage/shared");
12
+ const fs_list_1 = require("./fs-list");
13
+ const scan_1 = require("./scan");
14
+ const paths_1 = require("./paths");
15
+ const MAX_SCAN_ENTRIES = 100_000;
16
+ function isLoopbackAddress(addr) {
17
+ if (addr == null)
18
+ return true;
19
+ const a = String(addr);
20
+ return a === '127.0.0.1' || a === '::1' || a === '::ffff:127.0.0.1';
21
+ }
22
+ function assertLoopbackForFsApi(req, res) {
23
+ if (process.env.REPOIMAGE_FS_TRUST_REMOTE === '1') {
24
+ return true;
25
+ }
26
+ const addr = req.socket.remoteAddress;
27
+ if (!isLoopbackAddress(addr)) {
28
+ sendJson(res, 403, {
29
+ error: 'Filesystem API is only available to loopback clients. Set REPOIMAGE_FS_TRUST_REMOTE=1 to disable this check (not recommended).'
30
+ });
31
+ return false;
32
+ }
33
+ return true;
34
+ }
35
+ function resolveScanFolders(body) {
36
+ if (typeof body.folder === 'string' && body.folder.trim()) {
37
+ return [body.folder.trim()];
38
+ }
39
+ if (Array.isArray(body.folders)) {
40
+ return body.folders
41
+ .map(f => (typeof f === 'string' ? f.trim() : ''))
42
+ .filter(Boolean);
43
+ }
44
+ return [];
45
+ }
46
+ const IMAGE_FILE_EXTS = new Set(shared_1.IMAGE_EXTENSIONS);
47
+ const MIME = {
48
+ '.html': 'text/html; charset=utf-8',
49
+ '.css': 'text/css; charset=utf-8',
50
+ '.js': 'application/javascript; charset=utf-8',
51
+ '.json': 'application/json; charset=utf-8',
52
+ '.ico': 'image/x-icon',
53
+ '.png': 'image/png',
54
+ '.jpg': 'image/jpeg',
55
+ '.jpeg': 'image/jpeg',
56
+ '.gif': 'image/gif',
57
+ '.bmp': 'image/bmp',
58
+ '.tif': 'image/tiff',
59
+ '.tiff': 'image/tiff',
60
+ '.webp': 'image/webp',
61
+ '.svg': 'image/svg+xml',
62
+ '.avif': 'image/avif',
63
+ '.heic': 'image/heic',
64
+ '.heif': 'image/heif'
65
+ };
66
+ function readJsonBody(req) {
67
+ return new Promise((resolve, reject) => {
68
+ const chunks = [];
69
+ req.on('data', chunk => chunks.push(chunk));
70
+ req.on('end', () => {
71
+ const raw = Buffer.concat(chunks).toString('utf8');
72
+ if (!raw.trim()) {
73
+ resolve({});
74
+ return;
75
+ }
76
+ try {
77
+ resolve(JSON.parse(raw));
78
+ }
79
+ catch (e) {
80
+ reject(e);
81
+ }
82
+ });
83
+ req.on('error', reject);
84
+ });
85
+ }
86
+ function send(res, status, body, headers = {}) {
87
+ const buf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8');
88
+ res.writeHead(status, {
89
+ 'Content-Length': buf.length,
90
+ ...headers
91
+ });
92
+ res.end(buf);
93
+ }
94
+ function sendJson(res, status, obj) {
95
+ send(res, status, JSON.stringify(obj), {
96
+ 'Content-Type': 'application/json; charset=utf-8'
97
+ });
98
+ }
99
+ function createRepoimageServer(options = {}) {
100
+ const clientRoot = path_1.default.resolve(options.clientRoot ?? (0, paths_1.defaultClientRoot)());
101
+ function safeStaticFile(urlPath) {
102
+ const decoded = decodeURIComponent((urlPath || '').split('?')[0]);
103
+ if (decoded.includes('..')) {
104
+ return null;
105
+ }
106
+ const rel = decoded === '/' || decoded === '' ? 'index.html' : decoded.replace(/^\/+/, '');
107
+ const full = path_1.default.resolve(path_1.default.join(clientRoot, rel));
108
+ const root = path_1.default.resolve(clientRoot);
109
+ if (full !== root && !full.startsWith(root + path_1.default.sep)) {
110
+ return null;
111
+ }
112
+ return full;
113
+ }
114
+ function serveSpaIndex(res) {
115
+ const indexPath = path_1.default.join(clientRoot, 'index.html');
116
+ if (!fs_1.default.existsSync(indexPath)) {
117
+ send(res, 503, 'Client not built. Run npm run build from the repo root.', { 'Content-Type': 'text/plain; charset=utf-8' });
118
+ return;
119
+ }
120
+ send(res, 200, fs_1.default.readFileSync(indexPath), {
121
+ 'Content-Type': 'text/html; charset=utf-8'
122
+ });
123
+ }
124
+ return http_1.default.createServer(async (req, res) => {
125
+ const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
126
+ if (req.method === 'POST' && url.pathname === '/api/scan') {
127
+ try {
128
+ const body = await readJsonBody(req);
129
+ const rawFolders = resolveScanFolders(body);
130
+ const normalized = rawFolders.map(f => path_1.default.resolve(f)).filter(Boolean);
131
+ if (normalized.length === 0) {
132
+ sendJson(res, 400, {
133
+ error: 'Provide a project root: non-empty string "folder" or "folders" array.'
134
+ });
135
+ return;
136
+ }
137
+ const sortByScore = Boolean(body.sortByScore);
138
+ const excludeCommon = Boolean(body.excludeCommonFolders);
139
+ const { images, issues } = (0, scan_1.findImagesInFolders)(normalized, {
140
+ verbose: false,
141
+ excludeCommon
142
+ });
143
+ const imagesOut = sortByScore ? (0, scan_1.sortByCompressionScore)(images) : images;
144
+ sendJson(res, 200, { images: imagesOut, issues });
145
+ }
146
+ catch (e) {
147
+ sendJson(res, 400, {
148
+ error: 'Invalid JSON body',
149
+ detail: String(e instanceof Error ? e.message : e)
150
+ });
151
+ }
152
+ return;
153
+ }
154
+ if (req.method === 'POST' && url.pathname === '/api/scan-entries') {
155
+ try {
156
+ const body = await readJsonBody(req);
157
+ const entries = Array.isArray(body.entries) ? body.entries : [];
158
+ if (entries.length === 0) {
159
+ sendJson(res, 400, { error: 'Provide a non-empty "entries" array.' });
160
+ return;
161
+ }
162
+ if (entries.length > MAX_SCAN_ENTRIES) {
163
+ sendJson(res, 400, {
164
+ error: `Too many entries (max ${MAX_SCAN_ENTRIES}).`
165
+ });
166
+ return;
167
+ }
168
+ const images = [];
169
+ for (const raw of entries) {
170
+ const row = (0, scan_1.enrichImageEntry)(raw);
171
+ if (row) {
172
+ images.push(row);
173
+ }
174
+ }
175
+ const sortByScore = Boolean(body.sortByScore);
176
+ const imagesOut = sortByScore ? (0, scan_1.sortByCompressionScore)(images) : images;
177
+ sendJson(res, 200, { images: imagesOut, issues: [] });
178
+ }
179
+ catch (e) {
180
+ sendJson(res, 400, {
181
+ error: 'Invalid JSON body',
182
+ detail: String(e instanceof Error ? e.message : e)
183
+ });
184
+ }
185
+ return;
186
+ }
187
+ if (url.pathname === '/api/fs/list') {
188
+ if (!assertLoopbackForFsApi(req, res)) {
189
+ return;
190
+ }
191
+ let rawPath = '';
192
+ if (req.method === 'GET') {
193
+ rawPath = url.searchParams.get('path') ?? '';
194
+ }
195
+ else if (req.method === 'POST') {
196
+ try {
197
+ const body = await readJsonBody(req);
198
+ rawPath = typeof body.path === 'string' ? body.path : '';
199
+ }
200
+ catch {
201
+ sendJson(res, 400, { error: 'Invalid JSON body' });
202
+ return;
203
+ }
204
+ }
205
+ else {
206
+ send(res, 405, 'Method not allowed', { 'Content-Type': 'text/plain; charset=utf-8' });
207
+ return;
208
+ }
209
+ const trimmed = rawPath.trim();
210
+ if (!trimmed) {
211
+ sendJson(res, 400, {
212
+ error: 'Missing path. Use GET ?path=/absolute/path or POST JSON { "path": "/absolute/path" }.'
213
+ });
214
+ return;
215
+ }
216
+ const result = (0, fs_list_1.listDirectory)(trimmed);
217
+ if ('error' in result) {
218
+ let code = 400;
219
+ if (result.error === 'ENOENT')
220
+ code = 404;
221
+ else if (result.error === 'ENOTDIR')
222
+ code = 400;
223
+ else if (result.error === 'EACCES' || result.error === 'ESTAT')
224
+ code = 403;
225
+ sendJson(res, code, result);
226
+ return;
227
+ }
228
+ sendJson(res, 200, result);
229
+ return;
230
+ }
231
+ if (req.method === 'GET' && url.pathname === '/api/fs/home') {
232
+ if (!assertLoopbackForFsApi(req, res)) {
233
+ return;
234
+ }
235
+ const home = os_1.default.homedir();
236
+ sendJson(res, 200, { path: home.split(path_1.default.sep).join('/') });
237
+ return;
238
+ }
239
+ if (req.method === 'GET' && url.pathname === '/api/file') {
240
+ if (!assertLoopbackForFsApi(req, res)) {
241
+ return;
242
+ }
243
+ const raw = url.searchParams.get('path') ?? '';
244
+ const trimmed = raw.trim();
245
+ if (!trimmed) {
246
+ sendJson(res, 400, { error: 'Missing path. Use ?path=/absolute/path/to/image.png' });
247
+ return;
248
+ }
249
+ const resolved = path_1.default.resolve(trimmed);
250
+ const ext = path_1.default.extname(resolved).toLowerCase();
251
+ if (!IMAGE_FILE_EXTS.has(ext)) {
252
+ sendJson(res, 403, { error: 'Not an allowed image type.' });
253
+ return;
254
+ }
255
+ let st;
256
+ try {
257
+ st = fs_1.default.statSync(resolved);
258
+ }
259
+ catch (e) {
260
+ const err = e;
261
+ const code = err.code === 'ENOENT' ? 404 : 403;
262
+ sendJson(res, code, {
263
+ error: err.code ?? 'ESTAT',
264
+ message: String(err.message || e)
265
+ });
266
+ return;
267
+ }
268
+ if (!st.isFile()) {
269
+ sendJson(res, 400, { error: 'ENOTFILE', message: 'Path is not a file.' });
270
+ return;
271
+ }
272
+ const type = MIME[ext] || 'application/octet-stream';
273
+ send(res, 200, fs_1.default.readFileSync(resolved), { 'Content-Type': type });
274
+ return;
275
+ }
276
+ if (req.method === 'GET') {
277
+ let filePath = safeStaticFile(url.pathname);
278
+ if (!filePath) {
279
+ send(res, 403, 'Forbidden', { 'Content-Type': 'text/plain; charset=utf-8' });
280
+ return;
281
+ }
282
+ if (fs_1.default.existsSync(filePath) && fs_1.default.statSync(filePath).isDirectory()) {
283
+ filePath = path_1.default.join(filePath, 'index.html');
284
+ }
285
+ if (!fs_1.default.existsSync(filePath) || !fs_1.default.statSync(filePath).isFile()) {
286
+ if (!url.pathname.startsWith('/api')) {
287
+ serveSpaIndex(res);
288
+ return;
289
+ }
290
+ send(res, 404, 'Not found', { 'Content-Type': 'text/plain; charset=utf-8' });
291
+ return;
292
+ }
293
+ const ext = path_1.default.extname(filePath).toLowerCase();
294
+ const type = MIME[ext] || 'application/octet-stream';
295
+ send(res, 200, fs_1.default.readFileSync(filePath), { 'Content-Type': type });
296
+ return;
297
+ }
298
+ send(res, 405, 'Method not allowed', { 'Content-Type': 'text/plain; charset=utf-8' });
299
+ });
300
+ }
301
+ if (require.main === module) {
302
+ const requested = process.env.PORT;
303
+ const port = requested !== undefined && requested !== '' ? Number(requested) : 3847;
304
+ const host = process.env.REPOIMAGE_BIND || '127.0.0.1';
305
+ const server = createRepoimageServer();
306
+ server.listen(port, host, () => {
307
+ const addr = server.address();
308
+ const p = typeof addr === 'object' && addr ? addr.port : port;
309
+ const h = typeof addr === 'object' && addr ? addr.address : host;
310
+ process.stderr.write(`RepoImage GUI: http://${h}:${p}/\n`);
311
+ });
312
+ }
313
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@repoimage/server",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "main": "./dist/server.js",
6
+ "bin": {
7
+ "repoimage": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "dev": "tsx watch src/server.ts",
12
+ "start": "node dist/server.js"
13
+ },
14
+ "dependencies": {
15
+ "@repoimage/shared": "*",
16
+ "image-size": "^1.0.2"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^22.10.0",
20
+ "tsx": "^4.19.0"
21
+ }
22
+ }
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { findImagesInFolders, sortByCompressionScore } from './scan';
4
+
5
+ const args = process.argv.slice(2);
6
+ const jsonMode = args.includes('--json');
7
+ const sortByScore = args.includes('--sort-by-score');
8
+ const folders = args.filter(arg => !arg.startsWith('--'));
9
+
10
+ if (folders.length === 0) {
11
+ const usage =
12
+ 'Usage: repoimage [--json] [--sort-by-score] <folder1> [folder2] ...\n' +
13
+ 'Example: repoimage --json /path/to/repo\n' +
14
+ ' repoimage --sort-by-score /path/to/Pictures';
15
+ if (jsonMode) {
16
+ process.stderr.write(`${usage}\n`);
17
+ } else {
18
+ console.log(usage);
19
+ }
20
+ process.exit(1);
21
+ }
22
+
23
+ let { images, issues } = findImagesInFolders(folders, { verbose: !jsonMode });
24
+
25
+ if (sortByScore) {
26
+ images = sortByCompressionScore(images);
27
+ }
28
+
29
+ if (jsonMode) {
30
+ process.stdout.write(JSON.stringify({ images, issues }));
31
+ process.stdout.write('\n');
32
+ process.exit(0);
33
+ }
34
+
35
+ if (issues.length) {
36
+ for (const issue of issues) {
37
+ console.error(issue.message);
38
+ }
39
+ if (issues.some(i => i.code === 'MISSING_FOLDER')) {
40
+ console.error('');
41
+ }
42
+ }
43
+
44
+ function formatFileSize(bytes: number): string {
45
+ if (bytes === 0) return '0 B';
46
+ const k = 1024;
47
+ const sizes = ['B', 'KB', 'MB', 'GB'];
48
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
49
+ return `${parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
50
+ }
51
+
52
+ console.log(`\nFound ${images.length} images:`);
53
+ images.forEach((image, index) => {
54
+ const dimensions =
55
+ image.width && image.height ? `${image.width}x${image.height}` : 'unknown';
56
+ const compression = image.compressionRatio ? `${image.compressionRatio}/10` : 'N/A';
57
+ console.log(
58
+ `${index + 1}. ${image.path} (${formatFileSize(image.size)}, ${dimensions}, compression: ${compression})`
59
+ );
60
+ });
61
+
62
+ const totalSize = images.reduce((sum, image) => sum + image.size, 0);
63
+ console.log(`\nTotal: ${images.length} images found (${formatFileSize(totalSize)})`);
@@ -0,0 +1,70 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import type { FsListEntry, FsListResult } from '@repoimage/shared';
4
+
5
+ function typeFromDirent(d: fs.Dirent): FsListEntry['type'] {
6
+ if (d.isSymbolicLink()) return 'symlink';
7
+ if (d.isDirectory()) return 'directory';
8
+ if (d.isFile()) return 'file';
9
+ return 'other';
10
+ }
11
+
12
+ export function listDirectory(inputPath: string): FsListResult {
13
+ const resolved = path.resolve(inputPath);
14
+
15
+ if (!fs.existsSync(resolved)) {
16
+ return { error: 'ENOENT', message: `Path not found: ${resolved}` };
17
+ }
18
+
19
+ let st: fs.Stats;
20
+ try {
21
+ st = fs.statSync(resolved);
22
+ } catch (e) {
23
+ return {
24
+ error: 'ESTAT',
25
+ message: e instanceof Error ? e.message : String(e)
26
+ };
27
+ }
28
+
29
+ if (!st.isDirectory()) {
30
+ return { error: 'ENOTDIR', message: `Not a directory: ${resolved}` };
31
+ }
32
+
33
+ let dirents: fs.Dirent[];
34
+ try {
35
+ dirents = fs.readdirSync(resolved, { withFileTypes: true });
36
+ } catch (e) {
37
+ return {
38
+ error: 'EACCES',
39
+ message: e instanceof Error ? e.message : String(e)
40
+ };
41
+ }
42
+
43
+ const entries: FsListEntry[] = dirents.map(d => {
44
+ const name = d.name;
45
+ const fullPath = path.join(resolved, name);
46
+ return {
47
+ name,
48
+ path: fullPath.split(path.sep).join('/'),
49
+ type: typeFromDirent(d)
50
+ };
51
+ });
52
+
53
+ entries.sort((a, b) => {
54
+ const order: Record<FsListEntry['type'], number> = {
55
+ directory: 0,
56
+ symlink: 1,
57
+ file: 2,
58
+ other: 3
59
+ };
60
+ const oa = order[a.type] ?? 3;
61
+ const ob = order[b.type] ?? 3;
62
+ if (oa !== ob) return oa - ob;
63
+ return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
64
+ });
65
+
66
+ return {
67
+ path: resolved.split(path.sep).join('/'),
68
+ entries
69
+ };
70
+ }
@@ -0,0 +1,6 @@
1
+ import path from 'path';
2
+
3
+ /** Default production client bundle: `client/dist` from repo root. */
4
+ export function defaultClientRoot(): string {
5
+ return path.resolve(__dirname, '../../client/dist');
6
+ }