🌳
pt0/deployF/entrypointsF/inferEpTypeFromFileAI.mts
1// Static analysis to infer ep-type from deploy script file contents
3import fs from 'fs'
4import { execSync } from 'child_process'
5import ts from 'typescript'
8import type { absFileDirPath } from '../../ptDirF.mts'
12// Resolve script path to absolute (handles both absolute and relative paths)
13export const resolveScriptPath = (scriptPath: string) =>
14 scriptPath.startsWith('/') ? scriptPath : pathDownJoin(ptDir, scriptPath)
16// Top-level package scopes (each is a monorepo package root).
17// Discovery is glob-based (mkEpGlob(scope)), so all package dirs are scanned for ep*.{mjs,mts}.
18const nonPackageDirs = ['node_modules', '.git', 'tmp', '.opencode', '.next', 'dist']
19export const getRegistryScopes = () =>
20 fs.readdirSync(ptDir, { withFileTypes: true })
21 .filter(d => d.isDirectory() && !nonPackageDirs.includes(d.name))
22 .map(d => d.name)
24export const readFileContent = (scriptPath: string) => {
25 const absPath = resolveScriptPath(scriptPath)
26 if (!fs.existsSync(absPath)) return null
27 return fs.readFileSync(absPath, 'utf8')
30export const inferEpTypeFromFile = (scriptPath: string) => {
31 const contents = readFileContent(scriptPath)
32 if (!contents) return null
33 const sourceFile = tsSrcFile({ptPath: scriptPath as absFileDirPath, contents})
34 let result: string | null = null
35 const visit = (node: ts.Node) => {
36 if (result) return
37 if (ts.isCallExpression(node)) {
38 const fnText = node.expression.getText(sourceFile)
39 const m = fnText.match(/^ept([A-Z][a-zA-Z0-9]*)$/)
40 if (m) result = 'ept' + m[1]
41 }
42 ts.forEachChild(node, visit)
43 }
44 visit(sourceFile)
45 return result
48export const inferDbQsNameFromFile = (scriptPath: string): string | null => {
49 const contents = readFileContent(scriptPath)
50 if (!contents) return null
51 const sourceFile = tsSrcFile({ptPath: scriptPath as absFileDirPath, contents})
52 let cnpgResult: string | null = null
53 let vanillaResult: string | null = null
54 const visit = (node: ts.Node) => {
55 if (cnpgResult) return
56 if (ts.isCallExpression(node)) {
57 const fnText = node.expression.getText(sourceFile)
58 if (fnText === 'eptCnPgDb' && node.arguments[0] && ts.isObjectLiteralExpression(node.arguments[0])) {
59 for (const prop of node.arguments[0].properties) {
60 if (ts.isPropertyAssignment(prop) && prop.name.getText() === 'name' && ts.isStringLiteral(prop.initializer)) {
61 cnpgResult = prop.initializer.text + '_qs'
62 }
63 }
64 }
65 }
66 if (!vanillaResult && ts.isPropertyAssignment(node) &&
67 node.name.getText() === 'dbQsName' && ts.isStringLiteral(node.initializer)) {
68 vanillaResult = node.initializer.text
69 }
70 if (!cnpgResult) ts.forEachChild(node, visit)
71 }
72 visit(sourceFile)
73 return cnpgResult || vanillaResult
76const epTypeDefCache = new Map()
77export const getEpTypeDefPath = (epType: string | null, searchRepoWide = false) => {
78 if (!epType) return null
79 const cacheKey = searchRepoWide ? `${epType}|repo` : epType
80 if (epTypeDefCache.has(cacheKey)) return epTypeDefCache.get(cacheKey)
81 try {
82 const scopeGlob = searchRepoWide ? '' : ' pt0/'
83 const result = execSync(`rg -l "export const ${epType}" --glob "${extGlob(epFileExtA)}"${scopeGlob}`, {encoding: 'utf8', cwd: ptDir, maxBuffer: 1024 * 1024})
84 const defPath = result.trim().split('\n')[0] || null
85 epTypeDefCache.set(cacheKey, defPath)
86 return defPath
87 } catch (e: any) {
88 if (e.status === 1) { epTypeDefCache.set(cacheKey, null); return null }
89 throw e
90 }
93export const inferK8sResFromEpType = (epType: string | null): {kind: string, apiVersion: string} | null => {
94 if (!epType) return null
95 const defPath = getEpTypeDefPath(epType, true)
96 if (!defPath) return null
97 const contents = readFileContent(defPath)
98 if (!contents) return null
99 const targetName = `${epType}K8sRes`
100 const sourceFile = tsSrcFile({ptPath: defPath as absFileDirPath, contents})
101 let result: {kind: string, apiVersion: string} | null = null
102 const visit = (node: ts.Node) => {
103 if (result) return
104 if (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
105 for (const decl of node.declarationList.declarations) {
106 if (result) break
107 const init = decl.initializer
108 if (ts.isIdentifier(decl.name) && decl.name.text === targetName && init && ts.isObjectLiteralExpression(init)) {
109 const props = new Map<string, string>()
110 for (const p of init.properties) {
111 const val = (p as ts.PropertyAssignment).initializer
112 if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && val && ts.isStringLiteral(val)) props.set(p.name.text, val.text)
113 }
114 const kind = props.get('kind'), apiVersion = props.get('apiVersion')
115 if (kind && apiVersion) result = {kind, apiVersion}
116 }
117 }
118 }
119 ts.forEachChild(node, visit)
120 }
121 visit(sourceFile)
122 return result