@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.
Files changed (104) hide show
  1. package/README.md +3 -3
  2. package/cli/engine/design-system-checks.mjs +381 -0
  3. package/cli/engine/design-system-parse.mjs +433 -0
  4. package/cli/engine/design-system.mjs +20 -736
  5. package/cli/engine/detect-antipatterns-browser.js +12 -1
  6. package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
  7. package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
  8. package/cli/engine/engines/regex/detect-text.mjs +11 -523
  9. package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
  10. package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
  11. package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
  12. package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
  13. package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
  14. package/cli/engine/registry/antipatterns-provider.mjs +60 -0
  15. package/cli/engine/registry/antipatterns-quality.mjs +218 -0
  16. package/cli/engine/registry/antipatterns-slop.mjs +226 -0
  17. package/cli/engine/registry/antipatterns.mjs +7 -494
  18. package/cli/engine/rules/checks-background.mjs +155 -0
  19. package/cli/engine/rules/checks-css-values.mjs +137 -0
  20. package/cli/engine/rules/checks-dom-effects.mjs +243 -0
  21. package/cli/engine/rules/checks-dom.mjs +148 -0
  22. package/cli/engine/rules/checks-element.mjs +286 -0
  23. package/cli/engine/rules/checks-html.mjs +204 -0
  24. package/cli/engine/rules/checks-overflow.mjs +263 -0
  25. package/cli/engine/rules/checks-page.mjs +465 -0
  26. package/cli/engine/rules/checks-pure.mjs +453 -0
  27. package/cli/engine/rules/checks-quality.mjs +447 -0
  28. package/cli/engine/rules/checks-stylesheet.mjs +324 -0
  29. package/cli/engine/rules/checks.mjs +98 -2939
  30. package/cli/lib/monodesign-config-consent.mjs +93 -0
  31. package/cli/lib/monodesign-config-core.mjs +37 -0
  32. package/cli/lib/monodesign-config-detection.mjs +133 -0
  33. package/cli/lib/monodesign-config-filter.mjs +198 -0
  34. package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
  35. package/cli/lib/monodesign-config.mjs +21 -623
  36. package/package.json +2 -2
  37. package/skill/scripts/context-fields.mjs +65 -0
  38. package/skill/scripts/context-resolve.mjs +425 -0
  39. package/skill/scripts/context-update.mjs +151 -0
  40. package/skill/scripts/context-utils.mjs +68 -0
  41. package/skill/scripts/context-workspace.mjs +245 -0
  42. package/skill/scripts/context.mjs +17 -891
  43. package/skill/scripts/hook-admin-config.mjs +152 -0
  44. package/skill/scripts/hook-admin-ignore.mjs +131 -0
  45. package/skill/scripts/hook-admin-manifest.mjs +292 -0
  46. package/skill/scripts/hook-admin.mjs +8 -541
  47. package/skill/scripts/hook-before-edit-shell.mjs +184 -0
  48. package/skill/scripts/hook-before-edit.mjs +9 -165
  49. package/skill/scripts/hook-lib-cache.mjs +145 -0
  50. package/skill/scripts/hook-lib-config.mjs +292 -0
  51. package/skill/scripts/hook-lib-detector.mjs +72 -0
  52. package/skill/scripts/hook-lib-events.mjs +182 -0
  53. package/skill/scripts/hook-lib-findings.mjs +201 -0
  54. package/skill/scripts/hook-lib-ignore.mjs +205 -0
  55. package/skill/scripts/hook-lib-render.mjs +273 -0
  56. package/skill/scripts/hook-lib-run.mjs +281 -0
  57. package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
  58. package/skill/scripts/hook-lib.mjs +71 -1747
  59. package/skill/scripts/lib/design-parser-colors.mjs +182 -0
  60. package/skill/scripts/lib/design-parser-extract.mjs +333 -0
  61. package/skill/scripts/lib/design-parser-sections.mjs +214 -0
  62. package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
  63. package/skill/scripts/lib/design-parser.mjs +10 -783
  64. package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
  65. package/skill/scripts/live/manual-apply-compact.mjs +122 -0
  66. package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
  67. package/skill/scripts/live/manual-apply-log.mjs +179 -0
  68. package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
  69. package/skill/scripts/live/manual-apply.mjs +65 -640
  70. package/skill/scripts/live/svelte-component-accept.mjs +248 -0
  71. package/skill/scripts/live/svelte-component-css.mjs +230 -0
  72. package/skill/scripts/live/svelte-component-markup.mjs +104 -0
  73. package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
  74. package/skill/scripts/live/svelte-component-session.mjs +158 -0
  75. package/skill/scripts/live/svelte-component.mjs +32 -792
  76. package/skill/scripts/live-accept-mutate.mjs +152 -0
  77. package/skill/scripts/live-accept-parse.mjs +370 -0
  78. package/skill/scripts/live-accept-scrub.mjs +59 -0
  79. package/skill/scripts/live-accept-search.mjs +58 -0
  80. package/skill/scripts/live-accept.mjs +15 -631
  81. package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
  82. package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
  83. package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
  84. package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
  85. package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
  86. package/skill/scripts/live-commit-manual-edits.mjs +24 -881
  87. package/skill/scripts/live-copy-edit-agent.mjs +16 -551
  88. package/skill/scripts/live-copy-edit-compact.mjs +137 -0
  89. package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
  90. package/skill/scripts/live-copy-edit-runners.mjs +297 -0
  91. package/skill/scripts/live-inject-csp.mjs +132 -0
  92. package/skill/scripts/live-inject-files.mjs +229 -0
  93. package/skill/scripts/live-inject-tag.mjs +114 -0
  94. package/skill/scripts/live-inject.mjs +13 -440
  95. package/skill/scripts/live-server-poll.mjs +232 -0
  96. package/skill/scripts/live-server-routes.mjs +449 -0
  97. package/skill/scripts/live-server-state.mjs +361 -0
  98. package/skill/scripts/live-server.mjs +10 -946
  99. package/skill/scripts/live-wrap-css.mjs +132 -0
  100. package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
  101. package/skill/scripts/live-wrap-search.mjs +247 -0
  102. package/skill/scripts/live-wrap.mjs +21 -484
  103. package/skill/scripts/palette-seeds.mjs +401 -0
  104. package/skill/scripts/palette.mjs +1 -395
