nothumanallowed 14.3.2 → 14.3.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nothumanallowed",
|
|
3
|
-
"version": "14.3.
|
|
3
|
+
"version": "14.3.4",
|
|
4
4
|
"description": "NotHumanAllowed — 38 AI agents, 80 tools, Studio (visual agentic workflows). Email, calendar, browser automation, screen capture, canvas, cron/heartbeat, Alexandria E2E messaging, GitHub, Notion, Slack, voice chat, free AI (Liara), 28 languages. Zero-dependency CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/constants.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __filename = fileURLToPath(import.meta.url);
|
|
6
6
|
const __dirname = path.dirname(__filename);
|
|
7
7
|
|
|
8
|
-
export const VERSION = '14.3.
|
|
8
|
+
export const VERSION = '14.3.4';
|
|
9
9
|
export const BASE_URL = 'https://nothumanallowed.com/cli';
|
|
10
10
|
export const API_BASE = 'https://nothumanallowed.com/api/v1';
|
|
11
11
|
|
|
@@ -488,31 +488,67 @@ async function runWebCraftAgent(config, projectName, message, attachments, emit)
|
|
|
488
488
|
const toolSpec = `
|
|
489
489
|
AVAILABLE TOOLS (use exactly ONE tool per <tool> tag):
|
|
490
490
|
|
|
491
|
-
|
|
491
|
+
── FILE OPERATIONS ──
|
|
492
|
+
|
|
493
|
+
1. read — Read a file's content:
|
|
492
494
|
<tool>{"op":"read","path":"relative/path.js"}</tool>
|
|
493
495
|
|
|
494
|
-
2.
|
|
495
|
-
<tool>{"op":"edit","path":"relative/path.js","old":"
|
|
496
|
+
2. edit — Surgical replacement (EXACT match required):
|
|
497
|
+
<tool>{"op":"edit","path":"relative/path.js","old":"EXACT_CODE_TO_REPLACE","new":"REPLACEMENT_CODE"}</tool>
|
|
496
498
|
|
|
497
|
-
3. Write/create a file (
|
|
499
|
+
3. write — Write/create a file (full content):
|
|
498
500
|
<tool>{"op":"write","path":"relative/path.js","content":"FULL_FILE_CONTENT"}</tool>
|
|
499
501
|
|
|
500
|
-
4.
|
|
502
|
+
4. rename — Rename or move a file:
|
|
503
|
+
<tool>{"op":"rename","path":"old/path.js","newPath":"new/path.js"}</tool>
|
|
504
|
+
|
|
505
|
+
5. delete — Delete a file:
|
|
506
|
+
<tool>{"op":"delete","path":"relative/path.js"}</tool>
|
|
507
|
+
|
|
508
|
+
── VERIFICATION ──
|
|
509
|
+
|
|
510
|
+
6. check — Syntax check (JS/JSON/CSS/HTML):
|
|
501
511
|
<tool>{"op":"check","path":"relative/path.js"}</tool>
|
|
502
512
|
|
|
513
|
+
7. lint — Full diagnostics with line numbers:
|
|
514
|
+
<tool>{"op":"lint","path":"relative/path.js"}</tool>
|
|
515
|
+
|
|
516
|
+
8. search — Grep/find text in project files:
|
|
517
|
+
<tool>{"op":"search","query":"searchPattern","glob":"*.js"}</tool>
|
|
518
|
+
|
|
519
|
+
9. list — List all project files with sizes:
|
|
520
|
+
<tool>{"op":"list"}</tool>
|
|
521
|
+
|
|
522
|
+
── EXECUTION ──
|
|
523
|
+
|
|
524
|
+
10. run — Execute a shell command in the project directory:
|
|
525
|
+
<tool>{"op":"run","cmd":"npm install express"}</tool>
|
|
526
|
+
<tool>{"op":"run","cmd":"npm test"}</tool>
|
|
527
|
+
<tool>{"op":"run","cmd":"node -e \\"console.log(1+1)\\""}</tool>
|
|
528
|
+
|
|
529
|
+
11. sandbox — Restart the sandbox server to test changes:
|
|
530
|
+
<tool>{"op":"sandbox"}</tool>
|
|
531
|
+
|
|
532
|
+
── DIFF ──
|
|
533
|
+
|
|
534
|
+
12. diff — Show diff between current file and last snapshot:
|
|
535
|
+
<tool>{"op":"diff","path":"relative/path.js"}</tool>
|
|
536
|
+
|
|
503
537
|
WORKFLOW — follow this for every change:
|
|
504
538
|
1. Read the file(s) you need to modify
|
|
505
539
|
2. Make your changes with edit or write
|
|
506
|
-
3. Use check to verify
|
|
507
|
-
4.
|
|
540
|
+
3. Use check or lint to verify — fix any errors
|
|
541
|
+
4. Use sandbox to restart and verify the app works
|
|
508
542
|
5. When ALL changes are complete and verified, output: <done/>
|
|
509
543
|
|
|
510
544
|
RULES:
|
|
511
|
-
- "old" must be
|
|
545
|
+
- "old" in edit must be EXACT verbatim code — copy-paste from read output
|
|
512
546
|
- Use edit for targeted changes, write for new files or complete rewrites
|
|
513
|
-
- ALWAYS read before edit if you haven't seen the file content
|
|
514
|
-
- ALWAYS check
|
|
515
|
-
-
|
|
547
|
+
- ALWAYS read before edit if you haven't seen the file content
|
|
548
|
+
- ALWAYS check/lint after modifications
|
|
549
|
+
- Use run for npm install, npm test, or any shell command
|
|
550
|
+
- Use search to find code patterns across the project
|
|
551
|
+
- Output <done/> when you are completely finished — MANDATORY
|
|
516
552
|
`;
|
|
517
553
|
|
|
518
554
|
// Build system prompt with fresh file list each step
|
|
@@ -611,19 +647,24 @@ RULES:
|
|
|
611
647
|
continue;
|
|
612
648
|
}
|
|
613
649
|
|
|
614
|
-
const { op, path: relPath, old: oldStr, new: newStr, content } = toolCall;
|
|
615
|
-
|
|
650
|
+
const { op, path: relPath, old: oldStr, new: newStr, content, newPath, query, glob: globPat, cmd } = toolCall;
|
|
651
|
+
|
|
652
|
+
// Path validation (skip for path-less ops)
|
|
653
|
+
const needsPath = !['list', 'search', 'sandbox', 'run'].includes(op);
|
|
654
|
+
if (needsPath && (!relPath || !_isSafePath(relPath))) {
|
|
616
655
|
toolResults.push({ op, path: relPath, result: 'unsafe_path' });
|
|
617
656
|
emit({ type: 'tool', op, path: relPath ?? '', result: 'unsafe_path' });
|
|
618
657
|
continue;
|
|
619
658
|
}
|
|
620
659
|
|
|
660
|
+
// ── read ──
|
|
621
661
|
if (op === 'read') {
|
|
622
662
|
const src = ProjectStore.readFile(projectName, relPath);
|
|
623
663
|
const result = src !== null ? 'ok' : 'not_found';
|
|
624
|
-
toolResults.push({ op: 'read', path: relPath, result, content: src?.slice(0,
|
|
664
|
+
toolResults.push({ op: 'read', path: relPath, result, content: src?.slice(0, 16000) });
|
|
625
665
|
emit({ type: 'tool', op: 'read', path: relPath, result });
|
|
626
666
|
|
|
667
|
+
// ── check ──
|
|
627
668
|
} else if (op === 'check') {
|
|
628
669
|
const src = ProjectStore.readFile(projectName, relPath);
|
|
629
670
|
let checkResult = 'ok';
|
|
@@ -633,7 +674,6 @@ RULES:
|
|
|
633
674
|
} else if (relPath.endsWith('.json')) {
|
|
634
675
|
try { JSON.parse(src); } catch (e) { checkResult = `json_error: ${e.message}`; }
|
|
635
676
|
} else if (relPath.endsWith('.css')) {
|
|
636
|
-
// Basic CSS validation: balanced braces
|
|
637
677
|
const opens = (src.match(/\{/g) || []).length;
|
|
638
678
|
const closes = (src.match(/\}/g) || []).length;
|
|
639
679
|
if (opens !== closes) checkResult = `css_error: unbalanced braces (${opens} open, ${closes} close)`;
|
|
@@ -643,6 +683,38 @@ RULES:
|
|
|
643
683
|
toolResults.push({ op: 'check', path: relPath, result: checkResult });
|
|
644
684
|
emit({ type: 'tool', op: 'check', path: relPath, result: checkResult });
|
|
645
685
|
|
|
686
|
+
// ── lint (full diagnostics with line numbers) ──
|
|
687
|
+
} else if (op === 'lint') {
|
|
688
|
+
const src = ProjectStore.readFile(projectName, relPath);
|
|
689
|
+
if (!src) {
|
|
690
|
+
toolResults.push({ op: 'lint', path: relPath, result: 'file_not_found' });
|
|
691
|
+
emit({ type: 'tool', op: 'lint', path: relPath, result: 'file_not_found' });
|
|
692
|
+
} else {
|
|
693
|
+
const diags = [];
|
|
694
|
+
const ext = relPath.split('.').pop()?.toLowerCase();
|
|
695
|
+
if (ext === 'js' || ext === 'mjs') {
|
|
696
|
+
try { new Function(src); } catch (e) {
|
|
697
|
+
diags.push({ line: 1, message: e.message.replace(/\n.*/s, ''), severity: 'error' });
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (ext === 'json') {
|
|
701
|
+
try { JSON.parse(src); } catch (e) {
|
|
702
|
+
diags.push({ line: 1, message: e.message, severity: 'error' });
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (ext === 'css') {
|
|
706
|
+
const o = (src.match(/\{/g) || []).length, c = (src.match(/\}/g) || []).length;
|
|
707
|
+
if (o !== c) diags.push({ line: src.split('\n').length, message: `Unbalanced braces: ${o} open, ${c} close`, severity: 'warning' });
|
|
708
|
+
}
|
|
709
|
+
if (ext === 'html' && !src.includes('</html>')) {
|
|
710
|
+
diags.push({ line: src.split('\n').length, message: 'Missing </html>', severity: 'warning' });
|
|
711
|
+
}
|
|
712
|
+
const result = diags.length === 0 ? 'ok' : diags.map((d) => `L${d.line}: [${d.severity}] ${d.message}`).join('\n');
|
|
713
|
+
toolResults.push({ op: 'lint', path: relPath, result });
|
|
714
|
+
emit({ type: 'tool', op: 'lint', path: relPath, result });
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ── edit ──
|
|
646
718
|
} else if (op === 'edit') {
|
|
647
719
|
const src = ProjectStore.readFile(projectName, relPath);
|
|
648
720
|
if (src === null) {
|
|
@@ -669,6 +741,7 @@ RULES:
|
|
|
669
741
|
emit({ type: 'tool', op: 'edit', path: relPath, result: 'ok', oldSnippet: oldStr.slice(0, 300), newSnippet: newStr?.slice(0, 300) ?? '' });
|
|
670
742
|
}
|
|
671
743
|
|
|
744
|
+
// ── write ──
|
|
672
745
|
} else if (op === 'write') {
|
|
673
746
|
if (content === undefined) {
|
|
674
747
|
toolResults.push({ op: 'write', path: relPath, result: 'missing_content' });
|
|
@@ -680,6 +753,179 @@ RULES:
|
|
|
680
753
|
toolResults.push({ op: 'write', path: relPath, result: 'ok' });
|
|
681
754
|
emit({ type: 'tool', op: 'write', path: relPath, result: 'ok' });
|
|
682
755
|
}
|
|
756
|
+
|
|
757
|
+
// ── rename ──
|
|
758
|
+
} else if (op === 'rename') {
|
|
759
|
+
if (!newPath || !_isSafePath(newPath)) {
|
|
760
|
+
toolResults.push({ op: 'rename', path: relPath, result: 'invalid_newPath' });
|
|
761
|
+
emit({ type: 'tool', op: 'rename', path: relPath, result: 'invalid_newPath' });
|
|
762
|
+
} else {
|
|
763
|
+
const oldAbs = path.join(dir, relPath);
|
|
764
|
+
const newAbs = path.join(dir, newPath);
|
|
765
|
+
if (!fs.existsSync(oldAbs)) {
|
|
766
|
+
toolResults.push({ op: 'rename', path: relPath, result: 'file_not_found' });
|
|
767
|
+
emit({ type: 'tool', op: 'rename', path: relPath, result: 'file_not_found' });
|
|
768
|
+
} else {
|
|
769
|
+
ensureDir(path.dirname(newAbs));
|
|
770
|
+
fs.renameSync(oldAbs, newAbs);
|
|
771
|
+
hasChanges = true;
|
|
772
|
+
modifiedFiles.add(newPath);
|
|
773
|
+
toolResults.push({ op: 'rename', path: relPath, result: `ok → ${newPath}` });
|
|
774
|
+
emit({ type: 'tool', op: 'rename', path: relPath, result: 'ok' });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// ── delete ──
|
|
779
|
+
} else if (op === 'delete') {
|
|
780
|
+
const abs = path.join(dir, relPath);
|
|
781
|
+
if (!fs.existsSync(abs)) {
|
|
782
|
+
toolResults.push({ op: 'delete', path: relPath, result: 'file_not_found' });
|
|
783
|
+
emit({ type: 'tool', op: 'delete', path: relPath, result: 'file_not_found' });
|
|
784
|
+
} else {
|
|
785
|
+
fs.unlinkSync(abs);
|
|
786
|
+
hasChanges = true;
|
|
787
|
+
toolResults.push({ op: 'delete', path: relPath, result: 'ok' });
|
|
788
|
+
emit({ type: 'tool', op: 'delete', path: relPath, result: 'ok' });
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// ── search (grep) ──
|
|
792
|
+
} else if (op === 'search') {
|
|
793
|
+
const matches = ProjectStore.grep(projectName, query || '', globPat);
|
|
794
|
+
const resultText = matches.length === 0 ? 'no_matches'
|
|
795
|
+
: matches.slice(0, 30).map((m) => `${m.file}:${m.lineNum}: ${m.line}`).join('\n');
|
|
796
|
+
toolResults.push({ op: 'search', result: resultText, content: resultText });
|
|
797
|
+
emit({ type: 'tool', op: 'search', path: query || '', result: `${matches.length} matches` });
|
|
798
|
+
|
|
799
|
+
// ── list ──
|
|
800
|
+
} else if (op === 'list') {
|
|
801
|
+
const allFiles = _listProjectFiles(dir);
|
|
802
|
+
const listing = allFiles.map((f) => {
|
|
803
|
+
try {
|
|
804
|
+
const stat = fs.statSync(path.join(dir, f));
|
|
805
|
+
return `${f} (${stat.size} B)`;
|
|
806
|
+
} catch { return f; }
|
|
807
|
+
}).join('\n');
|
|
808
|
+
toolResults.push({ op: 'list', result: listing, content: listing });
|
|
809
|
+
emit({ type: 'tool', op: 'list', path: '', result: `${allFiles.length} files` });
|
|
810
|
+
|
|
811
|
+
// ── run (shell command) ──
|
|
812
|
+
} else if (op === 'run') {
|
|
813
|
+
if (!cmd) {
|
|
814
|
+
toolResults.push({ op: 'run', result: 'missing cmd' });
|
|
815
|
+
emit({ type: 'tool', op: 'run', path: '', result: 'missing_cmd' });
|
|
816
|
+
} else {
|
|
817
|
+
// Security: block dangerous commands
|
|
818
|
+
const blocked = /rm\s+-rf|rmdir|format|mkfs|dd\s+if|shutdown|reboot|kill\s+-9\s+1\b/i;
|
|
819
|
+
if (blocked.test(cmd)) {
|
|
820
|
+
toolResults.push({ op: 'run', result: 'blocked: dangerous command' });
|
|
821
|
+
emit({ type: 'tool', op: 'run', path: cmd, result: 'blocked' });
|
|
822
|
+
} else {
|
|
823
|
+
try {
|
|
824
|
+
const { stdout, stderr } = await execAsync(cmd, {
|
|
825
|
+
cwd: dir,
|
|
826
|
+
timeout: 30_000,
|
|
827
|
+
env: { ...process.env, NODE_ENV: 'development' },
|
|
828
|
+
});
|
|
829
|
+
const output = (stdout + (stderr ? `\n[stderr] ${stderr}` : '')).slice(0, 8000);
|
|
830
|
+
toolResults.push({ op: 'run', result: output || '(no output)', content: output });
|
|
831
|
+
emit({ type: 'tool', op: 'run', path: cmd, result: 'ok' });
|
|
832
|
+
} catch (e) {
|
|
833
|
+
const errMsg = (e.stderr || e.message || '').slice(0, 2000);
|
|
834
|
+
toolResults.push({ op: 'run', result: `error: ${errMsg}`, content: errMsg });
|
|
835
|
+
emit({ type: 'tool', op: 'run', path: cmd, result: 'error' });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// ── sandbox (restart) ──
|
|
841
|
+
} else if (op === 'sandbox') {
|
|
842
|
+
if (sandbox.isRunning()) {
|
|
843
|
+
await sandbox.stop();
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
const projectDir = ProjectStore.dir(projectName);
|
|
847
|
+
const port = await _findFreePort(4000, 4999);
|
|
848
|
+
if (port) {
|
|
849
|
+
const shimDir = path.join(projectDir, '.nha-shims');
|
|
850
|
+
ensureDir(shimDir);
|
|
851
|
+
_writeShims(shimDir);
|
|
852
|
+
const entryFile = _detectEntry(projectDir);
|
|
853
|
+
if (entryFile) {
|
|
854
|
+
const patchedEntry = _patchEntry(projectDir, entryFile, shimDir, port);
|
|
855
|
+
const proc = spawn('node', [patchedEntry], {
|
|
856
|
+
cwd: projectDir,
|
|
857
|
+
env: { ...process.env, PORT: String(port), NODE_ENV: 'development', NHA_SANDBOX: '1' },
|
|
858
|
+
detached: false, stdio: ['ignore', 'pipe', 'pipe'],
|
|
859
|
+
});
|
|
860
|
+
sandbox._sandbox = { proc, port, projectName, startedAt: new Date(), healthy: false };
|
|
861
|
+
let sandboxStderr = '';
|
|
862
|
+
proc.stderr.on('data', (d) => { sandboxStderr += d.toString(); });
|
|
863
|
+
proc.stdout.on('data', (d) => {
|
|
864
|
+
if (/listen|running|started|ready|port/i.test(d.toString())) sandbox._sandbox.healthy = true;
|
|
865
|
+
});
|
|
866
|
+
const healthy = await _waitForPort(port, 10_000);
|
|
867
|
+
if (healthy) {
|
|
868
|
+
sandbox._sandbox.healthy = true;
|
|
869
|
+
toolResults.push({ op: 'sandbox', result: `ok: running on port ${port}` });
|
|
870
|
+
emit({ type: 'tool', op: 'sandbox', path: '', result: `port:${port}` });
|
|
871
|
+
emit({ type: 'sandbox_ready', port });
|
|
872
|
+
} else {
|
|
873
|
+
const errLine = sandboxStderr.split('\n').find((l) => l.includes('Error')) || 'startup timeout';
|
|
874
|
+
toolResults.push({ op: 'sandbox', result: `error: ${errLine.slice(0, 500)}`, content: errLine });
|
|
875
|
+
emit({ type: 'tool', op: 'sandbox', path: '', result: 'error' });
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
toolResults.push({ op: 'sandbox', result: 'no entry point found' });
|
|
879
|
+
emit({ type: 'tool', op: 'sandbox', path: '', result: 'no_entry' });
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} catch (e) {
|
|
883
|
+
toolResults.push({ op: 'sandbox', result: `error: ${e.message?.slice(0, 200)}` });
|
|
884
|
+
emit({ type: 'tool', op: 'sandbox', path: '', result: 'error' });
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ── diff ──
|
|
888
|
+
} else if (op === 'diff') {
|
|
889
|
+
const current = ProjectStore.readFile(projectName, relPath);
|
|
890
|
+
// Load last snapshot
|
|
891
|
+
const snapshots = SnapshotStore.list(projectName);
|
|
892
|
+
if (!current) {
|
|
893
|
+
toolResults.push({ op: 'diff', path: relPath, result: 'file_not_found' });
|
|
894
|
+
emit({ type: 'tool', op: 'diff', path: relPath, result: 'file_not_found' });
|
|
895
|
+
} else if (snapshots.length === 0) {
|
|
896
|
+
toolResults.push({ op: 'diff', path: relPath, result: 'no snapshots available' });
|
|
897
|
+
emit({ type: 'tool', op: 'diff', path: relPath, result: 'no_snapshots' });
|
|
898
|
+
} else {
|
|
899
|
+
const lastSnap = snapshots[0];
|
|
900
|
+
const snapDir = SnapshotStore.dir(projectName);
|
|
901
|
+
try {
|
|
902
|
+
const snapData = JSON.parse(fs.readFileSync(path.join(snapDir, `${lastSnap.ts}.json`), 'utf-8'));
|
|
903
|
+
const oldFile = snapData.files?.find((f) => f.name === relPath);
|
|
904
|
+
const oldContent = oldFile?.content || '';
|
|
905
|
+
// Simple line diff
|
|
906
|
+
const oldLines = oldContent.split('\n');
|
|
907
|
+
const newLines = current.split('\n');
|
|
908
|
+
const diffs = [];
|
|
909
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
910
|
+
for (let i = 0; i < maxLen; i++) {
|
|
911
|
+
if (oldLines[i] !== newLines[i]) {
|
|
912
|
+
if (oldLines[i] !== undefined) diffs.push(`- L${i + 1}: ${oldLines[i]}`);
|
|
913
|
+
if (newLines[i] !== undefined) diffs.push(`+ L${i + 1}: ${newLines[i]}`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const diffText = diffs.length === 0 ? 'no changes' : diffs.slice(0, 100).join('\n');
|
|
917
|
+
toolResults.push({ op: 'diff', path: relPath, result: diffText, content: diffText });
|
|
918
|
+
emit({ type: 'tool', op: 'diff', path: relPath, result: `${diffs.length} changes` });
|
|
919
|
+
} catch {
|
|
920
|
+
toolResults.push({ op: 'diff', path: relPath, result: 'snapshot read error' });
|
|
921
|
+
emit({ type: 'tool', op: 'diff', path: relPath, result: 'error' });
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ── unknown op ──
|
|
926
|
+
} else {
|
|
927
|
+
toolResults.push({ op, path: relPath, result: `unknown_op: ${op}` });
|
|
928
|
+
emit({ type: 'tool', op, path: relPath ?? '', result: 'unknown_op' });
|
|
683
929
|
}
|
|
684
930
|
}
|
|
685
931
|
|
|
@@ -1438,6 +1684,68 @@ export function register(router) {
|
|
|
1438
1684
|
} catch (e) { sendError(res, 500, e.message); }
|
|
1439
1685
|
});
|
|
1440
1686
|
|
|
1687
|
+
// ── Diagnostics (lint) — returns errors/warnings for a file ───────────────
|
|
1688
|
+
router.post('/api/studio/webcraft/lint', async (req, res) => {
|
|
1689
|
+
try {
|
|
1690
|
+
const { projectName, path: relPath } = await parseBody(req);
|
|
1691
|
+
if (!projectName || !relPath) return sendError(res, 400, 'projectName and path required');
|
|
1692
|
+
const content = ProjectStore.readFile(projectName, relPath);
|
|
1693
|
+
if (content === null) return sendJSON(res, 200, { diagnostics: [] });
|
|
1694
|
+
|
|
1695
|
+
const diagnostics = [];
|
|
1696
|
+
const ext = relPath.split('.').pop()?.toLowerCase();
|
|
1697
|
+
|
|
1698
|
+
if (ext === 'js' || ext === 'mjs' || ext === 'jsx') {
|
|
1699
|
+
try { new Function(content); } catch (e) {
|
|
1700
|
+
const match = e.message.match(/^(.*?)$/m);
|
|
1701
|
+
const lineMatch = e.message.match(/:(\d+):(\d+)/);
|
|
1702
|
+
diagnostics.push({
|
|
1703
|
+
from: lineMatch ? { line: parseInt(lineMatch[1]), col: parseInt(lineMatch[2]) } : { line: 1, col: 0 },
|
|
1704
|
+
severity: 'error',
|
|
1705
|
+
message: match?.[1] || e.message,
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
if (ext === 'json') {
|
|
1711
|
+
try { JSON.parse(content); } catch (e) {
|
|
1712
|
+
const posMatch = e.message.match(/position (\d+)/);
|
|
1713
|
+
const pos = posMatch ? parseInt(posMatch[1]) : 0;
|
|
1714
|
+
const lines = content.slice(0, pos).split('\n');
|
|
1715
|
+
diagnostics.push({
|
|
1716
|
+
from: { line: lines.length, col: (lines[lines.length - 1] || '').length },
|
|
1717
|
+
severity: 'error',
|
|
1718
|
+
message: e.message,
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
if (ext === 'css') {
|
|
1724
|
+
const opens = (content.match(/\{/g) || []).length;
|
|
1725
|
+
const closes = (content.match(/\}/g) || []).length;
|
|
1726
|
+
if (opens !== closes) {
|
|
1727
|
+
diagnostics.push({
|
|
1728
|
+
from: { line: content.split('\n').length, col: 0 },
|
|
1729
|
+
severity: 'warning',
|
|
1730
|
+
message: `Unbalanced braces: ${opens} open, ${closes} close`,
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
if (ext === 'html' || ext === 'htm') {
|
|
1736
|
+
if (!content.includes('</html>')) {
|
|
1737
|
+
diagnostics.push({
|
|
1738
|
+
from: { line: content.split('\n').length, col: 0 },
|
|
1739
|
+
severity: 'warning',
|
|
1740
|
+
message: 'Missing </html> closing tag',
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
sendJSON(res, 200, { diagnostics });
|
|
1746
|
+
} catch (e) { sendError(res, 500, e.message); }
|
|
1747
|
+
});
|
|
1748
|
+
|
|
1441
1749
|
// ── File write (from IDE editor) ──────────────────────────────────────────
|
|
1442
1750
|
router.post('/api/studio/webcraft/file/write', async (req, res) => {
|
|
1443
1751
|
try {
|