@monoes/monodesign 1.2.18 → 1.2.22
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/README.md +3 -3
- package/cli/engine/design-system-checks.mjs +381 -0
- package/cli/engine/design-system-parse.mjs +433 -0
- package/cli/engine/design-system.mjs +20 -736
- package/cli/engine/detect-antipatterns-browser.js +12 -1
- package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
- package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
- package/cli/engine/engines/regex/detect-text.mjs +11 -523
- package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
- package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
- package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
- package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
- package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
- package/cli/engine/registry/antipatterns-provider.mjs +60 -0
- package/cli/engine/registry/antipatterns-quality.mjs +218 -0
- package/cli/engine/registry/antipatterns-slop.mjs +226 -0
- package/cli/engine/registry/antipatterns.mjs +7 -494
- package/cli/engine/rules/checks-background.mjs +155 -0
- package/cli/engine/rules/checks-css-values.mjs +137 -0
- package/cli/engine/rules/checks-dom-effects.mjs +243 -0
- package/cli/engine/rules/checks-dom.mjs +148 -0
- package/cli/engine/rules/checks-element.mjs +286 -0
- package/cli/engine/rules/checks-html.mjs +204 -0
- package/cli/engine/rules/checks-overflow.mjs +263 -0
- package/cli/engine/rules/checks-page.mjs +465 -0
- package/cli/engine/rules/checks-pure.mjs +453 -0
- package/cli/engine/rules/checks-quality.mjs +447 -0
- package/cli/engine/rules/checks-stylesheet.mjs +324 -0
- package/cli/engine/rules/checks.mjs +98 -2939
- package/cli/lib/monodesign-config-consent.mjs +93 -0
- package/cli/lib/monodesign-config-core.mjs +37 -0
- package/cli/lib/monodesign-config-detection.mjs +133 -0
- package/cli/lib/monodesign-config-filter.mjs +198 -0
- package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
- package/cli/lib/monodesign-config.mjs +21 -623
- package/package.json +2 -2
- package/skill/scripts/context-fields.mjs +65 -0
- package/skill/scripts/context-resolve.mjs +425 -0
- package/skill/scripts/context-update.mjs +151 -0
- package/skill/scripts/context-utils.mjs +68 -0
- package/skill/scripts/context-workspace.mjs +245 -0
- package/skill/scripts/context.mjs +17 -891
- package/skill/scripts/hook-admin-config.mjs +152 -0
- package/skill/scripts/hook-admin-ignore.mjs +131 -0
- package/skill/scripts/hook-admin-manifest.mjs +292 -0
- package/skill/scripts/hook-admin.mjs +8 -541
- package/skill/scripts/hook-before-edit-shell.mjs +184 -0
- package/skill/scripts/hook-before-edit.mjs +9 -165
- package/skill/scripts/hook-lib-cache.mjs +145 -0
- package/skill/scripts/hook-lib-config.mjs +292 -0
- package/skill/scripts/hook-lib-detector.mjs +72 -0
- package/skill/scripts/hook-lib-events.mjs +182 -0
- package/skill/scripts/hook-lib-findings.mjs +201 -0
- package/skill/scripts/hook-lib-ignore.mjs +205 -0
- package/skill/scripts/hook-lib-render.mjs +273 -0
- package/skill/scripts/hook-lib-run.mjs +281 -0
- package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
- package/skill/scripts/hook-lib.mjs +71 -1747
- package/skill/scripts/lib/design-parser-colors.mjs +182 -0
- package/skill/scripts/lib/design-parser-extract.mjs +333 -0
- package/skill/scripts/lib/design-parser-sections.mjs +214 -0
- package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
- package/skill/scripts/lib/design-parser.mjs +10 -783
- package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
- package/skill/scripts/live/manual-apply-compact.mjs +122 -0
- package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
- package/skill/scripts/live/manual-apply-log.mjs +179 -0
- package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
- package/skill/scripts/live/manual-apply.mjs +65 -640
- package/skill/scripts/live/svelte-component-accept.mjs +248 -0
- package/skill/scripts/live/svelte-component-css.mjs +230 -0
- package/skill/scripts/live/svelte-component-markup.mjs +104 -0
- package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
- package/skill/scripts/live/svelte-component-session.mjs +158 -0
- package/skill/scripts/live/svelte-component.mjs +32 -792
- package/skill/scripts/live-accept-mutate.mjs +152 -0
- package/skill/scripts/live-accept-parse.mjs +370 -0
- package/skill/scripts/live-accept-scrub.mjs +59 -0
- package/skill/scripts/live-accept-search.mjs +58 -0
- package/skill/scripts/live-accept.mjs +15 -631
- package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
- package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
- package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
- package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
- package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
- package/skill/scripts/live-commit-manual-edits.mjs +24 -881
- package/skill/scripts/live-copy-edit-agent.mjs +16 -551
- package/skill/scripts/live-copy-edit-compact.mjs +137 -0
- package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
- package/skill/scripts/live-copy-edit-runners.mjs +297 -0
- package/skill/scripts/live-inject-csp.mjs +132 -0
- package/skill/scripts/live-inject-files.mjs +229 -0
- package/skill/scripts/live-inject-tag.mjs +114 -0
- package/skill/scripts/live-inject.mjs +13 -440
- package/skill/scripts/live-server-poll.mjs +232 -0
- package/skill/scripts/live-server-routes.mjs +449 -0
- package/skill/scripts/live-server-state.mjs +361 -0
- package/skill/scripts/live-server.mjs +10 -946
- package/skill/scripts/live-wrap-css.mjs +132 -0
- package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
- package/skill/scripts/live-wrap-search.mjs +247 -0
- package/skill/scripts/live-wrap.mjs +21 -484
- package/skill/scripts/palette-seeds.mjs +401 -0
- package/skill/scripts/palette.mjs +1 -395
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
|
-
import { MONODESIGN_COMMAND } from './lib/provider.mjs';
|
|
25
24
|
|
|
26
25
|
import {
|
|
27
26
|
getConfigPath,
|
|
@@ -29,241 +28,17 @@ import {
|
|
|
29
28
|
getCachePath,
|
|
30
29
|
getPendingPath,
|
|
31
30
|
readConfig,
|
|
32
|
-
DEFAULT_CONFIG,
|
|
33
|
-
ensureHookGitExcludes,
|
|
34
|
-
normalizeIgnoreValue,
|
|
35
|
-
normalizeIgnoreValueEntries,
|
|
36
31
|
} from './hook-lib.mjs';
|
|
32
|
+
import {
|
|
33
|
+
readRawConfigFile,
|
|
34
|
+
readRawHookConfig,
|
|
35
|
+
mergeHookConfig,
|
|
36
|
+
writeHookConfig,
|
|
37
|
+
} from './hook-admin-config.mjs';
|
|
38
|
+
import { repairHookManifests } from './hook-admin-manifest.mjs';
|
|
39
|
+
import { addIgnoreRule, addIgnoreFile, addIgnoreValue } from './hook-admin-ignore.mjs';
|
|
37
40
|
|
|
38
41
|
const ACTIONS = new Set(['status', 'on', 'off', 'ignore-rule', 'ignore-file', 'ignore-value', 'reset']);
|
|
39
|
-
const MONODESIGN_HOOK_COMMAND_MARKERS = [
|
|
40
|
-
'skills/monodesign/scripts/hook-probe.mjs',
|
|
41
|
-
'skills/monodesign/scripts/hook.mjs',
|
|
42
|
-
'skills/monodesign/scripts/hook-before-edit.mjs',
|
|
43
|
-
'skills/monodesign/scripts/hook-after-edit.mjs',
|
|
44
|
-
'skills/monodesign/scripts/hook-stop.mjs',
|
|
45
|
-
];
|
|
46
|
-
const TIMEOUT_SECONDS = 5;
|
|
47
|
-
const STATUS_MESSAGE = 'Checking UI changes';
|
|
48
|
-
|
|
49
|
-
const HOOK_MANIFEST_TARGETS = [
|
|
50
|
-
{
|
|
51
|
-
provider: '.claude',
|
|
52
|
-
skillRel: '.claude/skills/monodesign',
|
|
53
|
-
destRel: '.claude/settings.local.json',
|
|
54
|
-
sharedDestRel: '.claude/settings.json',
|
|
55
|
-
manifest: () => ({
|
|
56
|
-
description: 'Monodesign design detector: runs after Edit/Write/MultiEdit on UI files and surfaces findings as system reminders.',
|
|
57
|
-
hooks: {
|
|
58
|
-
PostToolUse: [
|
|
59
|
-
{
|
|
60
|
-
matcher: 'Edit|Write|MultiEdit',
|
|
61
|
-
hooks: [
|
|
62
|
-
{
|
|
63
|
-
type: 'command',
|
|
64
|
-
command: 'node "${CLAUDE_PROJECT_DIR}/.claude/skills/monodesign/scripts/hook.mjs"',
|
|
65
|
-
timeout: TIMEOUT_SECONDS,
|
|
66
|
-
statusMessage: STATUS_MESSAGE,
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
}),
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
provider: '.agents',
|
|
76
|
-
skillRel: '.agents/skills/monodesign',
|
|
77
|
-
destRel: '.codex/config.toml',
|
|
78
|
-
format: 'toml',
|
|
79
|
-
manifest: () => [
|
|
80
|
-
'# monodesign:start native-hook',
|
|
81
|
-
'[[hooks.PostToolUse]]',
|
|
82
|
-
'matcher = "Edit|Write|apply_patch"',
|
|
83
|
-
'[[hooks.PostToolUse.hooks]]',
|
|
84
|
-
'type = "command"',
|
|
85
|
-
'command = "node .agents/skills/monodesign/scripts/hook.mjs"',
|
|
86
|
-
`timeout = ${TIMEOUT_SECONDS}`,
|
|
87
|
-
`statusMessage = "${STATUS_MESSAGE}"`,
|
|
88
|
-
'# monodesign:end native-hook',
|
|
89
|
-
'',
|
|
90
|
-
].join('\n'),
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
provider: '.cursor',
|
|
94
|
-
skillRel: '.cursor/skills/monodesign',
|
|
95
|
-
destRel: '.cursor/hooks.json',
|
|
96
|
-
manifest: () => ({
|
|
97
|
-
version: 1,
|
|
98
|
-
hooks: {
|
|
99
|
-
preToolUse: [
|
|
100
|
-
{
|
|
101
|
-
command: 'node ".cursor/skills/monodesign/scripts/hook-before-edit.mjs"',
|
|
102
|
-
timeout: TIMEOUT_SECONDS,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
},
|
|
106
|
-
}),
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
// GitHub Copilot reads repo-level hooks from `.github/hooks/*.json`. The same
|
|
110
|
-
// manifest is honored by the CLI (once committed to the default branch) and
|
|
111
|
-
// the cloud/app agent. Schema differs: lowercase `postToolUse`, flat entries,
|
|
112
|
-
// `bash`/`timeoutSec`, and a `matcher` regex against the `edit`/`create` tools.
|
|
113
|
-
provider: '.github',
|
|
114
|
-
skillRel: '.github/skills/monodesign',
|
|
115
|
-
destRel: '.github/hooks/monodesign.json',
|
|
116
|
-
manifest: () => ({
|
|
117
|
-
version: 1,
|
|
118
|
-
hooks: {
|
|
119
|
-
postToolUse: [
|
|
120
|
-
{
|
|
121
|
-
type: 'command',
|
|
122
|
-
matcher: 'edit|create|apply_patch',
|
|
123
|
-
bash: 'node "$(git rev-parse --show-toplevel)/.github/skills/monodesign/scripts/hook.mjs"',
|
|
124
|
-
timeoutSec: TIMEOUT_SECONDS,
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
}),
|
|
129
|
-
},
|
|
130
|
-
];
|
|
131
|
-
|
|
132
|
-
function readRawConfigFile(filePath) {
|
|
133
|
-
if (!fs.existsSync(filePath)) return { exists: false, malformed: false, raw: null };
|
|
134
|
-
try {
|
|
135
|
-
return { exists: true, malformed: false, raw: JSON.parse(fs.readFileSync(filePath, 'utf-8')) };
|
|
136
|
-
} catch {
|
|
137
|
-
return { exists: true, malformed: true, raw: null };
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const DETECTOR_CONFIG_KEYS = new Set(['ignoreRules', 'ignoreFiles', 'ignoreValues', 'designSystem']);
|
|
142
|
-
|
|
143
|
-
function hookSection(unified) {
|
|
144
|
-
return unified && typeof unified === 'object' && !Array.isArray(unified) && unified.hook && typeof unified.hook === 'object' && !Array.isArray(unified.hook)
|
|
145
|
-
? unified.hook
|
|
146
|
-
: null;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function detectorSection(unified) {
|
|
150
|
-
return unified && typeof unified === 'object' && !Array.isArray(unified) && unified.detector && typeof unified.detector === 'object' && !Array.isArray(unified.detector)
|
|
151
|
-
? unified.detector
|
|
152
|
-
: null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function readRawHookConfig(cwd, opts = {}) {
|
|
156
|
-
const unified = readRawConfigFile(opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd)).raw;
|
|
157
|
-
return hookSection(unified);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function readRawDetectorConfig(cwd, opts = {}) {
|
|
161
|
-
const unified = readRawConfigFile(opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd)).raw;
|
|
162
|
-
const merged = mergeDetectorConfig(hookSection(unified));
|
|
163
|
-
return mergeDetectorConfig(detectorSection(unified), merged);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function stripDetectorKeys(raw) {
|
|
167
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
168
|
-
const out = {};
|
|
169
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
170
|
-
if (!DETECTOR_CONFIG_KEYS.has(key)) out[key] = value;
|
|
171
|
-
}
|
|
172
|
-
return out;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Write hook runtime config under `hook`, leaving detector filters in
|
|
176
|
-
// `detector` and preserving sibling keys such as updateCheck.
|
|
177
|
-
function writeHookConfig(cwd, hookConfig, opts = {}) {
|
|
178
|
-
const filePath = opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd);
|
|
179
|
-
if (opts.local) ensureHookGitExcludes(cwd);
|
|
180
|
-
const existingRaw = readRawConfigFile(filePath).raw;
|
|
181
|
-
const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
|
|
182
|
-
const existingHook = stripDetectorKeys(hookSection(existing));
|
|
183
|
-
// Merge over the existing hook object so fields the merge helpers don't manage
|
|
184
|
-
// (consent, quiet, auditLog) survive an Monodesign hooks edit.
|
|
185
|
-
const next = { ...existing, hook: { ...existingHook, ...hookConfig } };
|
|
186
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
187
|
-
fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
188
|
-
return filePath;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function writeDetectorConfig(cwd, detectorConfig, opts = {}) {
|
|
192
|
-
const filePath = opts.local ? getLocalConfigPath(cwd) : getConfigPath(cwd);
|
|
193
|
-
if (opts.local) ensureHookGitExcludes(cwd);
|
|
194
|
-
const existingRaw = readRawConfigFile(filePath).raw;
|
|
195
|
-
const existing = existingRaw && typeof existingRaw === 'object' && !Array.isArray(existingRaw) ? existingRaw : {};
|
|
196
|
-
const nextHook = stripDetectorKeys(hookSection(existing));
|
|
197
|
-
const existingDetector = mergeDetectorConfig(detectorSection(existing));
|
|
198
|
-
const next = {
|
|
199
|
-
...existing,
|
|
200
|
-
detector: mergeDetectorConfig(detectorConfig, existingDetector),
|
|
201
|
-
};
|
|
202
|
-
if (Object.keys(nextHook).length > 0) next.hook = nextHook;
|
|
203
|
-
else delete next.hook;
|
|
204
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
205
|
-
fs.writeFileSync(filePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
206
|
-
return filePath;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function mergeHookConfig(existing) {
|
|
210
|
-
const base = existing && typeof existing === 'object' ? existing : {};
|
|
211
|
-
return {
|
|
212
|
-
enabled: base.enabled !== false,
|
|
213
|
-
limits: {
|
|
214
|
-
maxFindings: Number.isFinite(base?.limits?.maxFindings) ? base.limits.maxFindings : DEFAULT_CONFIG.limits.maxFindings,
|
|
215
|
-
maxChars: Number.isFinite(base?.limits?.maxChars) ? base.limits.maxChars : DEFAULT_CONFIG.limits.maxChars,
|
|
216
|
-
},
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function mergeDetectorConfig(existing, seed = null) {
|
|
221
|
-
const base = existing && typeof existing === 'object' ? existing : {};
|
|
222
|
-
const out = seed ? {
|
|
223
|
-
ignoreRules: [...seed.ignoreRules],
|
|
224
|
-
ignoreFiles: [...seed.ignoreFiles],
|
|
225
|
-
ignoreValues: normalizeIgnoreValueEntries(seed.ignoreValues),
|
|
226
|
-
} : {
|
|
227
|
-
ignoreRules: [],
|
|
228
|
-
ignoreFiles: [],
|
|
229
|
-
ignoreValues: [],
|
|
230
|
-
};
|
|
231
|
-
if (seed?.designSystem && typeof seed.designSystem === 'object' && !Array.isArray(seed.designSystem)) {
|
|
232
|
-
out.designSystem = { ...seed.designSystem };
|
|
233
|
-
}
|
|
234
|
-
if (base.designSystem && typeof base.designSystem === 'object' && !Array.isArray(base.designSystem)) {
|
|
235
|
-
out.designSystem = {
|
|
236
|
-
...(out.designSystem || {}),
|
|
237
|
-
enabled: base.designSystem.enabled !== false,
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
if (Array.isArray(base.ignoreRules)) {
|
|
241
|
-
out.ignoreRules = Array.from(new Set([...out.ignoreRules, ...base.ignoreRules.map(String)]));
|
|
242
|
-
}
|
|
243
|
-
if (Array.isArray(base.ignoreFiles)) {
|
|
244
|
-
out.ignoreFiles = Array.from(new Set([...out.ignoreFiles, ...base.ignoreFiles.map(String)]));
|
|
245
|
-
}
|
|
246
|
-
if (Array.isArray(base.ignoreValues)) {
|
|
247
|
-
out.ignoreValues = mergeIgnoreValueEntries(out.ignoreValues, base.ignoreValues);
|
|
248
|
-
}
|
|
249
|
-
return out;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function mergeIgnoreValueEntries(existing, incoming) {
|
|
253
|
-
const map = new Map();
|
|
254
|
-
for (const entry of normalizeIgnoreValueEntries(existing)) {
|
|
255
|
-
map.set(ignoreValueEntryKey(entry), entry);
|
|
256
|
-
}
|
|
257
|
-
for (const entry of normalizeIgnoreValueEntries(incoming)) {
|
|
258
|
-
map.set(ignoreValueEntryKey(entry), entry);
|
|
259
|
-
}
|
|
260
|
-
return Array.from(map.values());
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function ignoreValueEntryKey(entry) {
|
|
264
|
-
const files = Array.isArray(entry.files) && entry.files.length > 0 ? entry.files.join('\x1f') : '';
|
|
265
|
-
return `${entry.rule}\0${entry.value}\0${files}`;
|
|
266
|
-
}
|
|
267
42
|
|
|
268
43
|
function statusReport(cwd) {
|
|
269
44
|
const shared = readRawConfigFile(getConfigPath(cwd));
|
|
@@ -324,314 +99,6 @@ function setEnabled(cwd, value) {
|
|
|
324
99
|
return parts.join(' ');
|
|
325
100
|
}
|
|
326
101
|
|
|
327
|
-
function repairHookManifests(cwd) {
|
|
328
|
-
const result = { written: [], already: [], backups: [] };
|
|
329
|
-
for (const target of HOOK_MANIFEST_TARGETS) {
|
|
330
|
-
if (!fs.existsSync(path.join(cwd, target.skillRel))) continue;
|
|
331
|
-
const dest = path.join(cwd, target.destRel);
|
|
332
|
-
const sharedDest = target.sharedDestRel ? path.join(cwd, target.sharedDestRel) : null;
|
|
333
|
-
|
|
334
|
-
if (target.format === 'toml') {
|
|
335
|
-
const current = fs.existsSync(dest) ? safeReadText(dest) : '';
|
|
336
|
-
const next = mergeCodexHookConfig(current || '', target.manifest());
|
|
337
|
-
if (current === next) {
|
|
338
|
-
result.already.push(target.provider);
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
342
|
-
fs.writeFileSync(dest, next);
|
|
343
|
-
result.written.push(target.provider);
|
|
344
|
-
continue;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (sharedDest && fileHasMonodesignHookMarker(sharedDest)) {
|
|
348
|
-
pruneMonodesignHookFromManifest(dest);
|
|
349
|
-
result.already.push(target.provider);
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const fresh = target.manifest();
|
|
354
|
-
let next = fresh;
|
|
355
|
-
if (fs.existsSync(dest)) {
|
|
356
|
-
try {
|
|
357
|
-
next = mergeHookManifests(JSON.parse(fs.readFileSync(dest, 'utf-8')), fresh);
|
|
358
|
-
} catch {
|
|
359
|
-
const backup = `${dest}.bak`;
|
|
360
|
-
fs.copyFileSync(dest, backup);
|
|
361
|
-
result.backups.push(backup);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
366
|
-
const current = fs.existsSync(dest) ? safeReadText(dest) : null;
|
|
367
|
-
if (current === serialized) {
|
|
368
|
-
result.already.push(target.provider);
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
372
|
-
fs.writeFileSync(dest, serialized);
|
|
373
|
-
result.written.push(target.provider);
|
|
374
|
-
}
|
|
375
|
-
return result;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function mergeCodexHookConfig(existing, hookBlock) {
|
|
379
|
-
const completeBlock = /# monodesign:start native-hook[\s\S]*?# monodesign:end native-hook\n?/m;
|
|
380
|
-
const incompleteBlock = /# monodesign:start native-hook[\s\S]*$/m;
|
|
381
|
-
let merged = existing;
|
|
382
|
-
if (completeBlock.test(merged)) merged = merged.replace(completeBlock, hookBlock);
|
|
383
|
-
else if (incompleteBlock.test(merged)) merged = merged.replace(incompleteBlock, hookBlock);
|
|
384
|
-
else merged = `${merged.trimEnd()}${merged.trim() ? '\n\n' : ''}${hookBlock}`;
|
|
385
|
-
return enableCodexHooks(merged);
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function enableCodexHooks(config) {
|
|
389
|
-
if (!/^\[features\]\s*$/m.test(config)) {
|
|
390
|
-
return `[features]\nhooks = true\n\n${config}`;
|
|
391
|
-
}
|
|
392
|
-
const lines = config.split(/\r?\n/);
|
|
393
|
-
const start = lines.findIndex((line) => /^\[features\]\s*$/.test(line));
|
|
394
|
-
let end = start + 1;
|
|
395
|
-
while (end < lines.length && !/^\[\[?[^\]]+\]\]?\s*$/.test(lines[end])) end++;
|
|
396
|
-
const hookIndex = lines.slice(start + 1, end).findIndex((line) => /^hooks\s*=/.test(line));
|
|
397
|
-
if (hookIndex === -1) lines.splice(end, 0, 'hooks = true');
|
|
398
|
-
else lines[start + 1 + hookIndex] = 'hooks = true';
|
|
399
|
-
return lines.join('\n');
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function safeReadText(filePath) {
|
|
403
|
-
try {
|
|
404
|
-
return fs.readFileSync(filePath, 'utf-8');
|
|
405
|
-
} catch {
|
|
406
|
-
return null;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function mergeHookManifests(existing, fresh) {
|
|
411
|
-
const existingObject = existing && typeof existing === 'object' && !Array.isArray(existing) ? existing : {};
|
|
412
|
-
const freshObject = fresh && typeof fresh === 'object' && !Array.isArray(fresh) ? fresh : {};
|
|
413
|
-
const existingHooks = existingObject.hooks && typeof existingObject.hooks === 'object' && !Array.isArray(existingObject.hooks)
|
|
414
|
-
? existingObject.hooks
|
|
415
|
-
: {};
|
|
416
|
-
const freshHooks = freshObject.hooks && typeof freshObject.hooks === 'object' && !Array.isArray(freshObject.hooks)
|
|
417
|
-
? freshObject.hooks
|
|
418
|
-
: {};
|
|
419
|
-
|
|
420
|
-
const merged = { ...existingObject, hooks: {} };
|
|
421
|
-
if (freshObject.version !== undefined) merged.version = freshObject.version;
|
|
422
|
-
if (freshObject.description !== undefined) merged.description = freshObject.description;
|
|
423
|
-
|
|
424
|
-
const hookEvents = new Set([...Object.keys(existingHooks), ...Object.keys(freshHooks)]);
|
|
425
|
-
for (const event of hookEvents) {
|
|
426
|
-
const preserved = stripMonodesignHookEntries(existingHooks[event]);
|
|
427
|
-
const added = Array.isArray(freshHooks[event]) ? freshHooks[event] : [];
|
|
428
|
-
const mergedEntries = [...preserved, ...added];
|
|
429
|
-
if (mergedEntries.length > 0) merged.hooks[event] = mergedEntries;
|
|
430
|
-
}
|
|
431
|
-
return merged;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function fileHasMonodesignHookMarker(filePath) {
|
|
435
|
-
if (!fs.existsSync(filePath)) return false;
|
|
436
|
-
let parsed;
|
|
437
|
-
try {
|
|
438
|
-
parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
439
|
-
} catch {
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
|
443
|
-
if (!parsed.hooks || typeof parsed.hooks !== 'object') return false;
|
|
444
|
-
return valueHasMonodesignHookMarker(parsed.hooks);
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function valueHasMonodesignHookMarker(value) {
|
|
448
|
-
if (typeof value === 'string') {
|
|
449
|
-
return MONODESIGN_HOOK_COMMAND_MARKERS.some((marker) => value.includes(marker));
|
|
450
|
-
}
|
|
451
|
-
if (Array.isArray(value)) return value.some(valueHasMonodesignHookMarker);
|
|
452
|
-
if (value && typeof value === 'object') return Object.values(value).some(valueHasMonodesignHookMarker);
|
|
453
|
-
return false;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function stripMonodesignHookEntry(entry) {
|
|
457
|
-
if (!entry || typeof entry !== 'object') return entry;
|
|
458
|
-
// `command`/`args`: Claude/Codex/Cursor. `bash`/`powershell`: GitHub Copilot's
|
|
459
|
-
// flat entry shape, where the marker lives under the shell-command keys.
|
|
460
|
-
if (valueHasMonodesignHookMarker(entry.command) || valueHasMonodesignHookMarker(entry.args)
|
|
461
|
-
|| valueHasMonodesignHookMarker(entry.bash) || valueHasMonodesignHookMarker(entry.powershell)) {
|
|
462
|
-
return null;
|
|
463
|
-
}
|
|
464
|
-
if (!Array.isArray(entry.hooks)) return entry;
|
|
465
|
-
|
|
466
|
-
const strippedHooks = entry.hooks
|
|
467
|
-
.map(stripMonodesignHookEntry)
|
|
468
|
-
.filter(Boolean);
|
|
469
|
-
|
|
470
|
-
if (strippedHooks.length === 0 && entry.hooks.some(valueHasMonodesignHookMarker)) {
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
473
|
-
return { ...entry, hooks: strippedHooks };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function stripMonodesignHookEntries(entries) {
|
|
477
|
-
if (!Array.isArray(entries)) return [];
|
|
478
|
-
return entries
|
|
479
|
-
.map(stripMonodesignHookEntry)
|
|
480
|
-
.filter(Boolean);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
function pruneMonodesignHookFromManifest(manifestPath) {
|
|
484
|
-
if (!fileHasMonodesignHookMarker(manifestPath)) return false;
|
|
485
|
-
let parsed;
|
|
486
|
-
try {
|
|
487
|
-
parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
488
|
-
} catch {
|
|
489
|
-
return false;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const existingHooks = parsed.hooks && typeof parsed.hooks === 'object' && !Array.isArray(parsed.hooks)
|
|
493
|
-
? parsed.hooks
|
|
494
|
-
: {};
|
|
495
|
-
const cleanedHooks = {};
|
|
496
|
-
for (const [event, entries] of Object.entries(existingHooks)) {
|
|
497
|
-
const kept = stripMonodesignHookEntries(entries);
|
|
498
|
-
if (kept.length > 0) cleanedHooks[event] = kept;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const next = { ...parsed };
|
|
502
|
-
if (Object.keys(cleanedHooks).length > 0) {
|
|
503
|
-
next.hooks = cleanedHooks;
|
|
504
|
-
} else {
|
|
505
|
-
delete next.hooks;
|
|
506
|
-
delete next.description;
|
|
507
|
-
delete next.version;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (Object.keys(next).length === 0) {
|
|
511
|
-
fs.rmSync(manifestPath, { force: true });
|
|
512
|
-
} else {
|
|
513
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
514
|
-
}
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
function normalizeRuleId(rule) {
|
|
519
|
-
return String(rule || '').trim().toLowerCase();
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function parseIgnoreRuleArgs(args) {
|
|
523
|
-
const positionals = [];
|
|
524
|
-
let allValues = false;
|
|
525
|
-
|
|
526
|
-
for (let i = 0; i < args.length; i++) {
|
|
527
|
-
const arg = String(args[i] || '');
|
|
528
|
-
if (arg === '--all-values') {
|
|
529
|
-
allValues = true;
|
|
530
|
-
} else if (arg === '--reason') {
|
|
531
|
-
while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) i++;
|
|
532
|
-
} else if (arg.startsWith('--reason=')) {
|
|
533
|
-
// Accepted for command symmetry; ignoreRules stores rule ids only.
|
|
534
|
-
} else if (arg.startsWith('--')) {
|
|
535
|
-
throw new Error(`Unknown ignore-rule flag: ${arg}`);
|
|
536
|
-
} else {
|
|
537
|
-
positionals.push(arg);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return {
|
|
542
|
-
rule: normalizeRuleId(positionals[0]),
|
|
543
|
-
allValues,
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function addIgnoreRule(cwd, args) {
|
|
548
|
-
const parsed = parseIgnoreRuleArgs(args);
|
|
549
|
-
const rule = parsed.rule;
|
|
550
|
-
if (!rule) throw new Error(`Pass a rule id, e.g. ${MONODESIGN_COMMAND} hooks ignore-rule side-tab`);
|
|
551
|
-
if (rule === 'overused-font' && !parsed.allValues) {
|
|
552
|
-
throw new Error(`overused-font is value-specific by default. Use ${MONODESIGN_COMMAND} hooks ignore-value overused-font <font> for a confirmed font, or ${MONODESIGN_COMMAND} hooks ignore-rule overused-font --all-values only when the user asked to ignore overused fonts generally.`);
|
|
553
|
-
}
|
|
554
|
-
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
555
|
-
if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
|
|
556
|
-
writeDetectorConfig(cwd, config);
|
|
557
|
-
return `Added "${rule}" to detector.ignoreRules. Current: ${config.ignoreRules.join(', ')}`;
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
function addIgnoreFile(cwd, glob) {
|
|
561
|
-
if (!glob) throw new Error(`Pass a glob, e.g. ${MONODESIGN_COMMAND} hooks ignore-file "src/legacy/**"`);
|
|
562
|
-
const config = mergeDetectorConfig(readRawDetectorConfig(cwd));
|
|
563
|
-
if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
|
|
564
|
-
writeDetectorConfig(cwd, config);
|
|
565
|
-
return `Added "${glob}" to detector.ignoreFiles. Current: ${config.ignoreFiles.join(', ')}`;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function parseIgnoreValueArgs(args) {
|
|
569
|
-
const positionals = [];
|
|
570
|
-
let shared = false;
|
|
571
|
-
let local = false;
|
|
572
|
-
let reason = '';
|
|
573
|
-
|
|
574
|
-
for (let i = 0; i < args.length; i++) {
|
|
575
|
-
const arg = args[i];
|
|
576
|
-
if (arg === '--shared') {
|
|
577
|
-
shared = true;
|
|
578
|
-
} else if (arg === '--local') {
|
|
579
|
-
local = true;
|
|
580
|
-
} else if (arg === '--reason') {
|
|
581
|
-
const chunks = [];
|
|
582
|
-
while (i + 1 < args.length && !String(args[i + 1]).startsWith('--')) {
|
|
583
|
-
chunks.push(args[++i]);
|
|
584
|
-
}
|
|
585
|
-
reason = chunks.join(' ').trim();
|
|
586
|
-
} else if (String(arg).startsWith('--reason=')) {
|
|
587
|
-
reason = String(arg).slice('--reason='.length).trim();
|
|
588
|
-
} else {
|
|
589
|
-
positionals.push(arg);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const [rule, ...valueParts] = positionals;
|
|
594
|
-
return {
|
|
595
|
-
rule: String(rule || '').trim().toLowerCase(),
|
|
596
|
-
value: normalizeIgnoreValue(valueParts.join(' ')),
|
|
597
|
-
shared,
|
|
598
|
-
local,
|
|
599
|
-
reason,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function addIgnoreValue(cwd, args) {
|
|
604
|
-
const parsed = parseIgnoreValueArgs(args);
|
|
605
|
-
if (!parsed.rule || !parsed.value) {
|
|
606
|
-
throw new Error(`Pass a rule id and value, e.g. ${MONODESIGN_COMMAND} hooks ignore-value overused-font Inter`);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
if (parsed.shared && parsed.local) {
|
|
610
|
-
throw new Error('Pass only one scope flag: --shared or --local');
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const local = parsed.local;
|
|
614
|
-
const config = mergeDetectorConfig(readRawDetectorConfig(cwd, { local }));
|
|
615
|
-
const key = `${parsed.rule}\0${parsed.value}`;
|
|
616
|
-
const existing = config.ignoreValues.find((entry) => `${entry.rule}\0${entry.value}` === key);
|
|
617
|
-
|
|
618
|
-
if (existing) {
|
|
619
|
-
if (parsed.reason) existing.reason = parsed.reason;
|
|
620
|
-
} else {
|
|
621
|
-
const entry = {
|
|
622
|
-
rule: parsed.rule,
|
|
623
|
-
value: parsed.value,
|
|
624
|
-
createdAt: new Date().toISOString(),
|
|
625
|
-
};
|
|
626
|
-
if (parsed.reason) entry.reason = parsed.reason;
|
|
627
|
-
config.ignoreValues.push(entry);
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
const target = writeDetectorConfig(cwd, config, { local });
|
|
631
|
-
const scope = local ? 'local detector.ignoreValues' : 'shared detector.ignoreValues';
|
|
632
|
-
return `Added ${parsed.rule}=${parsed.value} to ${scope} (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
102
|
function reset(cwd) {
|
|
636
103
|
const removed = [];
|
|
637
104
|
// Unified files may hold non-hook keys (e.g. updateCheck); strip only the
|