@@ -7,14 +7,18 @@
7
7
  * helper to ask Codex/Claude to edit true source files.
8
8
  */
9
9
 
10
- import { spawn, spawnSync } from 'node:child_process';
11
10
  import fs from 'node:fs';
12
11
  import os from 'node:os';
13
12
  import path from 'node:path';
14
- import { createRequire } from 'node:module';
15
-
16
- const DEFAULT_TIMEOUT_MS = 60_000;
17
- const require = createRequire(import.meta.url);
13
+ import { compactBatchForPrompt, normalizeBatchResult, mockBatchResult } from './live-copy-edit-compact.mjs';
14
+ import {
15
+ parseCopyEditAgentResult,
16
+ chooseCopyEditAgent,
17
+ describeNoProviderError,
18
+ runCodex,
19
+ runClaude,
20
+ } from './live-copy-edit-runners.mjs';
21
+ import { runCopyEditPostApplyChecks } from './live-copy-edit-post-apply.mjs';
18
22
 
19
23
  export function buildCopyEditBatchPrompt(batch, { cwd = process.cwd() } = {}) {
20
24
  const repairLines = batch?.repair ? [
@@ -134,550 +138,11 @@ export async function runCopyEditBatchAgent(batch, opts = {}) {
134
138
  throw new Error(`AI copy-edit batch did not return a valid completion payload. ${tail.trim()}`);
135
139
  }
136
140
 
137
- export function runCopyEditPostApplyChecks({ cwd = process.cwd(), files = [] } = {}) {
138
- const failures = [];
139
- const warnings = [];
140
- const uniqueFiles = [...new Set((files || []).filter((file) => typeof file === 'string' && file.trim()))];
141
- for (const relativeFile of uniqueFiles) {
142
- const file = path.resolve(cwd, relativeFile);
143
- if (!isPathInsideOrEqual(cwd, file) || !fs.existsSync(file)) {
144
- warnings.push({ file: relativeFile, reason: 'file_missing_or_outside_cwd' });
145
- continue;
146
- }
147
- let content = '';
148
- try { content = fs.readFileSync(file, 'utf-8'); } catch (err) {
149
- failures.push({ file: relativeFile, reason: 'read_failed', message: err.message });
150
- continue;
151
- }
152
- const markerMatch = findLeftoverMonodesignMarker(content);
153
- if (markerMatch) failures.push({ file: relativeFile, reason: 'leftover_monodesign_marker', marker: markerMatch });
154
- if (/\.json$/.test(relativeFile)) {
155
- try {
156
- JSON.parse(content);
157
- } catch (err) {
158
- failures.push({
159
- file: relativeFile,
160
- reason: 'invalid_json',
161
- message: err.message || String(err),
162
- });
163
- }
164
- }
165
- const syntaxCheck = checkFrameworkSourceSyntax(relativeFile, content);
166
- if (syntaxCheck?.failure) failures.push(syntaxCheck.failure);
167
- if (syntaxCheck?.warning) warnings.push(syntaxCheck.warning);
168
- if (/\.(mjs|cjs|js)$/.test(relativeFile)) {
169
- const check = spawnSync(process.execPath, ['--check', file], { cwd, encoding: 'utf-8' });
170
- if (check.status !== 0) {
171
- failures.push({
172
- file: relativeFile,
173
- reason: 'invalid_js',
174
- message: (check.stderr || check.stdout || '').trim(),
175
- });
176
- }
177
- }
178
- }
179
- const validation = runManualEditValidationScript(cwd);
180
- if (validation?.failure) failures.push(validation.failure);
181
- if (validation?.warning) warnings.push(validation.warning);
182
- return { ok: failures.length === 0, failures, warnings };
183
- }
184
-
185
- function checkFrameworkSourceSyntax(relativeFile, content) {
186
- if (!/\.(jsx|tsx|ts)$/.test(relativeFile)) return null;
187
- let parser;
188
- try {
189
- parser = require('@babel/parser');
190
- } catch {
191
- return { warning: { file: relativeFile, reason: 'syntax_parser_unavailable' } };
192
- }
193
- const plugins = ['jsx'];
194
- if (/\.(ts|tsx)$/.test(relativeFile)) plugins.push('typescript');
195
- try {
196
- parser.parse(content, {
197
- sourceType: 'module',
198
- plugins,
199
- errorRecovery: false,
200
- });
201
- return null;
202
- } catch (err) {
203
- return {
204
- failure: {
205
- file: relativeFile,
206
- reason: 'invalid_source_syntax',
207
- message: err.message || String(err),
208
- },
209
- };
210
- }
211
- }
212
-
213
- function findLeftoverMonodesignMarker(content) {
214
- const commentMarker = content.match(/^\s*(?:<!--|\{\/\*)\s*monodesign-carbonize-(?:start|end)\b|^\s*(?:<!--|\{\/\*)\s*monodesign-variants-(?:start|end)\b/m);
215
- if (commentMarker) return commentMarker[0];
216
-
217
- const attrPattern = /\bdata-monodesign-(?:variants?|original-text|editable|text-wrap)\s*=/g;
218
- for (const line of content.split(/\r?\n/)) {
219
- attrPattern.lastIndex = 0;
220
- let match;
221
- while ((match = attrPattern.exec(line))) {
222
- if (!isInsideQuotedLiteral(line, match.index)) return match[0];
223
- }
224
- }
225
- return null;
226
- }
227
-
228
- function isInsideQuotedLiteral(line, index) {
229
- let quote = null;
230
- let escaped = false;
231
- for (let i = 0; i < index; i++) {
232
- const ch = line[i];
233
- if (escaped) {
234
- escaped = false;
235
- continue;
236
- }
237
- if (ch === '\\') {
238
- escaped = true;
239
- continue;
240
- }
241
- if (quote) {
242
- if (ch === quote) quote = null;
243
- continue;
244
- }
245
- if (ch === '"' || ch === "'" || ch === '`') quote = ch;
246
- }
247
- return quote !== null;
248
- }
249
-
250
- function runManualEditValidationScript(cwd) {
251
- const script = readManualEditValidationScript(cwd);
252
- if (!script) return null;
253
- const validation = spawnSync(script, {
254
- cwd,
255
- encoding: 'utf-8',
256
- shell: true,
257
- timeout: 30_000,
258
- });
259
- if (validation.error) {
260
- return {
261
- failure: {
262
- file: 'package.json',
263
- reason: 'manual_edit_validation_failed',
264
- message: validation.error.message || String(validation.error),
265
- },
266
- };
267
- }
268
- if (validation.status !== 0) {
269
- return {
270
- failure: {
271
- file: 'package.json',
272
- reason: 'manual_edit_validation_failed',
273
- message: [validation.stderr, validation.stdout].filter(Boolean).join('\n').trim(),
274
- },
275
- };
276
- }
277
- return null;
278
- }
279
-
280
- function readManualEditValidationScript(cwd) {
281
- const pkgPath = path.join(cwd, 'package.json');
282
- if (!fs.existsSync(pkgPath)) return null;
283
- try {
284
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
285
- const script = pkg?.scripts?.['monodesign:manual-edit-validate'];
286
- return typeof script === 'string' && script.trim() ? script : null;
287
- } catch {
288
- return null;
289
- }
290
- }
291
-
292
- function compactBatchForPrompt(batch) {
293
- return {
294
- pageUrl: batch?.pageUrl || null,
295
- repair: batch?.repair || undefined,
296
- entries: (batch?.entries || []).map((entry) => ({
297
- id: entry.id,
298
- pageUrl: entry.pageUrl,
299
- stagedAt: entry.stagedAt || null,
300
- element: compactContextForBatch(entry.element),
301
- ops: (entry.ops || []).map(compactBatchOp),
302
- })),
303
- candidates: batch?.candidates || [],
304
- };
305
- }
306
-
307
- function compactBatchOp(op) {
308
- return {
309
- entryId: op.entryId,
310
- ref: op.ref,
311
- contextRef: op.contextRef,
312
- tag: op.tag,
313
- elementId: op.elementId,
314
- classes: op.classes,
315
- originalText: op.originalText,
316
- newText: op.newText,
317
- deleted: op.deleted === true || undefined,
318
- sourceHint: op.sourceHint,
319
- leaf: compactContextForBatch(op.leaf),
320
- nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [],
321
- container: compactContextForBatch(op.container),
322
- contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [],
323
- };
324
- }
325
-
326
- function compactContextForBatch(value) {
327
- if (!value || typeof value !== 'object') return value || null;
328
- return {
329
- ref: value.ref,
330
- tagName: value.tagName,
331
- id: value.id,
332
- classes: value.classes,
333
- textContent: truncate(value.textContent, 900),
334
- outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800),
335
- };
336
- }
337
-
338
- function stripLiveRuntimeHtml(html) {
339
- if (typeof html !== 'string') return html || null;
340
- return html
341
- .replace(/\sdata-monodesign-(?:original-text|editable|text-wrap)(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/g, '')
342
- .replace(/\scontenteditable(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/g, '')
343
- .replace(/\sstyle=(["'])(?:(?!\1)[\s\S])*(?:-webkit-user-modify|user-select:\s*text|cursor:\s*text)(?:(?!\1)[\s\S])*\1/g, '');
344
- }
345
-
346
- function normalizeBatchResult(result) {
347
- const status = result.status === 'partial' ? 'partial' : result.status === 'error' ? 'error' : 'done';
348
- const appliedEntryIds = Array.isArray(result.appliedEntryIds)
349
- ? result.appliedEntryIds.filter((id) => typeof id === 'string')
350
- : [];
351
- const failed = Array.isArray(result.failed)
352
- ? result.failed.filter(Boolean).map((item) => ({
353
- entryId: item.entryId || item.id || null,
354
- reason: item.reason || item.message || 'failed',
355
- candidates: Array.isArray(item.candidates) ? item.candidates : [],
356
- }))
357
- : [];
358
- const files = Array.isArray(result.files) ? result.files.filter((file) => typeof file === 'string') : [];
359
- const notes = Array.isArray(result.notes) ? result.notes.filter((note) => typeof note === 'string') : [];
360
- const warnings = Array.isArray(result.warnings)
361
- ? result.warnings
362
- .filter(Boolean)
363
- .map((warning) => typeof warning === 'string' ? { message: warning } : warning)
364
- .filter((warning) => warning && typeof warning === 'object')
365
- : [];
366
- return {
367
- status,
368
- message: result.message || null,
369
- appliedEntryIds,
370
- failed,
371
- files,
372
- notes,
373
- warnings,
374
- };
375
- }
376
-
377
- function mockBatchResult(batch, env, cwd = process.cwd()) {
378
- applyMockWrites(env, cwd);
379
- const raw = env.MONODESIGN_LIVE_COPY_AGENT_MOCK_RESULT;
380
- if (raw) {
381
- const parsed = parseCopyEditBatchResult(raw);
382
- if (parsed) return parsed;
383
- throw new Error('Invalid MONODESIGN_LIVE_COPY_AGENT_MOCK_RESULT JSON');
384
- }
385
- return {
386
- status: 'done',
387
- appliedEntryIds: (batch?.entries || []).map((entry) => entry.id).filter(Boolean),
388
- failed: [],
389
- files: [],
390
- notes: ['mock copy-edit batch result'],
391
- };
392
- }
393
-
394
- function applyMockWrites(env, cwd) {
395
- const raw = env.MONODESIGN_LIVE_COPY_AGENT_MOCK_WRITES;
396
- if (!raw) return;
397
- const writes = tryParseJson(raw);
398
- if (!writes || typeof writes !== 'object' || Array.isArray(writes)) {
399
- throw new Error('Invalid MONODESIGN_LIVE_COPY_AGENT_MOCK_WRITES JSON');
400
- }
401
- for (const [relativeFile, content] of Object.entries(writes)) {
402
- if (typeof relativeFile !== 'string' || typeof content !== 'string') continue;
403
- const absolute = path.resolve(cwd, relativeFile);
404
- if (!isPathInsideOrEqual(cwd, absolute)) continue;
405
- fs.mkdirSync(path.dirname(absolute), { recursive: true });
406
- fs.writeFileSync(absolute, content, 'utf-8');
407
- }
408
- }
409
-
410
- export function parseCopyEditAgentResult(text) {
411
- const trimmed = String(text || '').trim();
412
- if (!trimmed) return null;
413
-
414
- const parsedOuter = tryParseJson(trimmed);
415
- if (parsedOuter) {
416
- if (typeof parsedOuter.result === 'string') {
417
- const nested = parseCopyEditAgentResult(parsedOuter.result);
418
- if (nested) return nested;
419
- }
420
- if (parsedOuter.status === 'done' || parsedOuter.status === 'partial' || parsedOuter.status === 'error') return parsedOuter;
421
- }
422
-
423
- const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
424
- if (!jsonMatch) return null;
425
- const parsed = tryParseJson(jsonMatch[0]);
426
- if (parsed?.status === 'done' || parsed?.status === 'partial' || parsed?.status === 'error') return parsed;
427
- return null;
428
- }
429
-
430
- export function chooseCopyEditAgent({
431
- env = process.env,
432
- authCheck = commandAuthed,
433
- chatAvailable = () => false,
434
- } = {}) {
435
- const mode = (env.MONODESIGN_LIVE_COPY_AGENT || 'auto').trim().toLowerCase();
436
- if (mode === '0' || mode === 'false' || mode === 'off' || mode === 'none') return null;
437
- if (mode === 'mock') return 'mock';
438
- if (mode === 'chat') return chatAvailable() ? 'chat' : null;
439
- if (mode === 'codex') return commandExists('codex') ? 'codex' : null;
440
- if (mode === 'claude') return commandExists('claude') ? 'claude' : null;
441
- if (mode !== 'auto') return null;
442
- if (authCheck('codex')) return 'codex';
443
- if (authCheck('claude')) return 'claude';
444
- if (chatAvailable()) return 'chat';
445
- return null;
446
- }
447
-
448
- function runCodex(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_TIMEOUT_MS }) {
449
- const args = [
450
- 'exec',
451
- '--cd', cwd,
452
- '--dangerously-bypass-approvals-and-sandbox',
453
- '--ephemeral',
454
- '--output-last-message', resultPath,
455
- '-c', `model_reasoning_effort="${env.MONODESIGN_LIVE_COPY_AGENT_EFFORT || 'low'}"`,
456
- ];
457
- if (env.MONODESIGN_LIVE_COPY_AGENT_MODEL) {
458
- args.push('--model', env.MONODESIGN_LIVE_COPY_AGENT_MODEL);
459
- }
460
- args.push('-');
461
- return runAgentProcess('codex', args, prompt, { cwd, env, logPath, timeoutMs });
462
- }
463
-
464
- function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_TIMEOUT_MS }) {
465
- const args = [
466
- '--print',
467
- '--permission-mode', 'bypassPermissions',
468
- '--output-format', 'json',
469
- ];
470
- if (env.MONODESIGN_LIVE_COPY_AGENT_MODEL) {
471
- args.push('--model', env.MONODESIGN_LIVE_COPY_AGENT_MODEL);
472
- }
473
- args.push(prompt);
474
- // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow
475
- // through. On macOS, `claude /login` stores creds in the Keychain, which a
476
- // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via
477
- // `claude setup-token`) is the supported headless auth path.
478
- return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath });
479
- }
480
-
481
- function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) {
482
- return new Promise((resolve, reject) => {
483
- const log = fs.createWriteStream(logPath, { flags: 'a' });
484
- const child = spawn(command, args, {
485
- cwd,
486
- env,
487
- stdio: ['pipe', 'pipe', 'pipe'],
488
- });
489
- let output = '';
490
- let settled = false;
491
- const timer = setTimeout(() => {
492
- child.kill('SIGTERM');
493
- rejectOnce(new Error(`AI copy-edit worker timed out after ${timeoutMs}ms`));
494
- }, timeoutMs);
495
-
496
- const rejectOnce = (err) => {
497
- if (settled) return;
498
- settled = true;
499
- clearTimeout(timer);
500
- log.end();
501
- reject(err);
502
- };
503
- const resolveOnce = () => {
504
- if (settled) return;
505
- settled = true;
506
- clearTimeout(timer);
507
- if (mirrorOutputPath) fs.writeFileSync(mirrorOutputPath, output);
508
- log.end();
509
- resolve();
510
- };
511
-
512
- process.once('SIGTERM', () => {
513
- try { child.kill('SIGTERM'); } catch {}
514
- });
515
- child.stdout.on('data', (chunk) => {
516
- output += chunk.toString();
517
- log.write(chunk);
518
- });
519
- child.stderr.on('data', (chunk) => {
520
- log.write(chunk);
521
- });
522
- child.on('error', rejectOnce);
523
- child.on('exit', (code, signal) => {
524
- if (code === 0) {
525
- resolveOnce();
526
- } else {
527
- const hint = extractRunnerErrorMessage(output, command);
528
- rejectOnce(new Error(hint || `${command} exited with ${signal || code}`));
529
- }
530
- });
531
- if (stdin) child.stdin.end(stdin);
532
- else child.stdin.end();
533
- });
534
- }
535
-
536
- function isPathInsideOrEqual(cwd, file) {
537
- const relative = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join('/');
538
- return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
539
- }
540
-
541
- function tryParseJson(text) {
542
- try { return JSON.parse(text); } catch { return null; }
543
- }
544
-
545
- function truncate(value, max) {
546
- if (typeof value !== 'string') return value;
547
- if (value.length <= max) return value;
548
- return `${value.slice(0, max)}... [truncated ${value.length - max} chars]`;
549
- }
550
-
551
- function commandExists(command) {
552
- const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
553
- return !result.error && result.status === 0;
554
- }
555
-
556
- /**
557
- * Build a diagnostic error message explaining why no AI runner is usable.
558
- * Splits the previous "Install/authenticate Codex or Claude" lump into a
559
- * per-provider summary so the user knows exactly which step unblocks them.
560
- */
561
- export function describeNoProviderError({
562
- exists = commandExists,
563
- chatAvailable = () => false,
564
- env = process.env,
565
- } = {}) {
566
- const lines = ['No live copy-edit AI runner is available.'];
567
- if (exists('claude')) {
568
- if (env.CLAUDE_CODE_OAUTH_TOKEN) {
569
- lines.push(' • Claude CLI: installed; CLAUDE_CODE_OAUTH_TOKEN is set but the CLI still rejected it. The token may be expired or invalid.');
570
- } else {
571
- lines.push(' • Claude CLI: installed but not selected. If Apply still fails, the subprocess may be unable to read your `claude /login` credentials (on macOS, the Keychain can be unreachable from a no-TTY child).');
572
- lines.push(' Headless fix: run `claude setup-token` once, then `export CLAUDE_CODE_OAUTH_TOKEN=<the printed sk-ant-oat01-… token>` before starting `live-server.mjs`.');
573
- lines.push(' Alternative: `export ANTHROPIC_API_KEY=<key>` if you have console.anthropic.com credits.');
574
- }
575
- } else {
576
- lines.push(' • Claude CLI: not installed.');
577
- }
578
- if (exists('codex')) {
579
- lines.push(' • Codex CLI: installed. If Apply still fails, run `codex login` to authenticate.');
580
- } else {
581
- lines.push(' • Codex CLI: not installed.');
582
- }
583
- if (chatAvailable()) {
584
- lines.push(' • Chat: an Monodesign live session is polling but selection chose another provider — unexpected; please report.');
585
- } else {
586
- lines.push(' • Chat: no Monodesign live session is currently polling on this server. Start Monodesign live in your chat to route Apply through the chat agent.');
587
- }
588
- lines.push('Fix one of the above, or set MONODESIGN_LIVE_COPY_AGENT=mock for tests.');
589
- return lines.join('\n');
590
- }
591
-
592
- /**
593
- * Pull a human-readable failure reason out of a subprocess's stdout when the
594
- * process exited non-zero. Recognizes:
595
- * - Claude CLI `--output-format json` errors:
596
- * {"is_error": true, "result": "Not logged in · Please run /login", ...}
597
- * - Generic JSON payloads with `message` or `error` strings.
598
- * - The last non-empty line of unstructured output.
599
- * Returns null when nothing meaningful surfaces, so the caller can fall back
600
- * to its existing "X exited with N" message.
601
- */
602
- export function extractRunnerErrorMessage(output, command) {
603
- const text = String(output || '').trim();
604
- if (!text) return null;
605
- const candidates = [];
606
- const direct = tryParseJson(text);
607
- if (direct) candidates.push(direct);
608
- const trailingMatch = text.match(/\{[\s\S]*\}\s*$/);
609
- if (trailingMatch) {
610
- const tail = tryParseJson(trailingMatch[0]);
611
- if (tail && tail !== direct) candidates.push(tail);
612
- }
613
- for (const parsed of candidates) {
614
- if (!parsed || typeof parsed !== 'object') continue;
615
- if (parsed.is_error === true && typeof parsed.result === 'string' && parsed.result.trim()) {
616
- return `${command} CLI: ${parsed.result.trim()}`;
617
- }
618
- if (typeof parsed.message === 'string' && parsed.message.trim()) {
619
- return `${command} CLI: ${parsed.message.trim()}`;
620
- }
621
- if (typeof parsed.error === 'string' && parsed.error.trim()) {
622
- return `${command} CLI: ${parsed.error.trim()}`;
623
- }
624
- }
625
- const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
626
- if (lines.length > 0) {
627
- const last = lines[lines.length - 1];
628
- if (last.length > 0 && last.length < 400) return `${command}: ${last}`;
629
- }
630
- return null;
631
- }
632
141
 
633
- /**
634
- * Pre-flight a CLI provider with a trivial prompt and report whether it can
635
- * actually do work. Cached per process so the `auto` branch of
636
- * chooseCopyEditAgent only pays the cost once per server boot.
637
- *
638
- * For claude we run the same `--print --output-format json` invocation we use
639
- * for real batches; an unauthenticated CLI fails in ~36 ms with
640
- * { is_error: true, result: "Not logged in · ..." }.
641
- * For codex we only confirm the binary exists — `codex exec` always burns a
642
- * real LLM call, so checking auth without spending tokens is not possible
643
- * here; if the user has codex installed but unauthed, the runtime error from
644
- * runCodex (now improved by extractRunnerErrorMessage) will surface clearly.
645
- */
646
- const COMMAND_AUTH_CACHE = new Map();
647
-
648
- function commandAuthed(command) {
649
- if (COMMAND_AUTH_CACHE.has(command)) return COMMAND_AUTH_CACHE.get(command);
650
- const ok = computeCommandAuthed(command);
651
- COMMAND_AUTH_CACHE.set(command, ok);
652
- return ok;
653
- }
654
-
655
- function computeCommandAuthed(command) {
656
- if (!commandExists(command)) return false;
657
- if (command === 'codex') return true;
658
- if (command !== 'claude') return false;
659
- let result;
660
- try {
661
- result = spawnSync('claude', [
662
- '--print',
663
- '--output-format', 'json',
664
- 'ping',
665
- ], {
666
- encoding: 'utf-8',
667
- timeout: 10000,
668
- env: process.env,
669
- });
670
- } catch {
671
- return false;
672
- }
673
- if (result.error || result.signal) return false;
674
- const stdout = String(result.stdout || '').trim();
675
- if (result.status !== 0) {
676
- // Non-zero exit: probably an auth or config error. Definitely not usable.
677
- return false;
678
- }
679
- if (!stdout) return true;
680
- const parsed = tryParseJson(stdout) || tryParseJson(stdout.match(/\{[\s\S]*\}\s*$/)?.[0] || '');
681
- if (parsed && parsed.is_error === true) return false;
682
- return true;
683
- }
142
+ export {
143
+ runCopyEditPostApplyChecks,
144
+ parseCopyEditAgentResult,
145
+ chooseCopyEditAgent,
146
+ describeNoProviderError,
147
+ };
148
+ export { extractRunnerErrorMessage } from './live-copy-edit-runners.mjs';