@monoes/monodesign 1.0.1 → 1.1.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/NOTICE.md +7 -19
- package/cli/bin/cli.js +0 -1
- package/cli/bin/commands/ignores.mjs +0 -1
- package/cli/engine/browser/injected/index.mjs +0 -1
- package/cli/engine/cli/main.mjs +0 -1
- package/cli/engine/design-system.mjs +0 -1
- package/cli/engine/detect-antipatterns-browser.js +0 -1
- package/cli/engine/detect-antipatterns.mjs +0 -1
- package/cli/engine/engines/browser/detect-url.mjs +13 -32
- package/cli/engine/engines/browser/drivers.mjs +258 -0
- package/cli/engine/engines/regex/detect-text.mjs +0 -1
- package/cli/engine/engines/static-html/css-cascade.mjs +0 -1
- package/cli/engine/engines/static-html/detect-html.mjs +0 -1
- package/cli/engine/engines/visual/screenshot-contrast.mjs +9 -1
- package/cli/engine/findings.mjs +0 -1
- package/cli/engine/node/file-system.mjs +0 -1
- package/cli/engine/profile/profiler.mjs +0 -1
- package/cli/engine/registry/antipatterns.mjs +0 -1
- package/cli/engine/rules/checks.mjs +0 -1
- package/cli/engine/shared/color.mjs +0 -1
- package/cli/engine/shared/constants.mjs +0 -1
- package/cli/engine/shared/fonts.mjs +0 -1
- package/cli/engine/shared/inline-ignores.mjs +0 -1
- package/cli/engine/shared/page.mjs +0 -1
- package/cli/lib/monodesign-config.mjs +0 -1
- package/package.json +2 -1
- package/skill/SKILL.src.md +1 -1
- package/skill/scripts/context-signals.mjs +0 -1
- package/skill/scripts/context.mjs +0 -1
- package/skill/scripts/critique-storage.mjs +0 -1
- package/skill/scripts/detect-csp.mjs +0 -1
- package/skill/scripts/detect.mjs +0 -1
- package/skill/scripts/hook-admin.mjs +0 -1
- package/skill/scripts/hook-before-edit.mjs +0 -1
- package/skill/scripts/hook-lib.mjs +0 -1
- package/skill/scripts/hook.mjs +0 -1
- package/skill/scripts/lib/design-parser.mjs +0 -1
- package/skill/scripts/lib/is-generated.mjs +0 -1
- package/skill/scripts/lib/monodesign-paths.mjs +0 -1
- package/skill/scripts/lib/provider.mjs +0 -1
- package/skill/scripts/lib/target-args.mjs +0 -1
- package/skill/scripts/live/browser-script-parts.mjs +0 -1
- package/skill/scripts/live/completion.mjs +0 -1
- package/skill/scripts/live/event-validation.mjs +0 -1
- package/skill/scripts/live/insert-ui.mjs +0 -1
- package/skill/scripts/live/manual-apply.mjs +0 -1
- package/skill/scripts/live/manual-edit-routes.mjs +0 -1
- package/skill/scripts/live/manual-edits-buffer.mjs +0 -1
- package/skill/scripts/live/session-store.mjs +0 -1
- package/skill/scripts/live/svelte-component.mjs +0 -1
- package/skill/scripts/live/sveltekit-adapter.mjs +0 -1
- package/skill/scripts/live/ui-core.mjs +0 -1
- package/skill/scripts/live/vocabulary.mjs +0 -1
- package/skill/scripts/live-accept.mjs +0 -1
- package/skill/scripts/live-browser-dom.js +0 -1
- package/skill/scripts/live-browser-session.js +0 -1
- package/skill/scripts/live-browser.js +0 -1
- package/skill/scripts/live-commit-manual-edits.mjs +0 -1
- package/skill/scripts/live-complete.mjs +0 -1
- package/skill/scripts/live-copy-edit-agent.mjs +0 -1
- package/skill/scripts/live-discard-manual-edits.mjs +0 -1
- package/skill/scripts/live-inject.mjs +0 -1
- package/skill/scripts/live-insert.mjs +0 -1
- package/skill/scripts/live-manual-edit-evidence.mjs +0 -1
- package/skill/scripts/live-poll.mjs +0 -1
- package/skill/scripts/live-resume.mjs +0 -1
- package/skill/scripts/live-server.mjs +0 -1
- package/skill/scripts/live-status.mjs +0 -1
- package/skill/scripts/live-target.mjs +0 -1
- package/skill/scripts/live-wrap.mjs +0 -1
- package/skill/scripts/live.mjs +0 -1
- package/skill/scripts/modern-screenshot.umd.js +0 -1
- package/skill/scripts/palette.mjs +0 -1
- package/skill/scripts/pin.mjs +0 -1
- package/src/tokens.css +0 -1
package/NOTICE.md
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Notices
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Monodesign is an independent monomind project.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
**Original license:** Apache-2.0
|
|
11
|
-
**Author:** Paul Bakaus
|
|
12
|
-
|
|
13
|
-
Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
14
|
-
|
|
15
|
-
## Platform Design Skills
|
|
16
|
-
|
|
17
|
-
The `skill/reference/ios.md` and `skill/reference/android.md` platform reference files are distilled from ehmo's `platform-design-skills` (Apple Human Interface Guidelines and Material Design 3 rules), rewritten in the skill's voice.
|
|
18
|
-
|
|
19
|
-
**Original work:** https://github.com/ehmo/platform-design-skills
|
|
20
|
-
**Original license:** MIT
|
|
21
|
-
**Author:** ehmo
|
|
5
|
+
Portions of this package incorporate code originally published under the
|
|
6
|
+
Apache License 2.0 by Paul Bakaus (impeccable, https://github.com/pbakaus/impeccable)
|
|
7
|
+
and guidance distilled from ehmo/platform-design-skills (MIT). This notice is
|
|
8
|
+
retained to satisfy those licenses; monodesign is developed and maintained
|
|
9
|
+
independently.
|
package/cli/bin/cli.js
CHANGED
package/cli/engine/cli/main.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { fileURLToPath } from 'node:url';
|
|
@@ -7,6 +6,7 @@ import { finding } from '../../findings.mjs';
|
|
|
7
6
|
import { filterByProviders } from '../../registry/antipatterns.mjs';
|
|
8
7
|
import { profileFindingsAsync, profileStep, profileStepAsync } from '../../profile/profiler.mjs';
|
|
9
8
|
import { captureVisualContrastCandidate } from '../visual/screenshot-contrast.mjs';
|
|
9
|
+
import { launchDetectionBrowser, normalizeBrowserHandle } from './drivers.mjs';
|
|
10
10
|
|
|
11
11
|
function serializeDesignSystemForBrowser(designSystem) {
|
|
12
12
|
if (!designSystem?.present) return null;
|
|
@@ -101,7 +101,8 @@ async function runVisualContrastFallback(page, serializedGroups, options, profil
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
|
-
//
|
|
104
|
+
// Browser detection (for URLs) — driver-based: monobrowse (native CDP,
|
|
105
|
+
// preferred) with puppeteer as fallback. See ./drivers.mjs for the seam.
|
|
105
106
|
// ---------------------------------------------------------------------------
|
|
106
107
|
|
|
107
108
|
async function detectUrl(url, options = {}) {
|
|
@@ -109,20 +110,9 @@ async function detectUrl(url, options = {}) {
|
|
|
109
110
|
const waitUntil = options?.waitUntil || 'networkidle0';
|
|
110
111
|
const settleMs = Number.isFinite(options?.settleMs) ? options.settleMs : 0;
|
|
111
112
|
const viewport = options?.viewport || { width: 1280, height: 800 };
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
puppeteer = await profileStepAsync(profile, {
|
|
117
|
-
engine: 'browser',
|
|
118
|
-
phase: 'setup',
|
|
119
|
-
ruleId: 'import-puppeteer',
|
|
120
|
-
target: url,
|
|
121
|
-
}, () => import('puppeteer'));
|
|
122
|
-
} catch {
|
|
123
|
-
throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
|
|
124
|
-
}
|
|
125
|
-
}
|
|
113
|
+
// options.browser accepts either a driver handle (from createBrowserDetector)
|
|
114
|
+
// or a raw puppeteer Browser (legacy callers).
|
|
115
|
+
const externalBrowser = options?.browser ? normalizeBrowserHandle(options.browser) : null;
|
|
126
116
|
|
|
127
117
|
// Read the browser detection script — reuse it instead of reimplementing
|
|
128
118
|
const browserScriptPath = path.resolve(
|
|
@@ -143,16 +133,12 @@ async function detectUrl(url, options = {}) {
|
|
|
143
133
|
throw new Error(`Browser script not found at ${browserScriptPath}`);
|
|
144
134
|
}
|
|
145
135
|
|
|
146
|
-
// CI runners (GitHub Actions Ubuntu) block unprivileged user namespaces, so
|
|
147
|
-
// Chrome can't initialize its sandbox there. Disable the sandbox only when
|
|
148
|
-
// running in CI; local users keep the default hardened launch.
|
|
149
|
-
const launchArgs = process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : [];
|
|
150
136
|
const browser = externalBrowser || await profileStepAsync(profile, {
|
|
151
137
|
engine: 'browser',
|
|
152
138
|
phase: 'load',
|
|
153
139
|
ruleId: 'launch-browser',
|
|
154
140
|
target: url,
|
|
155
|
-
}, () =>
|
|
141
|
+
}, () => launchDetectionBrowser({ launchArgs: options?.launchArgs, headless: true }));
|
|
156
142
|
const page = await profileStepAsync(profile, {
|
|
157
143
|
engine: 'browser',
|
|
158
144
|
phase: 'load',
|
|
@@ -243,17 +229,12 @@ async function detectUrl(url, options = {}) {
|
|
|
243
229
|
}
|
|
244
230
|
|
|
245
231
|
async function createBrowserDetector(options = {}) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
|
|
253
|
-
const browser = options.browser || await puppeteer.default.launch({
|
|
254
|
-
headless: options.headless ?? true,
|
|
255
|
-
args: launchArgs,
|
|
256
|
-
});
|
|
232
|
+
// Pooled reuse for multiple URLs. `browser` is a driver handle (monobrowse
|
|
233
|
+
// or puppeteer); pass a raw puppeteer Browser via options.browser to reuse
|
|
234
|
+
// an externally managed instance.
|
|
235
|
+
const browser = options.browser
|
|
236
|
+
? normalizeBrowserHandle(options.browser)
|
|
237
|
+
: await launchDetectionBrowser({ launchArgs: options.launchArgs, headless: options.headless ?? true });
|
|
257
238
|
const ownsBrowser = !options.browser;
|
|
258
239
|
const defaults = {
|
|
259
240
|
waitUntil: options.waitUntil || 'load',
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Browser driver seam for URL detection.
|
|
3
|
+
//
|
|
4
|
+
// The detection engine (detect-url.mjs) and the visual-contrast fallback
|
|
5
|
+
// (engines/visual/screenshot-contrast.mjs) only need a tiny, puppeteer-shaped
|
|
6
|
+
// page surface:
|
|
7
|
+
//
|
|
8
|
+
// browser: { driverName, newPage(): Promise<page>, close(): Promise<void> }
|
|
9
|
+
// page: {
|
|
10
|
+
// setViewport({ width, height })
|
|
11
|
+
// goto(url, { waitUntil, timeout })
|
|
12
|
+
// evaluate(fnOrExpressionString, ...serializableArgs) -> serializable value
|
|
13
|
+
// screenshot({ encoding: 'base64', clip?, captureBeyondViewport? }) -> base64 string
|
|
14
|
+
// close()
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// Two drivers implement it:
|
|
18
|
+
// - monobrowse (preferred): @monoes/monobrowse, the monorepo's native CDP
|
|
19
|
+
// client. Drives a locally installed Chrome/Edge/Chromium — no bundled
|
|
20
|
+
// Chromium download needed. Supports the full surface including clip
|
|
21
|
+
// screenshots (Page.captureScreenshot) and evaluate roundtrips
|
|
22
|
+
// (Runtime.evaluate), so the visual-contrast pixel fallback runs under it.
|
|
23
|
+
// - puppeteer (fallback): the original optionalDependency path, kept for
|
|
24
|
+
// environments that have puppeteer + its downloaded Chrome but no system
|
|
25
|
+
// browser. A raw puppeteer Page already satisfies the page surface, so the
|
|
26
|
+
// puppeteer driver returns real puppeteer pages untouched.
|
|
27
|
+
//
|
|
28
|
+
// Selection: MONODESIGN_BROWSER_DRIVER=monobrowse|puppeteer forces one;
|
|
29
|
+
// otherwise monobrowse is tried first and puppeteer is the fallback.
|
|
30
|
+
|
|
31
|
+
const DRIVER_ENV = 'MONODESIGN_BROWSER_DRIVER';
|
|
32
|
+
|
|
33
|
+
// Ports we launch headless Chrome on for detection runs. Deliberately away
|
|
34
|
+
// from 9222 (the default `monomind browse` port): we must never attach to a
|
|
35
|
+
// browser we did not launch, because browser.close() issues Browser.close.
|
|
36
|
+
const MONOBROWSE_PORT_MIN = 9520;
|
|
37
|
+
const MONOBROWSE_PORT_RANGE = 380;
|
|
38
|
+
|
|
39
|
+
function serializeEvalArg(arg) {
|
|
40
|
+
const json = JSON.stringify(arg);
|
|
41
|
+
return json === undefined ? 'undefined' : json;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function mapWaitUntil(waitUntil) {
|
|
45
|
+
if (waitUntil === 'domcontentloaded') return 'domcontentloaded';
|
|
46
|
+
if (waitUntil === 'networkidle0' || waitUntil === 'networkidle2' || waitUntil === 'networkidle') return 'networkidle';
|
|
47
|
+
return 'load';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withTimeout(promise, ms, label) {
|
|
51
|
+
let handle;
|
|
52
|
+
const timeout = new Promise((_, reject) => {
|
|
53
|
+
handle = setTimeout(() => reject(new Error(`Timeout waiting for ${label} after ${ms}ms`)), ms);
|
|
54
|
+
handle.unref?.();
|
|
55
|
+
});
|
|
56
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(handle));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// monobrowse driver
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function createMonobrowsePage(mb, client, sessionId, targetId, port) {
|
|
64
|
+
return {
|
|
65
|
+
async setViewport({ width, height }) {
|
|
66
|
+
await mb.setViewport(client, sessionId, width, height);
|
|
67
|
+
},
|
|
68
|
+
async goto(url, { waitUntil = 'load', timeout = 30000 } = {}) {
|
|
69
|
+
const condition = mapWaitUntil(waitUntil);
|
|
70
|
+
if (condition === 'networkidle') {
|
|
71
|
+
const nav = await client.send('Page.navigate', { url }, sessionId);
|
|
72
|
+
if (nav?.errorText) throw new Error(`Navigation to ${url} failed: ${nav.errorText}`);
|
|
73
|
+
await mb.waitForLoad(client, sessionId, 'networkidle', timeout);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// Register the lifecycle listener BEFORE navigating so a fast page load
|
|
77
|
+
// cannot slip past it (waitForLoad's readyState pre-check would otherwise
|
|
78
|
+
// race against the previous document's readyState).
|
|
79
|
+
const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
|
|
80
|
+
const [eventPromise, cancel] = client.onceWithOff(event, sessionId);
|
|
81
|
+
try {
|
|
82
|
+
const nav = await client.send('Page.navigate', { url }, sessionId);
|
|
83
|
+
if (nav?.errorText) throw new Error(`Navigation to ${url} failed: ${nav.errorText}`);
|
|
84
|
+
await withTimeout(eventPromise, timeout, condition);
|
|
85
|
+
} finally {
|
|
86
|
+
cancel();
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async evaluate(fnOrExpression, ...args) {
|
|
90
|
+
const expression = typeof fnOrExpression === 'function'
|
|
91
|
+
? `(${String(fnOrExpression)})(${args.map(serializeEvalArg).join(', ')})`
|
|
92
|
+
: String(fnOrExpression);
|
|
93
|
+
const result = await client.send('Runtime.evaluate', {
|
|
94
|
+
expression,
|
|
95
|
+
returnByValue: true,
|
|
96
|
+
awaitPromise: true,
|
|
97
|
+
}, sessionId);
|
|
98
|
+
if (result?.exceptionDetails) {
|
|
99
|
+
const details = result.exceptionDetails;
|
|
100
|
+
throw new Error(details.exception?.description || details.exception?.value || details.text || 'Evaluation failed');
|
|
101
|
+
}
|
|
102
|
+
return result?.result?.value;
|
|
103
|
+
},
|
|
104
|
+
// Always returns a base64 string (the only mode the engine uses).
|
|
105
|
+
async screenshot({ clip } = {}) {
|
|
106
|
+
const params = { format: 'png' };
|
|
107
|
+
if (clip) {
|
|
108
|
+
params.clip = { x: clip.x, y: clip.y, width: clip.width, height: clip.height, scale: 1 };
|
|
109
|
+
params.captureBeyondViewport = true;
|
|
110
|
+
}
|
|
111
|
+
const result = await client.send('Page.captureScreenshot', params, sessionId);
|
|
112
|
+
return result?.data;
|
|
113
|
+
},
|
|
114
|
+
async close() {
|
|
115
|
+
// HTTP endpoint works regardless of which domains the ws connection has.
|
|
116
|
+
try {
|
|
117
|
+
await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(targetId)}`);
|
|
118
|
+
} catch {
|
|
119
|
+
// Tab may already be gone; browser close cleans up anything left.
|
|
120
|
+
}
|
|
121
|
+
client.close();
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function launchMonobrowseBrowser(options = {}) {
|
|
127
|
+
const mb = await import('@monoes/monobrowse');
|
|
128
|
+
const headless = options.headless ?? true;
|
|
129
|
+
const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
|
|
130
|
+
|
|
131
|
+
const forcedPort = Number(process.env.MONODESIGN_MONOBROWSE_PORT);
|
|
132
|
+
let port = null;
|
|
133
|
+
if (Number.isInteger(forcedPort) && forcedPort > 0) {
|
|
134
|
+
port = forcedPort;
|
|
135
|
+
} else {
|
|
136
|
+
// Pick a free-looking port; skip ports where a CDP endpoint already
|
|
137
|
+
// listens so we never take over (and later Browser.close) someone
|
|
138
|
+
// else's browser session.
|
|
139
|
+
for (let attempt = 0; attempt < 5 && port === null; attempt++) {
|
|
140
|
+
const candidate = MONOBROWSE_PORT_MIN + Math.floor(Math.random() * MONOBROWSE_PORT_RANGE);
|
|
141
|
+
if (!(await mb.isPortOpen(candidate))) port = candidate;
|
|
142
|
+
}
|
|
143
|
+
if (port === null) throw new Error('Could not find a free CDP port for monobrowse');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const launchedPort = await mb.launchBrowser({ port, headless, args: launchArgs });
|
|
147
|
+
// Control connection (to the initial about:blank tab) — used for lifecycle
|
|
148
|
+
// commands; detection pages get their own dedicated connections.
|
|
149
|
+
const control = await mb.connectToTarget(launchedPort);
|
|
150
|
+
|
|
151
|
+
async function openPage() {
|
|
152
|
+
const target = await mb.fetchNewTarget(launchedPort, 'about:blank');
|
|
153
|
+
const wsUrl = target.webSocketDebuggerUrl || `ws://127.0.0.1:${launchedPort}/devtools/page/${target.id}`;
|
|
154
|
+
const client = new mb.CdpClient();
|
|
155
|
+
await client.connect(wsUrl);
|
|
156
|
+
const { sessionId } = await client.send('Target.attachToTarget', { targetId: target.id, flatten: true });
|
|
157
|
+
await Promise.all([
|
|
158
|
+
client.send('Page.enable', {}, sessionId),
|
|
159
|
+
client.send('Runtime.enable', {}, sessionId),
|
|
160
|
+
client.send('Network.enable', {}, sessionId),
|
|
161
|
+
]);
|
|
162
|
+
return createMonobrowsePage(mb, client, sessionId, target.id, launchedPort);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
driverName: 'monobrowse',
|
|
167
|
+
newPage: openPage,
|
|
168
|
+
async close() {
|
|
169
|
+
try {
|
|
170
|
+
if (typeof mb.closeBrowser === 'function') {
|
|
171
|
+
// Newer monobrowse: graceful Browser.close with PID-kill fallback.
|
|
172
|
+
await mb.closeBrowser(control.client, launchedPort);
|
|
173
|
+
} else {
|
|
174
|
+
// Published 1.0.3 has no closeBrowser export — send the CDP
|
|
175
|
+
// Browser.close command directly (best effort, short timeout).
|
|
176
|
+
await withTimeout(control.client.send('Browser.close', {}), 3000, 'Browser.close').catch(() => {});
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
control.client.close();
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// puppeteer driver (original behavior, kept as fallback)
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
function wrapPuppeteerBrowser(browser) {
|
|
190
|
+
return {
|
|
191
|
+
driverName: 'puppeteer',
|
|
192
|
+
raw: browser,
|
|
193
|
+
newPage: () => browser.newPage(),
|
|
194
|
+
close: () => browser.close(),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function launchPuppeteerBrowser(options = {}) {
|
|
199
|
+
let puppeteer;
|
|
200
|
+
try {
|
|
201
|
+
puppeteer = await import('puppeteer');
|
|
202
|
+
} catch {
|
|
203
|
+
throw new Error('puppeteer is required for URL scanning. Install: npm install puppeteer');
|
|
204
|
+
}
|
|
205
|
+
// CI runners (GitHub Actions Ubuntu) block unprivileged user namespaces, so
|
|
206
|
+
// Chrome can't initialize its sandbox there. Disable the sandbox only when
|
|
207
|
+
// running in CI; local users keep the default hardened launch.
|
|
208
|
+
const launchArgs = options.launchArgs || (process.env.CI ? ['--no-sandbox', '--disable-setuid-sandbox'] : []);
|
|
209
|
+
const browser = await puppeteer.default.launch({
|
|
210
|
+
headless: options.headless ?? true,
|
|
211
|
+
args: launchArgs,
|
|
212
|
+
});
|
|
213
|
+
return wrapPuppeteerBrowser(browser);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Selection
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/** Accepts either a driver browser handle or a raw puppeteer Browser. */
|
|
221
|
+
function normalizeBrowserHandle(browser) {
|
|
222
|
+
if (!browser) return null;
|
|
223
|
+
if (browser.driverName && typeof browser.newPage === 'function') return browser;
|
|
224
|
+
if (typeof browser.newPage === 'function') return wrapPuppeteerBrowser(browser);
|
|
225
|
+
throw new Error('Unsupported browser handle passed to detectUrl (expected a driver handle or puppeteer Browser)');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function launchDetectionBrowser(options = {}) {
|
|
229
|
+
const forced = (process.env[DRIVER_ENV] || '').trim().toLowerCase();
|
|
230
|
+
if (forced && forced !== 'monobrowse' && forced !== 'puppeteer') {
|
|
231
|
+
throw new Error(`Invalid ${DRIVER_ENV}=${forced} (expected "monobrowse" or "puppeteer")`);
|
|
232
|
+
}
|
|
233
|
+
const order = forced ? [forced] : ['monobrowse', 'puppeteer'];
|
|
234
|
+
const errors = [];
|
|
235
|
+
for (const name of order) {
|
|
236
|
+
try {
|
|
237
|
+
return name === 'monobrowse'
|
|
238
|
+
? await launchMonobrowseBrowser(options)
|
|
239
|
+
: await launchPuppeteerBrowser(options);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
errors.push(`${name}: ${error?.message || error}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
throw new Error(
|
|
245
|
+
'A browser is required for URL scanning and no driver could start one ' +
|
|
246
|
+
`(${errors.join(' | ')}). ` +
|
|
247
|
+
'Install Google Chrome/Chromium/Edge for the native @monoes/monobrowse driver, ' +
|
|
248
|
+
'or puppeteer is required for URL scanning. Install: npm install puppeteer'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export {
|
|
253
|
+
launchDetectionBrowser,
|
|
254
|
+
launchMonobrowseBrowser,
|
|
255
|
+
launchPuppeteerBrowser,
|
|
256
|
+
normalizeBrowserHandle,
|
|
257
|
+
wrapPuppeteerBrowser,
|
|
258
|
+
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
2
1
|
import { GENERIC_FONTS, OVERUSED_FONTS } from '../../shared/constants.mjs';
|
|
3
2
|
import { isNeutralColor } from '../../shared/color.mjs';
|
|
4
3
|
import { extractGoogleFontFamilies } from '../../shared/fonts.mjs';
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
//
|
|
2
|
+
// Visual-contrast pixel fallback. `page` is the driver page abstraction from
|
|
3
|
+
// ../browser/drivers.mjs (a raw puppeteer Page also satisfies it): it only
|
|
4
|
+
// needs `evaluate(fn, arg)` roundtrips and `screenshot({ encoding: 'base64',
|
|
5
|
+
// clip, captureBeyondViewport })` returning a base64 PNG string. The logic is
|
|
6
|
+
// driver-agnostic: hide the glyphs via CSS, screenshot the clip twice, and
|
|
7
|
+
// diff the pixels in-page on a canvas. Both the monobrowse (CDP
|
|
8
|
+
// Page.captureScreenshot + Runtime.evaluate) and puppeteer drivers support
|
|
9
|
+
// this fully.
|
|
2
10
|
function sanitizeScreenshotClip(clip, viewport) {
|
|
3
11
|
if (!clip) return null;
|
|
4
12
|
const x = Math.max(0, Math.floor(clip.x || 0));
|
package/cli/engine/findings.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monoes/monodesign",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Frontend design intelligence for AI agents \u2014 design tokens, antipattern detection, and the monodesign skill",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"marked": "^18.0.0"
|
|
46
46
|
},
|
|
47
47
|
"optionalDependencies": {
|
|
48
|
+
"@monoes/monobrowse": "^1.0.3",
|
|
48
49
|
"puppeteer": "^25.1.0"
|
|
49
50
|
}
|
|
50
51
|
}
|
package/skill/SKILL.src.md
CHANGED
|
@@ -56,7 +56,7 @@ allowed-tools:
|
|
|
56
56
|
- Bash
|
|
57
57
|
- WebSearch
|
|
58
58
|
- WebFetch
|
|
59
|
-
license:
|
|
59
|
+
license: Apache-2.0
|
|
60
60
|
---
|
|
61
61
|
|
|
62
62
|
Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft.
|
package/skill/scripts/detect.mjs
CHANGED
package/skill/scripts/hook.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
2
1
|
// Source scripts default to slash commands. The provider build replaces only
|
|
3
2
|
// this exact declaration, avoiding heuristic rewrites across executable code.
|
|
4
3
|
export const MONODESIGN_COMMAND_PREFIX = '/'; // @monodesign-provider-command-prefix
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
2
1
|
export function completionTypeForAcceptResult(eventType, acceptResult) {
|
|
3
2
|
if (eventType === 'discard') return acceptResult?.handled === true ? 'discarded' : 'error';
|
|
4
3
|
if (acceptResult?.handled === true && acceptResult?.carbonize === true) return 'agent_done';
|
package/skill/scripts/live.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
2
1
|
(function(y,v){typeof exports=="object"&&typeof module!="undefined"?v(exports):typeof define=="function"&&define.amd?define(["exports"],v):(y=typeof globalThis!="undefined"?globalThis:y||self,v(y.modernScreenshot={}))})(this,function(y){"use strict";var rr=Object.defineProperty,nr=Object.defineProperties;var or=Object.getOwnPropertyDescriptors;var Z=Object.getOwnPropertySymbols;var xe=Object.prototype.hasOwnProperty,Me=Object.prototype.propertyIsEnumerable;var Oe=Math.pow,Le=(y,v,N)=>v in y?rr(y,v,{enumerable:!0,configurable:!0,writable:!0,value:N}):y[v]=N,D=(y,v)=>{for(var N in v||(v={}))xe.call(v,N)&&Le(y,N,v[N]);if(Z)for(var N of Z(v))Me.call(v,N)&&Le(y,N,v[N]);return y},M=(y,v)=>nr(y,or(v));var je=(y,v)=>{var N={};for(var R in y)xe.call(y,R)&&v.indexOf(R)<0&&(N[R]=y[R]);if(y!=null&&Z)for(var R of Z(y))v.indexOf(R)<0&&Me.call(y,R)&&(N[R]=y[R]);return N};var C=(y,v,N)=>new Promise((R,O)=>{var X=P=>{try{q(N.next(P))}catch(W){O(W)}},j=P=>{try{q(N.throw(P))}catch(W){O(W)}},q=P=>P.done?R(P.value):Promise.resolve(P.value).then(X,j);q((N=N.apply(y,v)).next())});var Be;function v(e,t){return e[13]=1,e[14]=t>>8,e[15]=t&255,e[16]=t>>8,e[17]=t&255,e}const N=112,R=72,O=89,X=115;let j;function q(){const e=new Int32Array(256);for(let t=0;t<256;t++){let r=t;for(let n=0;n<8;n++)r=r&1?3988292384^r>>>1:r>>>1;e[t]=r}return e}function P(e){let t=-1;j||(j=q());for(let r=0;r<e.length;r++)t=j[(t^e[r])&255]^t>>>8;return t^-1}function W(e){const t=e.length-1;for(let r=t;r>=4;r--)if(e[r-4]===9&&e[r-3]===N&&e[r-2]===R&&e[r-1]===O&&e[r]===X)return r-3;return 0}function ae(e,t,r=!1){const n=new Uint8Array(13);t*=39.3701,n[0]=N,n[1]=R,n[2]=O,n[3]=X,n[4]=t>>>24,n[5]=t>>>16,n[6]=t>>>8,n[7]=t&255,n[8]=n[4],n[9]=n[5],n[10]=n[6],n[11]=n[7],n[12]=1;const i=P(n),a=new Uint8Array(4);if(a[0]=i>>>24,a[1]=i>>>16,a[2]=i>>>8,a[3]=i&255,r){const s=W(e);return e.set(n,s),e.set(a,s+13),e}else{const s=new Uint8Array(4);s[0]=0,s[1]=0,s[2]=0,s[3]=9;const o=new Uint8Array(54);return o.set(e,0),o.set(s,33),o.set(n,37),o.set(a,50),o}}const qe="AAlwSFlz",We="AAAJcEhZ",He="AAAACXBI";function Ve(e){let t=e.indexOf(qe);return t===-1&&(t=e.indexOf(We)),t===-1&&(t=e.indexOf(He)),t}const se="[modern-screenshot]",U=typeof window!="undefined",ze=U&&"Worker"in window,ie=U&&"atob"in window,Xe=U&&"btoa"in window,ee=U?(Be=window.navigator)==null?void 0:Be.userAgent:"",le=ee.includes("Chrome"),G=ee.includes("AppleWebKit")&&!le,te=ee.includes("Firefox"),Ge=e=>e&&"__CONTEXT__"in e,Ye=e=>e.constructor.name==="CSSFontFaceRule",Je=e=>e.constructor.name==="CSSImportRule",Ke=e=>e.constructor.name==="CSSLayerBlockRule",I=e=>e.nodeType===1,H=e=>typeof e.className=="object",ce=e=>e.tagName==="image",Qe=e=>e.tagName==="use",V=e=>I(e)&&typeof e.style!="undefined"&&!H(e),Ze=e=>e.nodeType===8,et=e=>e.nodeType===3,$=e=>e.tagName==="IMG",Y=e=>e.tagName==="VIDEO",tt=e=>e.tagName==="CANVAS",rt=e=>e.tagName==="TEXTAREA",nt=e=>e.tagName==="INPUT",ot=e=>e.tagName==="STYLE",at=e=>e.tagName==="SCRIPT",st=e=>e.tagName==="SELECT",it=e=>e.tagName==="SLOT",lt=e=>e.tagName==="IFRAME",ct=(...e)=>console.warn(se,...e);function ut(e){var r;const t=(r=e==null?void 0:e.createElement)==null?void 0:r.call(e,"canvas");return t&&(t.height=t.width=1),!!t&&"toDataURL"in t&&!!t.toDataURL("image/webp").includes("image/webp")}const re=e=>e.startsWith("data:");function ue(e,t){if(e.match(/^[a-z]+:\/\//i))return e;if(U&&e.match(/^\/\//))return window.location.protocol+e;if(e.match(/^[a-z]+:/i)||!U)return e;const r=J().implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),t&&(n.href=t),i.href=e,i.href}function J(e){var t;return(t=e&&I(e)?e==null?void 0:e.ownerDocument:e)!=null?t:window.document}const K="http://www.w3.org/2000/svg";function fe(e,t,r){const n=J(r).createElementNS(K,"svg");return n.setAttributeNS(null,"width",e.toString()),n.setAttributeNS(null,"height",t.toString()),n.setAttributeNS(null,"viewBox",`0 0 ${e} ${t}`),n}function de(e,t){let r=new XMLSerializer().serializeToString(e);return t&&(r=r.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu,"")),`data:image/svg+xml;charset=utf-8,${encodeURIComponent(r)}`}function ft(e,t="image/png",r=1){return C(this,null,function*(){try{return yield new Promise((n,i)=>{e.toBlob(a=>{a?n(a):i(new Error("Blob is null"))},t,r)})}catch(n){if(ie)return dt(e.toDataURL(t,r));throw n}})}function dt(e){var o,c;const[t,r]=e.split(","),n=(c=(o=t.match(/data:(.+);/))==null?void 0:o[1])!=null?c:void 0,i=window.atob(r),a=i.length,s=new Uint8Array(a);for(let u=0;u<a;u+=1)s[u]=i.charCodeAt(u);return new Blob([s],{type:n})}function ge(e,t){return new Promise((r,n)=>{const i=new FileReader;i.onload=()=>r(i.result),i.onerror=()=>n(i.error),i.onabort=()=>n(new Error(`Failed read blob to ${t}`)),t==="dataUrl"?i.readAsDataURL(e):t==="arrayBuffer"&&i.readAsArrayBuffer(e)})}const gt=e=>ge(e,"dataUrl"),mt=e=>ge(e,"arrayBuffer");function _(e,t){const r=J(t).createElement("img");return r.decoding="sync",r.loading="eager",r.src=e,r}function L(e,t){return new Promise(r=>{const{timeout:n,ownerDocument:i,onError:a,onWarn:s}=t!=null?t:{},o=typeof e=="string"?_(e,J(i)):e;let c=null,u=null;function l(){r(o),c&&clearTimeout(c),u==null||u()}if(n&&(c=setTimeout(l,n)),Y(o)){const d=o.currentSrc||o.src;if(!d)return o.poster?L(o.poster,t).then(r):l();if(o.readyState>=2)return l();const m=l,f=h=>{s==null||s("Failed video load",d,h),a==null||a(h),l()};u=()=>{o.removeEventListener("loadeddata",m),o.removeEventListener("error",f)},o.addEventListener("loadeddata",m,{once:!0}),o.addEventListener("error",f,{once:!0})}else{const d=ce(o)?o.href.baseVal:o.currentSrc||o.src;if(!d)return l();const m=()=>C(this,null,function*(){if($(o)&&"decode"in o)try{yield o.decode()}catch(h){s==null||s("Failed to decode image, trying to render anyway",o.dataset.originalSrc||d,h)}l()}),f=h=>{s==null||s("Failed image load",o.dataset.originalSrc||d,h),l()};if($(o)&&o.complete)return m();u=()=>{o.removeEventListener("load",m),o.removeEventListener("error",f)},o.addEventListener("load",m,{once:!0}),o.addEventListener("error",f,{once:!0})}})}function me(e,t){return C(this,null,function*(){V(e)&&($(e)||Y(e)?yield L(e,t):yield Promise.all(["img","video"].flatMap(r=>Array.from(e.querySelectorAll(r)).map(n=>L(n,t)))))})}const he=function(){let t=0;const r=()=>`0000${(Math.random()*Oe(36,4)<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${r()}${t}`)}();function we(e){return e==null?void 0:e.split(",").map(t=>t.trim().replace(/"|'/g,"").toLowerCase()).filter(Boolean)}let pe=0;function ht(e){const t=`${se}[#${pe}]`;return pe++,{time:r=>e&&console.time(`${t} ${r}`),timeEnd:r=>e&&console.timeEnd(`${t} ${r}`),warn:(...r)=>e&&ct(...r)}}function wt(e){return{cache:e?"no-cache":"force-cache"}}function k(e,t){return C(this,null,function*(){return Ge(e)?e:ye(e,M(D({},t),{autoDestruct:!0}))})}function ye(e,t){return C(this,null,function*(){var f,h,g,p,E;const{scale:r=1,workerUrl:n,workerNumber:i=1}=t||{},a=!!(t!=null&&t.debug),s=(f=t==null?void 0:t.features)!=null?f:!0,o=(h=e.ownerDocument)!=null?h:U?window.document:void 0,c=(p=(g=e.ownerDocument)==null?void 0:g.defaultView)!=null?p:U?window:void 0,u=new Map,l=M(D({width:0,height:0,quality:1,type:"image/png",scale:r,backgroundColor:null,style:null,filter:null,maximumCanvasSize:0,timeout:3e4,progress:null,debug:a,fetch:D({requestInit:wt((E=t==null?void 0:t.fetch)==null?void 0:E.bypassingCache),placeholderImage:"data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",bypassingCache:!1},t==null?void 0:t.fetch),fetchFn:null,font:{},drawImageInterval:100,workerUrl:null,workerNumber:i,onCloneEachNode:null,onCloneNode:null,onEmbedNode:null,onCreateForeignObjectSvg:null,includeStyleProperties:null,autoDestruct:!1},t),{__CONTEXT__:!0,log:ht(a),node:e,ownerDocument:o,ownerWindow:c,dpi:r===1?null:96*r,svgStyleElement:be(o),svgDefsElement:o==null?void 0:o.createElementNS(K,"defs"),svgStyles:new Map,defaultComputedStyles:new Map,workers:[...Array.from({length:ze&&n&&i?i:0})].map(()=>{try{const b=new Worker(n);return b.onmessage=w=>C(this,null,function*(){var A,F,B,$e;const{url:S,result:T}=w.data;T?(F=(A=u.get(S))==null?void 0:A.resolve)==null||F.call(A,T):($e=(B=u.get(S))==null?void 0:B.reject)==null||$e.call(B,new Error(`Error receiving message from worker: ${S}`))}),b.onmessageerror=w=>{var T,A;const{url:S}=w.data;(A=(T=u.get(S))==null?void 0:T.reject)==null||A.call(T,new Error(`Error receiving message from worker: ${S}`))},b}catch(b){return l.log.warn("Failed to new Worker",b),null}}).filter(Boolean),fontFamilies:new Map,fontCssTexts:new Map,acceptOfImage:`${[ut(o)&&"image/webp","image/svg+xml","image/*","*/*"].filter(Boolean).join(",")};q=0.8`,requests:u,drawImageCount:0,tasks:[],features:s,isEnable:b=>{var w,S;return b==="restoreScrollPosition"?typeof s=="boolean"?!1:(w=s[b])!=null?w:!1:typeof s=="boolean"?s:(S=s[b])!=null?S:!0},shadowRoots:[]});l.log.time("wait until load"),yield me(e,{timeout:l.timeout,onWarn:l.log.warn}),l.log.timeEnd("wait until load");const{width:d,height:m}=pt(e,l);return l.width=d,l.height=m,l})}function be(e){if(!e)return;const t=e.createElement("style"),r=t.ownerDocument.createTextNode(`
|
|
3
2
|
.______background-clip--text {
|
|
4
3
|
background-clip: text;
|
package/skill/scripts/pin.mjs
CHANGED