@must-b/must-b 1.72.7 → 1.72.8
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/dist/BUILD.json +3 -3
- package/dist/embedded-skills.json +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/public/assets/{index-CbW1nI1o.js → index-CRphQOGC.js} +1 -1
- package/dist/public/index.html +1 -1
- package/package.json +3 -2
- package/scripts/build-prod.mjs +307 -0
- package/scripts/bundle-skills.mjs +261 -0
- package/scripts/create-must-b-shim.mjs +45 -0
- package/scripts/install.ps1 +113 -0
- package/scripts/install.sh +112 -0
- package/scripts/paparazzi.mjs +92 -0
- package/scripts/pipeline-check.mjs +75 -0
- package/scripts/replace-assets.cjs +184 -0
- package/scripts/take-screenshots.mjs +91 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pipeline-check.mjs — Skill_Master Pipeline Monitor
|
|
4
|
+
*
|
|
5
|
+
* PIPELINE.md tablosunu okur; Departman=Skill_Master AND Durum=IN_PROGRESS
|
|
6
|
+
* olan satırları bulup stdout'a yazar. Görev yoksa sessiz çıkar.
|
|
7
|
+
*
|
|
8
|
+
* Kullanım:
|
|
9
|
+
* node scripts/pipeline-check.mjs
|
|
10
|
+
*
|
|
11
|
+
* Claude Code UserPromptSubmit hook olarak çalışır —
|
|
12
|
+
* çıktı her oturumda Claude'a otomatik inject edilir.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readFileSync, existsSync } from 'fs';
|
|
16
|
+
import { resolve, dirname } from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const PIPELINE_PATH = resolve(__dirname, '..', 'PIPELINE.md');
|
|
21
|
+
|
|
22
|
+
if (!existsSync(PIPELINE_PATH)) {
|
|
23
|
+
process.exit(0); // PIPELINE.md yoksa sessiz çık
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const content = readFileSync(PIPELINE_PATH, 'utf8');
|
|
27
|
+
|
|
28
|
+
// Markdown tablo satırlarını parse et
|
|
29
|
+
// Format: | ID | Departman | Açıklama | Durum | Not |
|
|
30
|
+
// (separator satırı |---|---| vb. atlanır)
|
|
31
|
+
const TABLE_ROW = /^\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|(?:\s*([^|]*?)\s*\|)?/;
|
|
32
|
+
|
|
33
|
+
const tasks = [];
|
|
34
|
+
|
|
35
|
+
for (const line of content.split('\n')) {
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
if (!trimmed.startsWith('|')) continue;
|
|
38
|
+
if (/^\|[-\s|]+\|$/.test(trimmed)) continue; // separator
|
|
39
|
+
|
|
40
|
+
const m = trimmed.match(TABLE_ROW);
|
|
41
|
+
if (!m) continue;
|
|
42
|
+
|
|
43
|
+
const [, id, department, description, status] = m;
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
department.trim() === 'Skill_Master' &&
|
|
47
|
+
status.trim() === 'IN_PROGRESS'
|
|
48
|
+
) {
|
|
49
|
+
tasks.push({
|
|
50
|
+
id: id.trim(),
|
|
51
|
+
description: description.trim().replace(/\*\*/g, ''), // bold işaretlerini kaldır
|
|
52
|
+
status: status.trim(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tasks.length === 0) {
|
|
58
|
+
process.exit(0); // Görev yok, sessiz çık
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Görev(ler) bulundu — Claude'a inject edilecek mesajı yaz
|
|
62
|
+
const lines = [
|
|
63
|
+
`[PIPELINE] Skill_Master için bekleyen görev${tasks.length > 1 ? 'ler' : ''}:`,
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
for (const t of tasks) {
|
|
67
|
+
lines.push(` • ${t.id} — ${t.description}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('Bu görev(leri) sırayla çalıştır. Her biri tamamlandığında PIPELINE.md\'deki');
|
|
72
|
+
lines.push('ilgili satırın Durum sütununu IN_PROGRESS → DONE olarak güncelle,');
|
|
73
|
+
lines.push('Not sütununa "Skill_Master | ' + new Date().toISOString().slice(0, 10) + '" ekle.');
|
|
74
|
+
|
|
75
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Must-b Asset Replacement — Windows PowerShell GDI+ edition
|
|
4
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
5
|
+
* Zero npm dependencies. Uses Windows' built-in System.Drawing (GDI+)
|
|
6
|
+
* via PowerShell to resize and copy the mascot image to every target.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/replace-assets.cjs "<path-to-image.png>"
|
|
10
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
const src = process.argv[2];
|
|
18
|
+
if (!src) {
|
|
19
|
+
console.error('\n Usage: node scripts/replace-assets.cjs "<path-to-image.png>"\n');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
if (!fs.existsSync(src)) {
|
|
23
|
+
console.error(`\n File not found: ${src}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const root = path.resolve(__dirname, '..');
|
|
28
|
+
const srcAbs = path.resolve(src);
|
|
29
|
+
|
|
30
|
+
const cyan = s => `\x1b[38;2;0;204;255m${s}\x1b[0m`;
|
|
31
|
+
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
32
|
+
const red = s => `\x1b[31m${s}\x1b[0m`;
|
|
33
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
34
|
+
|
|
35
|
+
// ── Target list ────────────────────────────────────────────────────────────
|
|
36
|
+
const TARGETS = [
|
|
37
|
+
// Web app
|
|
38
|
+
{ file: 'public/Luma/public/logo.png', w: 512, h: 512 },
|
|
39
|
+
{ file: 'public/Luma/public/apple-touch-icon.png', w: 180, h: 180 },
|
|
40
|
+
{ file: 'public/Luma/public/favicon-96x96.png', w: 96, h: 96 },
|
|
41
|
+
{ file: 'public/Luma/public/favicon-48x48.png', w: 48, h: 48 },
|
|
42
|
+
{ file: 'public/Luma/public/favicon-32x32.png', w: 32, h: 32 },
|
|
43
|
+
// macOS
|
|
44
|
+
{ file: 'apps/macos/Icon.icon/Assets/must-b-mac.png', w: 1024, h: 1024 },
|
|
45
|
+
// Android
|
|
46
|
+
{ file: 'apps/android/app/src/main/res/mipmap-mdpi/ic_launcher.png', w: 48, h: 48 },
|
|
47
|
+
{ file: 'apps/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png', w: 48, h: 48 },
|
|
48
|
+
{ file: 'apps/android/app/src/main/res/mipmap-hdpi/ic_launcher.png', w: 72, h: 72 },
|
|
49
|
+
{ file: 'apps/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png', w: 72, h: 72 },
|
|
50
|
+
{ file: 'apps/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png', w: 96, h: 96 },
|
|
51
|
+
{ file: 'apps/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png',w: 96, h: 96 },
|
|
52
|
+
{ file: 'apps/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png', w: 144, h: 144 },
|
|
53
|
+
{ file: 'apps/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png',w:144, h: 144 },
|
|
54
|
+
{ file: 'apps/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png', w: 192, h: 192 },
|
|
55
|
+
{ file: 'apps/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png',w:192,h: 192 },
|
|
56
|
+
// iOS app icons
|
|
57
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/29.png', w: 29, h: 29 },
|
|
58
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/40.png', w: 40, h: 40 },
|
|
59
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/48.png', w: 48, h: 48 },
|
|
60
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/55.png', w: 55, h: 55 },
|
|
61
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/57.png', w: 57, h: 57 },
|
|
62
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/58.png', w: 58, h: 58 },
|
|
63
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/60.png', w: 60, h: 60 },
|
|
64
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/66.png', w: 66, h: 66 },
|
|
65
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/80.png', w: 80, h: 80 },
|
|
66
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/87.png', w: 87, h: 87 },
|
|
67
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/88.png', w: 88, h: 88 },
|
|
68
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/92.png', w: 92, h: 92 },
|
|
69
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/100.png', w: 100, h: 100 },
|
|
70
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/102.png', w: 102, h: 102 },
|
|
71
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/108.png', w: 108, h: 108 },
|
|
72
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/114.png', w: 114, h: 114 },
|
|
73
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/120.png', w: 120, h: 120 },
|
|
74
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/172.png', w: 172, h: 172 },
|
|
75
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/180.png', w: 180, h: 180 },
|
|
76
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/196.png', w: 196, h: 196 },
|
|
77
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/216.png', w: 216, h: 216 },
|
|
78
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/234.png', w: 234, h: 234 },
|
|
79
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/258.png', w: 258, h: 258 },
|
|
80
|
+
{ file: 'apps/ios/Sources/Assets.xcassets/AppIcon.appiconset/1024.png', w: 1024, h: 1024 },
|
|
81
|
+
// iOS Watch
|
|
82
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-38@2x.png', w: 76, h: 76 },
|
|
83
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-40@2x.png', w: 80, h: 80 },
|
|
84
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-41@2x.png', w: 82, h: 82 },
|
|
85
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-44@2x.png', w: 88, h: 88 },
|
|
86
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-app-45@2x.png', w: 90, h: 90 },
|
|
87
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@2x.png', w: 58, h: 58 },
|
|
88
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-companion-29@3x.png', w: 87, h: 87 },
|
|
89
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-marketing-1024.png', w: 1024, h: 1024 },
|
|
90
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-38@2x.png', w: 48, h: 48 },
|
|
91
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-notification-42@2x.png', w: 48, h: 48 },
|
|
92
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-38@2x.png', w: 76, h: 76 },
|
|
93
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-42@2x.png', w: 84, h: 84 },
|
|
94
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-44@2x.png', w: 88, h: 88 },
|
|
95
|
+
{ file: 'apps/ios/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-quicklook-45@2x.png', w: 90, h: 90 },
|
|
96
|
+
// Chrome extension
|
|
97
|
+
{ file: 'src/core/assets/chrome-extension/icons/icon16.png', w: 16, h: 16 },
|
|
98
|
+
{ file: 'src/core/assets/chrome-extension/icons/icon32.png', w: 32, h: 32 },
|
|
99
|
+
{ file: 'src/core/assets/chrome-extension/icons/icon48.png', w: 48, h: 48 },
|
|
100
|
+
{ file: 'src/core/assets/chrome-extension/icons/icon128.png', w: 128, h: 128 },
|
|
101
|
+
// DMG backgrounds
|
|
102
|
+
{ file: 'src/core/assets/dmg-background.png', w: 800, h: 600 },
|
|
103
|
+
{ file: 'src/core/assets/dmg-background-small.png', w: 400, h: 300 },
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// ── PowerShell resize helper ───────────────────────────────────────────────
|
|
107
|
+
function resizeWithPowerShell(srcFile, destFile, w, h) {
|
|
108
|
+
const ps = `
|
|
109
|
+
Add-Type -AssemblyName System.Drawing
|
|
110
|
+
$src = New-Object System.Drawing.Bitmap([string]'${srcFile.replace(/'/g, "''")}')
|
|
111
|
+
$dst = New-Object System.Drawing.Bitmap([int]${w}, [int]${h})
|
|
112
|
+
$g = [System.Drawing.Graphics]::FromImage($dst)
|
|
113
|
+
$g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
|
114
|
+
$g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
|
115
|
+
$g.DrawImage($src, 0, 0, [int]${w}, [int]${h})
|
|
116
|
+
$g.Dispose()
|
|
117
|
+
$dst.Save([string]'${destFile.replace(/'/g, "''")}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
118
|
+
$src.Dispose()
|
|
119
|
+
$dst.Dispose()
|
|
120
|
+
`.trim();
|
|
121
|
+
|
|
122
|
+
const result = spawnSync('powershell.exe', [
|
|
123
|
+
'-NoProfile', '-NonInteractive', '-Command', ps
|
|
124
|
+
], { encoding: 'utf-8' });
|
|
125
|
+
|
|
126
|
+
if (result.status !== 0) {
|
|
127
|
+
throw new Error((result.stderr || result.stdout || 'PowerShell error').trim().slice(0, 200));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Main ──────────────────────────────────────────────────────────────────
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(cyan(' Must-b — Red Panda Asset Replacement'));
|
|
134
|
+
console.log(cyan(' ─────────────────────────────────────────────'));
|
|
135
|
+
console.log(dim(` Source : ${srcAbs}`));
|
|
136
|
+
console.log(dim(` Engine : Windows PowerShell GDI+ (zero npm deps)`));
|
|
137
|
+
console.log('');
|
|
138
|
+
|
|
139
|
+
let ok = 0, skipped = 0, failed = 0;
|
|
140
|
+
|
|
141
|
+
for (const t of TARGETS) {
|
|
142
|
+
const dest = path.join(root, t.file);
|
|
143
|
+
const destWin = dest.replace(/\//g, '\\');
|
|
144
|
+
const srcWin = srcAbs.replace(/\//g, '\\');
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(dest)) {
|
|
147
|
+
skipped++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
resizeWithPowerShell(srcWin, destWin, t.w, t.h);
|
|
153
|
+
console.log(` ${green('✓')} ${t.file} ${dim(`${t.w}×${t.h}`)}`);
|
|
154
|
+
ok++;
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.log(` ${red('✗')} ${t.file} ${dim(String(err.message).split('\n')[0])}`);
|
|
157
|
+
failed++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// favicon.ico — copy 32×32 as PNG-encoded ico
|
|
162
|
+
const icoPath = path.join(root, 'public', 'Luma', 'public', 'favicon.ico');
|
|
163
|
+
if (fs.existsSync(icoPath)) {
|
|
164
|
+
try {
|
|
165
|
+
resizeWithPowerShell(
|
|
166
|
+
srcAbs.replace(/\//g, '\\'),
|
|
167
|
+
icoPath.replace(/\//g, '\\'),
|
|
168
|
+
32, 32
|
|
169
|
+
);
|
|
170
|
+
console.log(` ${green('✓')} public/Luma/public/favicon.ico ${dim('32×32')}`);
|
|
171
|
+
ok++;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.log(` ${red('✗')} favicon.ico ${dim(String(err.message).split('\n')[0])}`);
|
|
174
|
+
failed++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log('');
|
|
179
|
+
if (failed === 0) {
|
|
180
|
+
console.log(green(` ✔ Done! ${ok} file(s) replaced with Red Panda mascot.`));
|
|
181
|
+
} else {
|
|
182
|
+
console.log(` ${green(ok + ' replaced')} · ${dim(skipped + ' skipped')} · ${red(failed + ' failed')}`);
|
|
183
|
+
}
|
|
184
|
+
console.log('');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* take-screenshots.mjs — Must-b local screenshot utility
|
|
3
|
+
*
|
|
4
|
+
* Usage: node scripts/take-screenshots.mjs
|
|
5
|
+
*
|
|
6
|
+
* Requires the Must-b server to be running on localhost:4309.
|
|
7
|
+
* Saves PNG files to local_screenshots/ (git + npm ignored).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { chromium } from 'playwright';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
17
|
+
const OUT_DIR = path.join(ROOT, 'local_screenshots');
|
|
18
|
+
const BASE_URL = 'http://localhost:4309';
|
|
19
|
+
const WAIT_MS = 2000; // extra settle time after networkidle
|
|
20
|
+
|
|
21
|
+
const ROUTES = [
|
|
22
|
+
{ name: 'dashboard', path: '/app' },
|
|
23
|
+
{ name: 'automations', path: '/app/automations' },
|
|
24
|
+
{ name: 'skills', path: '/app/skills' },
|
|
25
|
+
{ name: 'plugins', path: '/app/plugins' },
|
|
26
|
+
{ name: 'files', path: '/app/files' },
|
|
27
|
+
{ name: 'settings', path: '/app/settings' },
|
|
28
|
+
{ name: 'memory', path: '/app/memory' },
|
|
29
|
+
{ name: 'browser', path: '/app/browser' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// ── Preflight: ensure server is reachable ────────────────────────────────────
|
|
33
|
+
async function waitForServer(timeoutMs = 15_000) {
|
|
34
|
+
const start = Date.now();
|
|
35
|
+
process.stdout.write(' Waiting for server at ' + BASE_URL + ' …');
|
|
36
|
+
while (Date.now() - start < timeoutMs) {
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch(`${BASE_URL}/api/setup/status`, { signal: AbortSignal.timeout(1000) });
|
|
39
|
+
if (res.status < 500) { process.stdout.write(' ready!\n'); return; }
|
|
40
|
+
} catch { /* not ready */ }
|
|
41
|
+
await new Promise(r => setTimeout(r, 400));
|
|
42
|
+
process.stdout.write('.');
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
throw new Error('Server did not respond within ' + timeoutMs + 'ms — is "must-b" running?');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
49
|
+
async function main() {
|
|
50
|
+
console.log('\n Must-b Screenshot Tool\n ─────────────────────');
|
|
51
|
+
|
|
52
|
+
await waitForServer();
|
|
53
|
+
|
|
54
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
55
|
+
|
|
56
|
+
const browser = await chromium.launch({ headless: true });
|
|
57
|
+
const context = await browser.newContext({
|
|
58
|
+
viewport: { width: 1440, height: 900 },
|
|
59
|
+
colorScheme: 'dark',
|
|
60
|
+
deviceScaleFactor: 1,
|
|
61
|
+
});
|
|
62
|
+
const page = await context.newPage();
|
|
63
|
+
|
|
64
|
+
let ok = 0;
|
|
65
|
+
let fail = 0;
|
|
66
|
+
|
|
67
|
+
for (const route of ROUTES) {
|
|
68
|
+
const url = BASE_URL + route.path;
|
|
69
|
+
const outFile = path.join(OUT_DIR, `${route.name}.png`);
|
|
70
|
+
process.stdout.write(` → ${route.path.padEnd(22)}`);
|
|
71
|
+
try {
|
|
72
|
+
await page.goto(url, { waitUntil: 'networkidle', timeout: 20_000 });
|
|
73
|
+
// Extra settle for animations / deferred data fetches
|
|
74
|
+
await page.waitForTimeout(WAIT_MS);
|
|
75
|
+
await page.screenshot({ path: outFile, fullPage: false });
|
|
76
|
+
process.stdout.write(` ✓ saved: local_screenshots/${route.name}.png\n`);
|
|
77
|
+
ok++;
|
|
78
|
+
} catch (e) {
|
|
79
|
+
process.stdout.write(` ✗ ${e.message.split('\n')[0]}\n`);
|
|
80
|
+
fail++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await browser.close();
|
|
85
|
+
|
|
86
|
+
console.log(`\n ─────────────────────`);
|
|
87
|
+
console.log(` Done — ${ok} captured, ${fail} failed.`);
|
|
88
|
+
console.log(` Screenshots saved to: ${OUT_DIR}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
main().catch(err => { console.error('\n Error:', err.message); process.exit(1); });
|