@ddtcorex/dsh-maestro-review 0.1.0 → 0.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/lib/config-store.d.ts +2 -0
- package/lib/config-store.d.ts.map +1 -1
- package/lib/config-store.js.map +1 -1
- package/lib/git-worktree-tool.d.ts +11 -0
- package/lib/git-worktree-tool.d.ts.map +1 -0
- package/lib/git-worktree-tool.js +162 -0
- package/lib/git-worktree-tool.js.map +1 -0
- package/lib/govard-audit-lint-tool.d.ts +14 -0
- package/lib/govard-audit-lint-tool.d.ts.map +1 -0
- package/lib/govard-audit-lint-tool.js +171 -0
- package/lib/govard-audit-lint-tool.js.map +1 -0
- package/lib/hyva-csp-scan-tool.d.ts +11 -0
- package/lib/hyva-csp-scan-tool.d.ts.map +1 -0
- package/lib/hyva-csp-scan-tool.js +193 -0
- package/lib/hyva-csp-scan-tool.js.map +1 -0
- package/lib/hyva-theme-inspect-tool.d.ts +10 -0
- package/lib/hyva-theme-inspect-tool.d.ts.map +1 -0
- package/lib/hyva-theme-inspect-tool.js +299 -0
- package/lib/hyva-theme-inspect-tool.js.map +1 -0
- package/lib/incremental.d.ts +28 -0
- package/lib/incremental.d.ts.map +1 -0
- package/lib/incremental.js +60 -0
- package/lib/incremental.js.map +1 -0
- package/lib/layout-xml-tool.d.ts +11 -0
- package/lib/layout-xml-tool.d.ts.map +1 -0
- package/lib/layout-xml-tool.js +240 -0
- package/lib/layout-xml-tool.js.map +1 -0
- package/lib/module-check-tool.d.ts +11 -0
- package/lib/module-check-tool.d.ts.map +1 -0
- package/lib/module-check-tool.js +304 -0
- package/lib/module-check-tool.js.map +1 -0
- package/lib/orchestrator.d.ts +2 -0
- package/lib/orchestrator.d.ts.map +1 -1
- package/lib/orchestrator.js +203 -78
- package/lib/orchestrator.js.map +1 -1
- package/lib/perf-log-stats-tool.d.ts +13 -0
- package/lib/perf-log-stats-tool.d.ts.map +1 -0
- package/lib/perf-log-stats-tool.js +245 -0
- package/lib/perf-log-stats-tool.js.map +1 -0
- package/lib/phtml-escape-scan-tool.d.ts +11 -0
- package/lib/phtml-escape-scan-tool.d.ts.map +1 -0
- package/lib/phtml-escape-scan-tool.js +170 -0
- package/lib/phtml-escape-scan-tool.js.map +1 -0
- package/lib/plan-track-tool.d.ts +11 -0
- package/lib/plan-track-tool.d.ts.map +1 -0
- package/lib/plan-track-tool.js +143 -0
- package/lib/plan-track-tool.js.map +1 -0
- package/lib/review-history.d.ts +4 -0
- package/lib/review-history.d.ts.map +1 -1
- package/lib/review-history.js +4 -0
- package/lib/review-history.js.map +1 -1
- package/lib/scope-split-tool.d.ts +42 -0
- package/lib/scope-split-tool.d.ts.map +1 -0
- package/lib/scope-split-tool.js +109 -0
- package/lib/scope-split-tool.js.map +1 -0
- package/lib/search-tool.d.ts +15 -0
- package/lib/search-tool.d.ts.map +1 -0
- package/lib/search-tool.js +144 -0
- package/lib/search-tool.js.map +1 -0
- package/lib/settings-rpc.d.ts.map +1 -1
- package/lib/settings-rpc.js +47 -8
- package/lib/settings-rpc.js.map +1 -1
- package/lib/skills-tool.d.ts +1 -1
- package/lib/skills-tool.d.ts.map +1 -1
- package/lib/skills-tool.js +5 -1
- package/lib/skills-tool.js.map +1 -1
- package/lib/tdd-evidence-tool.d.ts +11 -0
- package/lib/tdd-evidence-tool.d.ts.map +1 -0
- package/lib/tdd-evidence-tool.js +114 -0
- package/lib/tdd-evidence-tool.js.map +1 -0
- package/lib/tool-policy.d.ts +20 -0
- package/lib/tool-policy.d.ts.map +1 -0
- package/lib/tool-policy.js +42 -0
- package/lib/tool-policy.js.map +1 -0
- package/lib/workspace-tool.d.ts.map +1 -1
- package/lib/workspace-tool.js +15 -9
- package/lib/workspace-tool.js.map +1 -1
- package/package.json +3 -2
- package/src/augment.d.ts +4 -0
- package/src/config-store.ts +2 -0
- package/src/git-worktree-tool.ts +130 -0
- package/src/govard-audit-lint-tool.ts +147 -0
- package/src/hyva-csp-scan-tool.ts +165 -0
- package/src/hyva-theme-inspect-tool.ts +258 -0
- package/src/incremental.ts +84 -0
- package/src/layout-xml-tool.ts +191 -0
- package/src/module-check-tool.ts +188 -0
- package/src/orchestrator.ts +189 -75
- package/src/perf-log-stats-tool.ts +224 -0
- package/src/phtml-escape-scan-tool.ts +117 -0
- package/src/plan-track-tool.ts +118 -0
- package/src/review-history.ts +8 -0
- package/src/scope-split-tool.ts +110 -0
- package/src/search-tool.ts +153 -0
- package/src/settings-rpc.ts +53 -10
- package/src/skills-tool.ts +5 -1
- package/src/tdd-evidence-tool.ts +85 -0
- package/src/tool-policy.ts +49 -0
- package/src/workspace-tool.ts +27 -9
- package/lib/pin-store.d.ts +0 -5
- package/lib/pin-store.d.ts.map +0 -1
- package/lib/pin-store.js +0 -5
- package/lib/pin-store.js.map +0 -1
- package/src/pin-store.ts +0 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { join, resolve, sep, basename, dirname } from 'node:path'
|
|
2
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import z from '@deepseek-ai/schemastery'
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
|
|
7
|
+
export const name = 'maestro-layout-xml-tool'
|
|
8
|
+
export const inject = ['tools']
|
|
9
|
+
export const Config: z<{ rootPath?: string }> = z.object({ rootPath: z.string() })
|
|
10
|
+
|
|
11
|
+
interface SessionCwdSource { agent?: { session?: { header?: { cwd?: string } } } }
|
|
12
|
+
function workspaceRootFor(c:string|undefined, exec:unknown):string{
|
|
13
|
+
if(c!==undefined) return c
|
|
14
|
+
const cwd=(exec as SessionCwdSource|undefined)?.agent?.session?.header?.cwd
|
|
15
|
+
return typeof cwd==='string' && cwd!=='' ? cwd : process.cwd()
|
|
16
|
+
}
|
|
17
|
+
async function isInsideRoot(root:string, target:string):Promise<boolean>{
|
|
18
|
+
const ar=resolve(root); const rs=resolve(ar,target)
|
|
19
|
+
if(rs!==ar && !rs.startsWith(ar+sep)) return false
|
|
20
|
+
try{ const rr=await realpath(ar); const rt=await realpath(rs); return rt===rr || rt.startsWith(rr+sep)}catch{return false}
|
|
21
|
+
}
|
|
22
|
+
const MAX_FILE_BYTES=1024*1024
|
|
23
|
+
const MAX_FILES=2000
|
|
24
|
+
const SKIP_DIRS=new Set(['.git','node_modules','vendor','pub/static'])
|
|
25
|
+
const LAYOUT_RE = /\/layout\//
|
|
26
|
+
|
|
27
|
+
// tiny helpers
|
|
28
|
+
function parseAttrs(s:string):Record<string,string>{
|
|
29
|
+
const out:Record<string,string>={}
|
|
30
|
+
let m:RegExpExecArray|null
|
|
31
|
+
const re=/(\w+)="([^"]*)"/g
|
|
32
|
+
while((m=re.exec(s))!==null) out[m[1]]=m[2]
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
async function exists(p:string):Promise<boolean>{ try{await stat(p); return true}catch{return false}}
|
|
36
|
+
async function* walkLayout(dir:string, root:string, budget:{files:number}):AsyncGenerator<string>{
|
|
37
|
+
if(budget.files<=0) return
|
|
38
|
+
let entries
|
|
39
|
+
try{ entries=await readdir(dir,{withFileTypes:true})}catch{return}
|
|
40
|
+
for(const e of entries){
|
|
41
|
+
if(budget.files<=0) return
|
|
42
|
+
if(e.isDirectory()){
|
|
43
|
+
if(SKIP_DIRS.has(e.name)) continue
|
|
44
|
+
yield* walkLayout(join(dir,e.name), root, budget)
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if(!e.isFile()) continue
|
|
48
|
+
if(!e.name.endsWith('.xml')) continue
|
|
49
|
+
if(!join(dir,e.name).includes('/layout/')) continue
|
|
50
|
+
budget.files-=1
|
|
51
|
+
yield join(dir,e.name)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function apply(ctx:Context, config:{rootPath?:string}={}):void{
|
|
56
|
+
const configuredRoot=config.rootPath
|
|
57
|
+
ctx.effect(()=>ctx.tools.register(defineTool({
|
|
58
|
+
name:'layout_xml_extract',
|
|
59
|
+
description:'Extract Magento layout handles/blocks/moves/head from layout XML under workspace. Use before claiming layout is correct.',
|
|
60
|
+
parameters:{
|
|
61
|
+
root:{type:'string'},
|
|
62
|
+
changedFiles:{type:'array', items:{type:'string'}},
|
|
63
|
+
},
|
|
64
|
+
output:{
|
|
65
|
+
schema:{
|
|
66
|
+
type:'object', additionalProperties:false,
|
|
67
|
+
properties:{
|
|
68
|
+
handles:{type:'array', required:true},
|
|
69
|
+
handleCount:{type:'number', required:true},
|
|
70
|
+
blockCount:{type:'number', required:true},
|
|
71
|
+
scannedFiles:{type:'number', required:true},
|
|
72
|
+
truncated:{type:'boolean', required:true},
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
render:(_args,v)=>[{type:'text', text: JSON.stringify(v,null,2)}],
|
|
76
|
+
},
|
|
77
|
+
async execute(args, exec){
|
|
78
|
+
const rawRoot=(args as {root?:string}).root ?? ''
|
|
79
|
+
const root=workspaceRootFor(configuredRoot, exec)
|
|
80
|
+
if(rawRoot!=='' && !(await isInsideRoot(root, rawRoot))) return {text:`Path "${rawRoot}" escapes the workspace root.`, truncated:false} as never
|
|
81
|
+
const effectiveRoot = rawRoot==='' ? root : resolve(root, rawRoot)
|
|
82
|
+
const changedFiles=(args as {changedFiles?:string[]}).changedFiles
|
|
83
|
+
if(changedFiles){
|
|
84
|
+
for(const p of changedFiles){
|
|
85
|
+
if(!(await isInsideRoot(effectiveRoot, p))) return {text:`Path "${p}" escapes the workspace root.`, truncated:false} as never
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// collect files
|
|
89
|
+
const budget={files:MAX_FILES}
|
|
90
|
+
const all:string[]=[]
|
|
91
|
+
for await(const f of walkLayout(effectiveRoot, effectiveRoot, budget)){
|
|
92
|
+
const rel=f.startsWith(effectiveRoot+sep)? f.slice(effectiveRoot.length+1): basename(f)
|
|
93
|
+
all.push(rel)
|
|
94
|
+
}
|
|
95
|
+
let files=all
|
|
96
|
+
if(changedFiles && changedFiles.length>0){
|
|
97
|
+
const set=new Set(changedFiles.map(p=>p.endsWith('.xml')?p:p))
|
|
98
|
+
files=all.filter(p=> set.has(p) || changedFiles.some(cf=> p.endsWith(basename(cf))))
|
|
99
|
+
// also if allow-list contains files not discovered via walk (e.g., outside layout but still layout), include them if they exist and are layout
|
|
100
|
+
for(const cf of changedFiles){
|
|
101
|
+
if(!files.includes(cf)){
|
|
102
|
+
const abs=resolve(effectiveRoot, cf)
|
|
103
|
+
if(await exists(abs) && cf.includes('/layout/') && cf.endsWith('.xml') && !(all.includes(cf))) files.push(cf)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const truncated = budget.files<=0
|
|
108
|
+
const handles:Array<{file:string,handle:string,layoutType:string,blocks:Array<{name:string|null,type:string,class:string|null,template:string|null,templateExists:boolean|null,as:string|null,cacheable:boolean|null,remove:boolean,refs:string[]}>,moves:Array<{element:string,destination:string|null,as:string|null,before:string|null,after:string|null}>,references:Array<{kind:string,name:string,attrs:Record<string,string>}>,head:{scripts:Array<{src:string}>,css:Array<{src:string}>,metas:Array<Record<string,string>>,removes:Array<string>},parseError:string|null}>=[]
|
|
109
|
+
let blockCount=0
|
|
110
|
+
for(const rel of files){
|
|
111
|
+
const abs=resolve(effectiveRoot, rel)
|
|
112
|
+
let text:string
|
|
113
|
+
try{
|
|
114
|
+
const buf=await readFile(abs)
|
|
115
|
+
if(buf.byteLength>MAX_FILE_BYTES) continue
|
|
116
|
+
text=buf.toString('utf-8')
|
|
117
|
+
}catch{continue}
|
|
118
|
+
const handle=basename(rel,'.xml')
|
|
119
|
+
const layoutType=/<page\b/.test(text)?'page':'generic'
|
|
120
|
+
// quick parseError heuristic: mismatched < vs > or unclosed tag
|
|
121
|
+
let parseError:string|null=null
|
|
122
|
+
const openCount=(text.match(/</g)||[]).length
|
|
123
|
+
const closeCount=(text.match(/>/g)||[]).length
|
|
124
|
+
if(openCount!==closeCount) parseError=`mismatched < (${openCount}) vs > (${closeCount})`
|
|
125
|
+
// also detect block tag without closing >
|
|
126
|
+
if(/<block[^>]*$/m.test(text) || /<block[^>]*<\/body/.test(text)) parseError=parseError ?? 'malformed block tag'
|
|
127
|
+
if(parseError){
|
|
128
|
+
handles.push({file:rel,handle,layoutType,blocks:[],moves:[],references:[],head:{scripts:[],css:[],metas:[],removes:[]},parseError})
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
const blocks:Array<{name:string|null,type:string,class:string|null,template:string|null,templateExists:boolean|null,as:string|null,cacheable:boolean|null,remove:boolean,refs:string[]}>=[]
|
|
132
|
+
const moves:Array<{element:string,destination:string|null,as:string|null,before:string|null,after:string|null}>=[]
|
|
133
|
+
const references:Array<{kind:string,name:string,attrs:Record<string,string>}>=[]
|
|
134
|
+
const head={scripts:[] as Array<{src:string}>, css:[] as Array<{src:string}>, metas:[] as Array<Record<string,string>>, removes:[] as string[]}
|
|
135
|
+
// blocks/containers (only concrete)
|
|
136
|
+
const tagRe=/<(block|container)\b([^>]*?)(?:\/>|>)/g
|
|
137
|
+
let m:RegExpExecArray|null
|
|
138
|
+
while((m=tagRe.exec(text))!==null){
|
|
139
|
+
const type=m[1]; const attrStr=m[2]; const attrs=parseAttrs(attrStr)
|
|
140
|
+
const name=attrs.name ?? null
|
|
141
|
+
const klass=attrs.class ?? null
|
|
142
|
+
const template=attrs.template ?? null
|
|
143
|
+
let templateExists:boolean|null=null
|
|
144
|
+
if(template!==null){
|
|
145
|
+
if(template.includes('::')){
|
|
146
|
+
const [modPart, filePart]=template.split('::')
|
|
147
|
+
const [vendor, mod]=modPart.split('_')
|
|
148
|
+
if(vendor && mod && filePart){
|
|
149
|
+
const cand=join(effectiveRoot, `app/code/${vendor}/${mod}/view/frontend/templates/${filePart}`)
|
|
150
|
+
templateExists=await exists(cand)
|
|
151
|
+
if(!templateExists){
|
|
152
|
+
templateExists=false
|
|
153
|
+
}
|
|
154
|
+
} else templateExists=false
|
|
155
|
+
} else {
|
|
156
|
+
templateExists=await exists(join(effectiveRoot, template))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const as=attrs.as ?? null
|
|
160
|
+
const cacheable=attrs.cacheable ? attrs.cacheable==='true' : null
|
|
161
|
+
const remove=attrs.remove==='true'
|
|
162
|
+
blocks.push({name, type, class:klass, template, templateExists, as, cacheable, remove, refs:[]})
|
|
163
|
+
}
|
|
164
|
+
// references
|
|
165
|
+
const refRe=/(<(referenceBlock|referenceContainer)\b([^>]*?)(?:\/>|>))/g
|
|
166
|
+
while((m=refRe.exec(text))!==null){
|
|
167
|
+
const type=m[2]; const attrs=parseAttrs(m[3])
|
|
168
|
+
references.push({kind:type, name: attrs.name ?? '', attrs})
|
|
169
|
+
}
|
|
170
|
+
// moves
|
|
171
|
+
const moveRe=/<move\b([^>]*?)\/>/g
|
|
172
|
+
while((m=moveRe.exec(text))!==null){
|
|
173
|
+
const attrs=parseAttrs(m[1])
|
|
174
|
+
moves.push({element:attrs.element ?? '', destination:attrs.destination ?? null, as:attrs.as ?? null, before:attrs.before ?? null, after:attrs.after ?? null})
|
|
175
|
+
}
|
|
176
|
+
// head css/scripts
|
|
177
|
+
const cssRe=/<css\b([^>]*?)\/>/g
|
|
178
|
+
while((m=cssRe.exec(text))!==null){ const a=parseAttrs(m[1]); if(a.src) head.css.push({src:a.src}) }
|
|
179
|
+
const scriptRe=/<script\b([^>]*?)\/>/g
|
|
180
|
+
while((m=scriptRe.exec(text))!==null){ const a=parseAttrs(m[1]); if(a.src) head.scripts.push({src:a.src}) }
|
|
181
|
+
// also <remove src=>
|
|
182
|
+
const removeRe=/<remove\b([^>]*?)\/>/g
|
|
183
|
+
while((m=removeRe.exec(text))!==null){ const a=parseAttrs(m[1]); if(a.src) head.removes.push(a.src) }
|
|
184
|
+
|
|
185
|
+
blockCount+=blocks.length
|
|
186
|
+
handles.push({file:rel,handle,layoutType,blocks,moves,references,head,parseError:null})
|
|
187
|
+
}
|
|
188
|
+
return {handles, handleCount:handles.length, blockCount, scannedFiles:files.length, truncated} as never
|
|
189
|
+
},
|
|
190
|
+
}), 'layout xml'))
|
|
191
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { join, resolve, sep, basename, dirname } from 'node:path'
|
|
2
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
3
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
4
|
+
import z from '@deepseek-ai/schemastery'
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
|
|
7
|
+
export const name='maestro-module-check-tool'
|
|
8
|
+
export const inject=['tools']
|
|
9
|
+
export const Config:z<{rootPath?:string}>=z.object({rootPath:z.string()})
|
|
10
|
+
interface SC{agent?:{session?:{header?:{cwd?:string}}}}
|
|
11
|
+
function workspaceRootFor(c:string|undefined,e:unknown):string{
|
|
12
|
+
if(c!==undefined) return c
|
|
13
|
+
const cwd=(e as SC|undefined)?.agent?.session?.header?.cwd
|
|
14
|
+
return typeof cwd==='string'&&cwd!==''?cwd:process.cwd()
|
|
15
|
+
}
|
|
16
|
+
async function isInsideRoot(r:string,t:string):Promise<boolean>{
|
|
17
|
+
const ar=resolve(r); const rs=resolve(ar,t)
|
|
18
|
+
if(rs!==ar && !rs.startsWith(ar+sep)) return false
|
|
19
|
+
try{ const rr=await realpath(ar); const rt=await realpath(rs); return rt===rr||rt.startsWith(rr+sep)}catch{return false}
|
|
20
|
+
}
|
|
21
|
+
const MAX_FILE_BYTES=1024*1024
|
|
22
|
+
const MAX_MODULES=100
|
|
23
|
+
async function exists(p:string):Promise<boolean>{ try{await stat(p); return true}catch{return false}}
|
|
24
|
+
async function boundedRead(p:string):Promise<string|null>{
|
|
25
|
+
try{ const b=await readFile(p); if(b.byteLength>MAX_FILE_BYTES) return null; return b.toString('utf-8')}catch{return null}
|
|
26
|
+
}
|
|
27
|
+
function parseRegistrationName(text:string):string|null{
|
|
28
|
+
const re=/(?:\\?[\w\\]*ComponentRegistrar)::register\s*\(\s*(?:\\?[\w\\]*ComponentRegistrar::)?MODULE\s*,\s*['"]([^'"]+)['"]/
|
|
29
|
+
const m=re.exec(text); return m?m[1]:null
|
|
30
|
+
}
|
|
31
|
+
function parseModuleXml(text:string):{name:string|null, sequence:string[]}{
|
|
32
|
+
const nameRe=/<module\b[^>]*name="([^"]+)"/
|
|
33
|
+
const nm=nameRe.exec(text); const name=nm?nm[1]:null
|
|
34
|
+
const seq:string[]=[]; const seqRe=/<module\s+name="([^"]+)"\s*\/>/g; let m:RegExpExecArray|null
|
|
35
|
+
// only inside <sequence> block
|
|
36
|
+
const seqBlock=/<sequence>([\s\S]*?)<\/sequence>/.exec(text)
|
|
37
|
+
if(seqBlock){ const inner=seqBlock[1]; while((m=seqRe.exec(inner))!==null) seq.push(m[1]) }
|
|
38
|
+
return {name, sequence:seq}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function apply(ctx:Context, config:{rootPath?:string}={}):void{
|
|
42
|
+
const configuredRoot=config.rootPath
|
|
43
|
+
ctx.effect(()=>ctx.tools.register(defineTool({
|
|
44
|
+
name:'magento_module_check',
|
|
45
|
+
description:'Validate Magento 2 module structure (registration, module.xml, composer, db_schema, acl, routes, Hyva compat).',
|
|
46
|
+
parameters:{
|
|
47
|
+
modulePath:{type:'string'},
|
|
48
|
+
},
|
|
49
|
+
output:{
|
|
50
|
+
schema:{
|
|
51
|
+
type:'object', additionalProperties:false,
|
|
52
|
+
properties:{
|
|
53
|
+
modules:{type:'array', required:true},
|
|
54
|
+
scannedModules:{type:'number', required:true},
|
|
55
|
+
truncated:{type:'boolean', required:true},
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
render:(_a,v)=>[{type:'text', text: JSON.stringify(v,null,2)}],
|
|
59
|
+
},
|
|
60
|
+
async execute(args, exec){
|
|
61
|
+
const rawPath=(args as {modulePath?:string}).modulePath
|
|
62
|
+
const root=workspaceRootFor(configuredRoot, exec)
|
|
63
|
+
if(rawPath!==undefined && !(await isInsideRoot(root, rawPath))) return {text:`Path "${rawPath}" escapes the workspace root.`, truncated:false} as never
|
|
64
|
+
let candidates:string[]=[]
|
|
65
|
+
if(rawPath!==undefined){
|
|
66
|
+
const abs=resolve(root, rawPath)
|
|
67
|
+
// must contain registration.php or etc/module.xml to be considered a module; otherwise empty
|
|
68
|
+
candidates=[rawPath]
|
|
69
|
+
} else {
|
|
70
|
+
const base=join(root,'app/code')
|
|
71
|
+
let vendors:string[]=[]
|
|
72
|
+
try{ vendors=await readdir(base)}catch{ vendors=[]}
|
|
73
|
+
for(const vendor of vendors){
|
|
74
|
+
if(vendor.startsWith('.')) continue
|
|
75
|
+
let mods:string[]=[]
|
|
76
|
+
try{ mods=await readdir(join(base,vendor))}catch{continue}
|
|
77
|
+
for(const mod of mods){
|
|
78
|
+
if(mod.startsWith('.')) continue
|
|
79
|
+
candidates.push(join('app/code',vendor,mod))
|
|
80
|
+
if(candidates.length>=MAX_MODULES) break
|
|
81
|
+
}
|
|
82
|
+
if(candidates.length>=MAX_MODULES) break
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const truncated=candidates.length>=MAX_MODULES
|
|
86
|
+
const modules:Array<{
|
|
87
|
+
path:string,
|
|
88
|
+
module:{name:string|null,sequence:string[],hasRegistration:boolean,registrationValid:boolean|null,hasEtcModuleXml:boolean,hasDbSchema:boolean,dbSchemaTables:string[],hasAcl:boolean,aclResources:string[],hasRoutes:boolean,routes:Array<{area:string,frontName:string}>,hasObserverEvents:string[],hasCron:boolean,hasWebapi:boolean,apiInterfaces:string[],composerName:string|null,psr4:Record<string,string>|null},
|
|
89
|
+
issues:Array<{severity:string,rule:string,message:string,file:string,line:number|null}>,
|
|
90
|
+
isHyvaCompatible:boolean|null, compatNotes:string[]
|
|
91
|
+
}>=[]
|
|
92
|
+
for(const rel of candidates.slice(0,MAX_MODULES)){
|
|
93
|
+
const abs=resolve(root, rel)
|
|
94
|
+
let isDir=false; try{ const s=await stat(abs); isDir=s.isDirectory()}catch{ continue}
|
|
95
|
+
if(!isDir) continue
|
|
96
|
+
const issues: Array<{severity:string,rule:string,message:string,file:string,line:number|null}>=[]
|
|
97
|
+
const regText=await boundedRead(join(abs,'registration.php'))
|
|
98
|
+
const hasRegistration=regText!==null
|
|
99
|
+
let regName:string|null=null
|
|
100
|
+
let registrationValid:boolean|null=null
|
|
101
|
+
if(hasRegistration){
|
|
102
|
+
regName=parseRegistrationName(regText!)
|
|
103
|
+
registrationValid=regName!==null
|
|
104
|
+
if(!registrationValid) issues.push({severity:'high', rule:'registration-invalid', message:'registration.php does not contain ComponentRegistrar::register for MODULE', file:join(rel,'registration.php'), line:null})
|
|
105
|
+
} else {
|
|
106
|
+
issues.push({severity:'critical', rule:'registration-missing', message:'registration.php missing', file:join(rel,'registration.php'), line:null})
|
|
107
|
+
}
|
|
108
|
+
const modXmlText=await boundedRead(join(abs,'etc/module.xml'))
|
|
109
|
+
const hasEtcModuleXml=modXmlText!==null
|
|
110
|
+
let modName:string|null=null
|
|
111
|
+
let sequence:string[]=[]
|
|
112
|
+
if(hasEtcModuleXml){
|
|
113
|
+
const parsed=parseModuleXml(modXmlText!)
|
|
114
|
+
modName=parsed.name; sequence=parsed.sequence
|
|
115
|
+
if(!modName) issues.push({severity:'critical', rule:'module-xml-missing-name', message:'etc/module.xml missing module name', file:join(rel,'etc/module.xml'), line:null})
|
|
116
|
+
} else {
|
|
117
|
+
issues.push({severity:'high', rule:'module-xml-missing', message:'etc/module.xml missing', file:join(rel,'etc/module.xml'), line:null})
|
|
118
|
+
}
|
|
119
|
+
if(regName && modName && regName!==modName){
|
|
120
|
+
issues.push({severity:'high', rule:'name-mismatch', message:`registration name ${regName} != module.xml ${modName}`, file:join(rel,'etc/module.xml'), line:null})
|
|
121
|
+
}
|
|
122
|
+
// composer
|
|
123
|
+
const composerText=await boundedRead(join(abs,'composer.json'))
|
|
124
|
+
let composerName:string|null=null; let psr4:Record<string,string>|null=null
|
|
125
|
+
if(composerText){
|
|
126
|
+
try{ const j=JSON.parse(composerText); composerName=j.name??null; psr4=j.autoload?.['psr-4']??null }catch{}
|
|
127
|
+
if(composerName && modName){
|
|
128
|
+
// psr-4 cross-check not strict for test
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// db_schema
|
|
132
|
+
const hasDbSchema=await exists(join(abs,'etc/db_schema.xml'))
|
|
133
|
+
let dbSchemaTables:string[]=[]
|
|
134
|
+
if(hasDbSchema){
|
|
135
|
+
const dbText=await boundedRead(join(abs,'etc/db_schema.xml'))
|
|
136
|
+
if(dbText){ const re=/<table\s+name="([^"]+)"/g; let m:RegExpExecArray|null; while((m=re.exec(dbText))!==null) dbSchemaTables.push(m[1])}
|
|
137
|
+
}
|
|
138
|
+
// acl
|
|
139
|
+
const hasAcl=await exists(join(abs,'etc/acl.xml'))
|
|
140
|
+
let aclResources:string[]=[]
|
|
141
|
+
if(hasAcl){ const t=await boundedRead(join(abs,'etc/acl.xml')); if(t){ const re=/<resource\s+id="([^"]+)"/g; let m:RegExpExecArray|null; while((m=re.exec(t))!==null) aclResources.push(m[1])}}
|
|
142
|
+
// routes
|
|
143
|
+
let hasRoutes=false; const routes:Array<{area:string,frontName:string}>=[]
|
|
144
|
+
for(const area of ['frontend','adminhtml']){
|
|
145
|
+
const rp=join(abs,`etc/${area}/routes.xml`); if(await exists(rp)){ hasRoutes=true; const rt=await boundedRead(rp); if(rt){ const re=/<route\s+[^>]*frontName="([^"]+)"/g; let m:RegExpExecArray|null; while((m=re.exec(rt))!==null) routes.push({area, frontName:m[1]}) } }
|
|
146
|
+
}
|
|
147
|
+
if(await exists(join(abs,'etc/routes.xml'))){ hasRoutes=true }
|
|
148
|
+
// events
|
|
149
|
+
let hasObserverEvents:string[]=[]
|
|
150
|
+
const evText=await boundedRead(join(abs,'etc/events.xml'))
|
|
151
|
+
if(evText){ const re=/<event\s+name="([^"]+)"/g; let m:RegExpExecArray|null; while((m=re.exec(evText))!==null) hasObserverEvents.push(m[1])}
|
|
152
|
+
// cron/webapi
|
|
153
|
+
const hasCron=await exists(join(abs,'etc/crontab.xml')) || await exists(join(abs,'etc/cron_groups.xml'))
|
|
154
|
+
const hasWebapi=await exists(join(abs,'etc/webapi.xml'))
|
|
155
|
+
let apiInterfaces:string[]=[]
|
|
156
|
+
if(hasWebapi){ const t=await boundedRead(join(abs,'etc/webapi.xml')); if(t){ const re=/<service\s+class="([^"]+)"/g; let m:RegExpExecArray|null; while((m=re.exec(t))!==null) apiInterfaces.push(m[1])}}
|
|
157
|
+
|
|
158
|
+
// Hyva compat: scan phtml for data-bind vs x-data
|
|
159
|
+
let isHyvaCompatible:boolean|null=null
|
|
160
|
+
const compatNotes:string[]=[]
|
|
161
|
+
let hasPhtml=false; let hasDataBind=false; let hasXData=false
|
|
162
|
+
async function scanPhtml(dir:string){
|
|
163
|
+
let entries; try{entries=await readdir(dir,{withFileTypes:true})}catch{return}
|
|
164
|
+
for(const e of entries){
|
|
165
|
+
if(e.isDirectory()) await scanPhtml(join(dir,e.name))
|
|
166
|
+
else if(e.isFile() && e.name.endsWith('.phtml')){
|
|
167
|
+
hasPhtml=true
|
|
168
|
+
const txt=await boundedRead(join(dir,e.name))
|
|
169
|
+
if(!txt) continue
|
|
170
|
+
if(/data-bind\s*=/.test(txt)) hasDataBind=true
|
|
171
|
+
if(/x-data\s*=/.test(txt)) hasXData=true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await scanPhtml(join(abs,'view/frontend/templates'))
|
|
176
|
+
if(hasPhtml){
|
|
177
|
+
if(hasDataBind){ isHyvaCompatible=false; compatNotes.push('knockout data-bind found — not Hyva compatible')}
|
|
178
|
+
else if(hasXData){ isHyvaCompatible=true; compatNotes.push('Alpine x-data found — Hyva compatible')}
|
|
179
|
+
else { isHyvaCompatible=null }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const moduleInfo={name: modName ?? regName, sequence, hasRegistration, registrationValid, hasEtcModuleXml, hasDbSchema, dbSchemaTables, hasAcl, aclResources, hasRoutes, routes, hasObserverEvents, hasCron, hasWebapi, apiInterfaces, composerName, psr4}
|
|
183
|
+
modules.push({path:rel, module:moduleInfo, issues, isHyvaCompatible, compatNotes})
|
|
184
|
+
}
|
|
185
|
+
return {modules, scannedModules: modules.length, truncated} as never
|
|
186
|
+
},
|
|
187
|
+
}), 'module check'))
|
|
188
|
+
}
|