UNPKG

okai

Version:
927 lines (923 loc) 37.1 kB
import fs from "fs"; import path from "path"; import blessed from 'blessed'; import { projectInfo } from './info.js'; import { leftPart, replaceMyApp, trimStart, toPascalCase, plural } from "./utils.js"; import { generateCsAstFromTsd, toAst, astForProject } from "./ts-ast.js"; import { CSharpApiGenerator } from "./cs-apis.js"; import { CSharpMigrationGenerator } from "./cs-migrations.js"; import { convertDefinitionsToAst, getFileContent } from "./client.js"; import { toTsd } from "./tsd-gen.js"; import { UiMjsGroupGenerator, UiMjsIndexGenerator } from "./ui-mjs.js"; function normalizeSwitches(cmd) { return cmd.replace(/^-+/, '/'); } function parseArgs(...args) { const ret = { type: "help", baseUrl: process.env.OKAI_URL || "https://okai.servicestack.com", script: path.basename(process.argv[1]) || "okai", }; for (let i = 0; i < args.length; i++) { const arg = args[i]; const opt = normalizeSwitches(arg); if (opt.startsWith('/')) { switch (opt) { case "/?": case "/h": case "/help": ret.type = "help"; break; case "/v": case "/verbose": ret.verbose = true; break; case "/D": ret.verbose = ret.debug = true; break; case "/w": case "/watch": ret.watch = true; break; case "/m": case "/models": ret.models = args[++i]; break; case "/l": case "/license": ret.license = args[++i]; break; case "/url": ret.baseUrl = args[++i]; break; case "/cached": ret.cached = true; break; case "/system": ret.system = args[++i]; break; default: ret.unknown = ret.unknown || []; ret.unknown.push(arg); break; } } else if (ret.type === "help" && ["help", "info", "init", "ls", "add", "rm", "update", "accept", "convert"].includes(arg)) { if (arg == "help") ret.type = "help"; else if (arg == "info") ret.type = "info"; else if (arg == "init") { ret.type = "init"; if (args[i + 1]) { ret.init = args[++i]; } } else if (arg == "update") { ret.type = "update"; ret.tsdFile = args[++i]; if (ret.tsdFile && !ret.tsdFile.endsWith('.d.ts')) { ret.tsdFile += '.d.ts'; } } else if (arg == "rm") { ret.type = "remove"; ret.tsdFile = args[++i]; if (ret.tsdFile && !ret.tsdFile.endsWith('.d.ts')) { ret.tsdFile += '.d.ts'; } } else if (arg == "ls") { ret.type = "list"; ret.list = args[++i]; } else if (arg == "add") { ret.type = "add"; ret.add = args[++i]; } else if (arg == "accept") { ret.type = "accept"; ret.accept = args[++i]; } else if (arg == "convert") { ret.type = "convert"; ret.convert = args[++i]; } } else if (arg == "chat") { ret.type = "chat"; } else if (arg.endsWith('.d.ts')) { if (ret.type == "help") ret.type = "update"; ret.tsdFile = arg; } else { if (ret.type == "help") ret.type = "prompt"; if (ret.prompt) { ret.prompt += ' '; } ret.prompt = (ret.prompt ?? '') + arg; } } if (ret.type === "prompt" || ret.type === "chat") { if (!ret.cached && process.env.OKAI_CACHED) { ret.cached = true; } if (!ret.models && process.env.OKAI_MODELS) { ret.models = process.env.OKAI_MODELS; } if (ret.models) { if (!ret.license && process.env.SERVICESTACK_CERTIFICATE) { ret.license = process.env.SERVICESTACK_CERTIFICATE; } if (!ret.license && process.env.SERVICESTACK_LICENSE) { ret.license = process.env.SERVICESTACK_LICENSE; } } } return ret; } export async function cli(cmdArgs) { const command = parseArgs(...cmdArgs); const script = command.script; if (command.verbose) { console.log(`Command: ${JSON.stringify(command, undefined, 2)}`); } if (command.debug) { console.log(`Environment:`); Object.keys(process.env).forEach(k => { console.log(`${k}: ${process.env[k]}`); }); process.exit(0); return; } if (command.type === "init" && !command.init) { let info = projectInfo(process.cwd()) ?? { projectName: "<MyApp>", slnDir: "/path/to/.sln/folder", hostDir: "/path/to/MyApp", migrationsDir: "/path/to/MyApp/Migrations", serviceModelDir: "/path/to/MyApp.ServiceModel", serviceInterfaceDir: "/path/to/MyApp.ServiceInterfaces", uiMjsDir: "/path/to/MyApp/wwwroot/admin/sections", }; fs.writeFileSync('okai.json', JSON.stringify(info, undefined, 2)); console.log(`Saved: okai.json`); process.exit(0); return; } if (command.type === "help" || command.unknown?.length) { const exitCode = command.unknown?.length ? 1 : 0; if (command.unknown?.length) { console.log(`Unknown Command: ${command.script} ${command.unknown.join(' ')}\n`); } const bin = script.padStart(7, ' '); console.log(`Usage: ${bin} <prompt> Generate new TypeScript Data Models, C# APIs and Migrations from prompt -m, -models <model,> Specify up to 5 LLM models to generate .d.ts Data Models -l, -license <LC-xxx> Specify valid license certificate or key to use premium models ${bin} <models>.d.ts Regenerate C# *.cs files for Data Models defined in the TypeScript .d.ts file -w, -watch Watch for changes to <models>.d.ts and regenerate *.cs on save ${bin} rm <models>.d.ts Remove <models>.d.ts and its generated *.cs files ${bin} ls models Display list of available premium LLM models ${bin} init Initialize okai.json with project info to override default paths ${bin} init <model> Create an empty <model>.d.ts file for the specified model ${bin} convert <schema.json> Convert .NET RDBMS TableDefinitions to TypeScript Data Models ${bin} info Display current project info ${bin} chat <prompt> Submit a new OpenAI chat request with the specified prompt -system <prompt> Specify a system prompt Options: -v, -verbose Display verbose logging --ignore-ssl-errors Ignore SSL Errors`); process.exit(exitCode); return; } if (command.type === "list") { if (command.list == "models") { const url = new URL('/models/list', command.baseUrl); if (command.verbose) console.log(`GET: ${url}`); const res = await fetch(url); if (!res.ok) { console.log(`Failed to fetch models: ${res.statusText}`); process.exit(1); } const models = await res.text(); console.log(models); process.exit(0); } } if (command.type === "chat") { try { const url = new URL('/chat', command.baseUrl); const formData = new FormData(); formData.append('prompt', command.prompt); if (command.system) { formData.append('system', command.system); } if (command.models) { formData.append('model', command.models); if (command.license) { formData.append('license', command.license); } } if (command.verbose) console.log(`POST: ${url}`); const res = await fetch(url, { method: 'POST', body: formData, }); if (!res.ok) { console.log(`Failed to chat: ${res.statusText}`); process.exit(1); } const response = await res.json(); if (command.verbose) console.log(JSON.stringify(response, undefined, 2)); const content = response.choices[response.choices.length - 1]?.message?.content; console.log(content); } catch (err) { console.error(err); } process.exit(0); } // Requires running in context of ServiceStack App const info = command.info = projectInfo(process.cwd()); if (!info) { if (!info) { console.log(`No .sln file found`); console.log(`okai needs to be run within the context of a ServiceStack App containing a ServiceModel project`); console.log(`To use with an external or custom project, create an okai.json config file with:`); console.log(`$ ${script} init`); process.exit(1); } } if (command.type === "info") { try { console.log(JSON.stringify(command.info, undefined, 2)); } catch (err) { console.error(err.message ?? `${err}`); } process.exit(0); return; } if (command.ignoreSsl) { process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = "0"; } if (command.type == "init" && command.info) { const toApiFile = path.join(info.serviceModelDir, 'api.d.ts'); fs.copyFileSync(path.join(import.meta.dirname, 'api.d.ts'), toApiFile); console.log(`Saved: ${toApiFile}`); let model = toPascalCase(leftPart(command.init, '.')); let groupName = plural(model); if (model == groupName) { if (model.endsWith('s')) { model = model.substring(0, model.length - 1); } } let tsd = ` @tag("${groupName}") export class ${model} { id:number name:string } `; const tsdAst = toAst(tsd); if (tsdAst.references.length == 0) { tsdAst.references.push({ path: './api.d.ts' }); } let uiFileName = info.uiMjsDir ? `${groupName}.mjs` : null; const tsdContent = createTsdFile(info, { prompt: `New ${model}`, apiFileName: `${groupName}.cs`, tsdAst, uiFileName, }); command.tsdFile = groupName + '.d.ts'; fs.writeFileSync(path.join(info.serviceModelDir, command.tsdFile), tsdContent, { encoding: 'utf-8' }); } if (command.type === "add") { if (command.add == "types" || command.add?.startsWith("api")) { const apiFile = path.join(import.meta.dirname, 'api.d.ts'); if (!fs.existsSync(apiFile)) { console.log(`Could not find: ${apiFile}`); process.exit(1); } const toFile = path.join(info.serviceModelDir, 'api.d.ts'); fs.copyFileSync(apiFile, toFile); console.log(`Saved: ${toFile}`); process.exit(0); } else { console.log(`Unknown add command: ${command.add}`); console.log(`Usage: add types`); process.exit(1); } } function assertTsdPath(tsdFile) { const tryPaths = [ path.join(process.cwd(), tsdFile), ]; if (info?.serviceModelDir) { tryPaths.push(path.join(info.serviceModelDir, tsdFile)); } const tsdPath = tryPaths.find(fs.existsSync); if (!tsdPath) { console.log(`Could not find: ${command.tsdFile}, tried:\n${tryPaths.join('\n')}`); process.exit(1); } return tsdPath; } function resolveFile(filePath) { return filePath.startsWith('~/') ? path.join(info.slnDir, trimStart(filePath, '~/')) : path.join(process.cwd(), filePath); } if (command.type === "update" || (command.type == "init" && command.info)) { let tsdPath = assertTsdPath(command.tsdFile); if (command.verbose) console.log(`Updating: ${tsdPath}...`); let tsdContent = fs.readFileSync(tsdPath, 'utf-8'); //const header = parseTsdHeader(tsdContent) const ast = toAst(tsdContent); const header = ast.config; if (command.verbose) console.log(JSON.stringify(header, undefined, 2)); function regenerate(header, tsdContent, logPrefix = '') { if (!header) { console.log(`No header found in ${tsdPath}`); process.exit(1); } const result = generateCsAstFromTsd(tsdContent); tsdContent = result.tsd; const genApis = new CSharpApiGenerator(); const csApiFiles = genApis.generate(result.csAst); const apiContent = replaceMyApp(csApiFiles[Object.keys(csApiFiles)[0]], info.projectName); const apiPath = resolveFile(header.api); console.log(`${logPrefix}${apiPath}`); fs.writeFileSync(apiPath, apiContent, { encoding: 'utf-8' }); if (header?.migration) { const migrationCls = leftPart(path.basename(header.migration), '.'); const getMigrations = new CSharpMigrationGenerator(); const csMigrationFiles = getMigrations.generate(result.csAst); const migrationContent = replaceMyApp(csMigrationFiles[Object.keys(csMigrationFiles)[0]].replaceAll('Migration1000', migrationCls), info.projectName); const migrationPath = resolveFile(header.migration); console.log(`${logPrefix}${migrationPath}`); fs.writeFileSync(migrationPath, migrationContent, { encoding: 'utf-8' }); } if (header?.uiMjs) { const uiMjsGroupPath = resolveFile(header.uiMjs); const uiMjsGroupGen = new UiMjsGroupGenerator(); const uiMjsGroup = uiMjsGroupGen.generate(result.csAst, result.groupName); console.log(`${logPrefix}${uiMjsGroupPath}`); fs.writeFileSync(uiMjsGroupPath, uiMjsGroup, { encoding: 'utf-8' }); const uiMjsDir = path.dirname(uiMjsGroupPath); const uiMjsIndexGen = new UiMjsIndexGenerator(); const uiMjsIndex = uiMjsIndexGen.generate(fs.readdirSync(uiMjsDir)); const uiMjsIndexPath = path.join(uiMjsDir, 'index.mjs'); console.log(`${logPrefix}${uiMjsIndexPath}`); fs.writeFileSync(uiMjsIndexPath, uiMjsIndex, { encoding: 'utf-8' }); } console.log(`${logPrefix}${tsdPath}`); result.tsdAst.config = header; const newTsdContent = toTsd(result.tsdAst); fs.writeFileSync(tsdPath, newTsdContent, { encoding: 'utf-8' }); return newTsdContent; } if (info.serviceModelDir) { const relativeServiceModelDir = trimStart(info.serviceModelDir.substring(info.slnDir.length), '~/'); const apiTypesPath = path.join(info.slnDir, relativeServiceModelDir, `api.d.ts`); const apiFile = path.join(import.meta.dirname, 'api.d.ts'); fs.writeFileSync(apiTypesPath, fs.readFileSync(apiFile, 'utf-8')); } if (command.watch) { let lastTsdContent = tsdContent; console.log(`watching ${tsdPath} ...`); let i = 0; fs.watchFile(tsdPath, { interval: 100 }, (curr, prev) => { let tsdContent = fs.readFileSync(tsdPath, 'utf-8'); if (tsdContent == lastTsdContent) { if (command.verbose) console.log(`No change detected`); return; } console.log(`\n${++i}. regenerated files at ${leftPart(new Date().toTimeString(), ' ')}:`); lastTsdContent = regenerate(header, tsdContent); }); return; } else { regenerate(header, tsdContent, 'Saved: '); if (command.type == "init") { console.log(`\nRun 'npm run migrate' to apply the new migration and create the new tables or:`); console.log(`$ dotnet run --AppTasks=migrate`); } else { console.log(`\nLast migration can be rerun with 'npm run rerun:last' or:`); console.log(`$ dotnet run --AppTasks=migrate.rerun:last`); } process.exit(0); } } if (command.type == "convert") { const dbJson = fs.readFileSync(command.convert, 'utf-8'); const tableDefs = JSON.parse(dbJson); const tsdAst = convertDefinitionsToAst(tableDefs); const groupName = path.basename(command.convert).replace('.json', ''); const apiFileName = `${groupName}.cs`; let uiFileName = info.uiMjsDir ? `${groupName}.mjs` : null; if (tsdAst.references.length == 0) { tsdAst.references.push({ path: './api.d.ts' }); } const tsdContent = createTsdFile(info, { prompt: path.basename(command.convert), apiFileName, tsdAst: tsdAst, uiFileName, }); console.log(tsdContent); process.exit(0); } if (command.type === "remove") { let tsdPath = assertTsdPath(command.tsdFile); if (command.verbose) console.log(`Removing: ${tsdPath}...`); const tsdContent = fs.readFileSync(tsdPath, 'utf-8'); const ast = toAst(tsdContent); const header = ast.config; if (command.verbose) console.log(JSON.stringify(header, undefined, 2)); if (header?.migration) { const migrationPath = resolveFile(header.migration); if (fs.existsSync(migrationPath)) { fs.unlinkSync(migrationPath); console.log(`Removed: ${migrationPath}`); } else { console.log(`Migration .cs file not found: ${migrationPath}`); } } if (header?.api) { const apiPath = resolveFile(header.api); if (fs.existsSync(apiPath)) { fs.unlinkSync(apiPath); console.log(`Removed: ${apiPath}`); } else { console.log(`APIs .cs file not found: ${apiPath}`); } } if (header?.uiMjs) { const uiMjsGroupPath = resolveFile(header.uiMjs); if (fs.existsSync(uiMjsGroupPath)) { fs.unlinkSync(uiMjsGroupPath); console.log(`Removed: ${uiMjsGroupPath}`); const uiMjsDir = path.dirname(uiMjsGroupPath); const uiMjsIndexGen = new UiMjsIndexGenerator(); const uiMjsIndex = uiMjsIndexGen.generate(fs.readdirSync(uiMjsDir)); const uiMjsIndexPath = path.join(uiMjsDir, 'index.mjs'); fs.writeFileSync(uiMjsIndexPath, uiMjsIndex, { encoding: 'utf-8' }); } else { console.log(`UI .mjs file not found: ${uiMjsGroupPath}`); } } fs.unlinkSync(tsdPath); console.log(`Removed: ${tsdPath}`); const serviceModelDir = path.dirname(tsdPath); const tsds = fs.readdirSync(serviceModelDir).filter(x => x.endsWith('.d.ts')); if (tsds.length == 1 && tsds[0] == 'api.d.ts') { const typesApiPath = path.join(serviceModelDir, 'api.d.ts'); fs.unlinkSync(typesApiPath); console.log(`Removed: ${typesApiPath}`); } process.exit(0); } if (command.type == "accept") { await acceptGist(command, command.accept); process.exit(0); } if (command.type === "prompt") { try { if (!info.serviceModelDir) { console.log("Could not find ServiceModel directory, ensure okai is run within the context of a ServiceStack App"); process.exit(1); } console.log(`Generating new APIs and Tables for: ${command.prompt}...`); const gist = await fetchGistFiles(command); // const projectGist = convertToProjectGist(info, gist) const ctx = await createGistPreview(command.prompt, gist); ctx.screen.key('a', () => chooseFile(ctx, info, gist, command)); ctx.screen.render(); } catch (err) { console.error(err); } } else { console.log(`Unknown command: ${command.type}`); process.exit(1); } } async function acceptGist(command, id) { try { const url = new URL(`/gist/${id}/accept`, command.baseUrl); if (command.verbose) console.log(`POST: ${url}`); const r = await fetch(url, { method: 'POST', }); const res = await r.text(); if (command.verbose) console.log(`Accepted: ${res}`); } catch (err) { if (command.verbose) console.error(err); } } async function fetchGistFiles(command) { const url = new URL('/models/gist', command.baseUrl); const formData = new FormData(); if (command.cached) { formData.append('cached', `1`); } formData.append('prompt', command.prompt); if (command.models) { formData.append('models', command.models); if (command.license) { formData.append('license', command.license); } } if (command.verbose) console.log(`POST: ${url}`); const res = await fetch(url, { method: 'POST', body: formData, }); if (!res.ok) { try { const errorResponse = await res.json(); console.error(errorResponse?.responseStatus?.message ?? errorResponse.message ?? errorResponse); } catch (err) { console.log(`Failed to generate data models: ${res.statusText}`); } process.exit(1); } const gist = await res.json(); const files = gist.files; if (!files || Object.keys(files).length === 0) { throw new Error(`Request didn't generate any files`); } return gist; } // When writing to disk, replace MyApp with the project name function convertToProjectGist(info, gist) { const to = Object.assign({}, gist, { files: {} }); const cwd = process.cwd(); for (const [displayName, file] of Object.entries(gist.files)) { const writeFileName = file.filename; const type = `text/csharp`; const content = replaceMyApp(file.content, info.projectName); const size = content.length; if (writeFileName.startsWith('MyApp.ServiceModel/') && info.serviceModelDir) { const fullPath = path.join(info.serviceModelDir, writeFileName.substring('MyApp.ServiceModel/'.length)); const relativePath = path.relative(cwd, fullPath); to.files[relativePath] = { filename: path.basename(fullPath), content, type, size, raw_url: fullPath, }; } else if (writeFileName.startsWith('MyApp/Migrations/') && info.migrationsDir) { const fullPath = path.join(info.migrationsDir, writeFileName.substring('MyApp.Migrations/'.length)); const relativePath = path.relative(cwd, fullPath); to.files[relativePath] = Object.assign({}, file, { filename: path.basename(fullPath), content, type, size, raw_url: fullPath, }); } else { const fullPath = path.join(info.slnDir, writeFileName); const relativePath = path.relative(cwd, fullPath); const toFilename = replaceMyApp(relativePath, info.projectName); to.files[relativePath] = Object.assign({}, file, { filename: path.basename(toFilename), content, type, size, raw_url: fullPath, }); } } return to; } async function createGistPreview(title, gist) { // Initialize screen const screen = blessed.screen({ smartCSR: true, title, }); // Create title bar const titleBar = blessed.box({ top: 0, left: 0, width: '100%', height: 1, content: ` ${title}`, style: { fg: 'black', bg: 'white' } }); // Create file list const fileList = blessed.list({ left: 0, top: 1, width: '35%', height: '95%-1', border: { type: 'line' }, style: { selected: { bg: 'blue', fg: 'black' } }, keys: true, vi: true, mouse: true, items: Object.keys(gist.files) }); const firstFilename = Object.keys(gist.files)[0]; // Create preview pane const preview = blessed.box({ right: 0, top: 1, width: '65%', height: '95%-1', border: { type: 'line' }, content: getFileContent(gist.files[firstFilename]), scrollable: true, alwaysScroll: true, keys: true, vi: true, mouse: true }); // Create status bar const statusBar = blessed.box({ bottom: 0, left: 0, width: '100%', height: 1, content: 'Press (a) accept (q) quit', style: { fg: 'black', bg: 'blue' } }); // Add components to screen screen.append(titleBar); screen.append(fileList); screen.append(preview); screen.append(statusBar); const result = { selectedFile: firstFilename }; // Handle file selection fileList.on('select item', (item) => { const filename = item.content; result.selectedFile = filename; const file = gist.files[filename]; if (file) { const content = getFileContent(file); preview.setContent(content); screen.render(); } }); fileList.on('keypress', (ch, key) => { const boxHeight = preview.height; const currentScroll = preview.getScroll(); const contentHeight = preview.getLines().length; // Calculate scroll amount (half the box height) const scrollAmount = Math.floor(boxHeight / 2); // Page Up handler if (key.name === 'pageup') { // Calculate new scroll position, ensuring it doesn't go below 0 const newScrollPosition = Math.max(0, currentScroll - scrollAmount); preview.setScroll(newScrollPosition); // titleBar.setContent(`UP ${newScrollPosition} = ${currentScroll} - ${scrollAmount}`) screen.render(); } // Page Down handler if (key.name === 'pagedown') { // Calculate max scroll position to prevent scrolling beyond content const maxScroll = Math.max(0, contentHeight - boxHeight + 2); // Calculate new scroll position, ensuring it doesn't exceed max const newScrollPosition = Math.min(maxScroll, currentScroll + scrollAmount); preview.setScroll(newScrollPosition); // titleBar.setContent(`DOWN ${maxScroll} = ${contentHeight} - ${boxHeight}; min(${maxScroll}, ${currentScroll} + ${scrollAmount}) => ${newScrollPosition}`) screen.render(); } // Home key handler (scroll to top) if (key.name === 'home') { preview.setScroll(0); screen.render(); } // End key handler (scroll to bottom) if (key.name === 'end') { // Calculate max scroll position to show last page of content const maxScroll = Math.max(0, contentHeight - boxHeight + 2); preview.setScroll(maxScroll); screen.render(); } }); // Handle key events screen.key(['q', 'C-c'], () => process.exit(0)); // screen.on('keypress', (ch, key) => { // console.log('keypress', ch, key) // }) // Focus on file list fileList.focus(); // Render screen return { screen, titleBar, fileList, preview, statusBar, result }; } function chooseFile(ctx, info, gist, comamnd) { const { screen, titleBar, fileList, preview, statusBar, result } = ctx; const file = gist.files[result.selectedFile]; screen.destroy(); console.clear(); let acceptTask = null; if (file.raw_url) { const acceptUrl = path.join(file.raw_url, 'accept'); if (comamnd.verbose) console.log(`POST ${acceptUrl}`); acceptTask = fetch(acceptUrl, { method: 'POST' }); } const origTsd = file.content; const tsdAst = astForProject(toAst(origTsd), info); const res = generateCsAstFromTsd(toTsd(tsdAst), { references: [`./api.d.ts`] }); const genApis = new CSharpApiGenerator(); const csApiFiles = genApis.generate(res.csAst); const getMigrations = new CSharpMigrationGenerator(); const csMigrationFiles = getMigrations.generate(res.csAst); const relativeServiceModelDir = trimStart(info.serviceModelDir.substring(info.slnDir.length), '~/'); const relativeMigrationDir = trimStart(info.migrationsDir.substring(info.slnDir.length), '~/'); const apiFileName = `${res.groupName}.cs`; const apiContent = replaceMyApp(csApiFiles[Object.keys(csApiFiles)[0]], info.projectName); const migrationPath = resolveMigrationFile(path.join(info.migrationsDir, `Migration1000.cs`)); const migrationFileName = path.basename(migrationPath); const migrationCls = leftPart(migrationFileName, '.'); const migrationContent = replaceMyApp(csMigrationFiles[Object.keys(csMigrationFiles)[0]].replaceAll('Migration1000', migrationCls), info.projectName); const apiTypesPath = path.join(info.slnDir, relativeServiceModelDir, `api.d.ts`); const apiFile = path.join(import.meta.dirname, 'api.d.ts'); fs.writeFileSync(apiTypesPath, fs.readFileSync(apiFile, 'utf-8')); let uiFileName = info.uiMjsDir ? `${res.groupName}.mjs` : null; const tsdContent = createTsdFile(info, { prompt: titleBar.content.replaceAll('/*', '').replaceAll('*/', ''), apiFileName, tsdAst: res.tsdAst, uiFileName, }); const tsdFileName = `${res.groupName}.d.ts`; const fullTsdPath = path.join(info.slnDir, relativeServiceModelDir, tsdFileName); const fullApiPath = path.join(info.slnDir, relativeServiceModelDir, apiFileName); const fullMigrationPath = path.join(info.slnDir, relativeMigrationDir, migrationFileName); if (!fs.existsSync(path.dirname(fullTsdPath))) { console.log(`Directory does not exist: ${path.dirname(fullTsdPath)}`); process.exit(1); } console.log(`\nSelected '${result.selectedFile}' data models`); fs.writeFileSync(fullTsdPath, tsdContent, { encoding: 'utf-8' }); console.log(`\nSaved: ${fullTsdPath}`); if (fs.existsSync(path.dirname(fullApiPath))) { fs.writeFileSync(fullApiPath, apiContent, { encoding: 'utf-8' }); console.log(`Saved: ${fullApiPath}`); } if (info.uiMjsDir) { const uiGroupPath = path.join(info.uiMjsDir, uiFileName); const uiVueGen = new UiMjsGroupGenerator(); const uiGroupSrc = uiVueGen.generate(res.csAst, res.groupName); fs.writeFileSync(uiGroupPath, uiGroupSrc); console.log(`Saved: ${uiGroupPath}`); const uiIndexGen = new UiMjsIndexGenerator(); const uiIndexSrc = uiIndexGen.generate(fs.readdirSync(info.uiMjsDir)); const uiIndexPath = path.join(info.uiMjsDir, `index.mjs`); fs.writeFileSync(uiIndexPath, uiIndexSrc); console.log(`Saved: ${uiIndexPath}`); } if (fs.existsSync(path.dirname(fullMigrationPath))) { fs.writeFileSync(fullMigrationPath, migrationContent, { encoding: 'utf-8' }); console.log(`Saved: ${fullMigrationPath}`); console.log(`\nRun 'dotnet run --AppTasks=migrate' to apply the new migration and create the new tables`); } const script = path.basename(process.argv[1]); console.log(`\nTo regenerate classes, update '${tsdFileName}' then run:`); console.log(`$ ${script} ${tsdFileName}`); if (acceptTask) { acceptTask.then((r) => r.text()) .then((txt) => { if (comamnd.verbose) console.log(`${txt}`); process.exit(0); }) .catch((err) => { if (comamnd.verbose) console.log(`ERROR: ${err.message ?? err}`); process.exit(1); }); } else { process.exit(0); } } function writeFile(info, filename, content) { let fullPath = path.join(process.cwd(), filename); const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (fs.existsSync(fullPath)) { const filename = path.basename(fullPath); const ext = path.extname(filename); const baseName = path.basename(filename, ext); // filename: Migration1000.cs, baseName: Migration1000, ext: .cs // console.log(`File already exists: ${fullPath}`, { filename, baseName, ext }) const numberedFile = baseName.match(/(\d+)$/); if (numberedFile) { let nextNumber = parseInt(numberedFile[1]); while (fs.existsSync(fullPath)) { if (numberedFile) { nextNumber += 1; fullPath = path.join(dir, `${baseName.replace(/\d+$/, '')}${nextNumber}${ext}`); } } const renamedFile = `${baseName.replace(/\d+$/, '')}${nextNumber}`; content = content.replaceAll(baseName, renamedFile); } } fs.writeFileSync(fullPath, content); } function resolveMigrationFile(fullPath) { const dir = path.dirname(fullPath); const filename = path.basename(fullPath); const ext = path.extname(filename); const baseName = path.basename(filename, ext); // filename: Migration1000.cs, baseName: Migration1000, ext: .cs // console.log(`File already exists: ${fullPath}`, { filename, baseName, ext }) const numberedFile = baseName.match(/(\d+)$/); if (numberedFile) { let nextNumber = parseInt(numberedFile[1]); while (fs.existsSync(fullPath)) { if (numberedFile) { nextNumber += 1; fullPath = path.join(dir, `${baseName.replace(/\d+$/, '')}${nextNumber}${ext}`); } } } return fullPath; } function exit(screen, info, gist) { screen.destroy(); if (info.migrationsDir) { console.log(`\nRun 'dotnet run --AppTasks=migrate' to apply any new migrations and create the new tables`); } process.exit(0); } function createTsdFile(info, opt) { const migrationPath = resolveMigrationFile(path.join(info.migrationsDir, `Migration1000.cs`)); const migrationFileName = path.basename(migrationPath); const relativeServiceModelDir = trimStart(info.serviceModelDir.substring(info.slnDir.length), '~/'); const relativeMigrationDir = info.migrationsDir && fs.existsSync(info.migrationsDir) ? trimStart(info.migrationsDir.substring(info.slnDir.length), '~/') : null; const relativeUiVueDir = opt.uiFileName && info.uiMjsDir && fs.existsSync(info.uiMjsDir) ? trimStart(info.uiMjsDir.substring(info.slnDir.length), '~/') : null; const ast = opt.tsdAst; ast.config = { prompt: opt.prompt, api: `~/${path.join(relativeServiceModelDir, opt.apiFileName)}` }; if (relativeMigrationDir) { ast.config.migration = `~/${path.join(relativeMigrationDir, migrationFileName)}`; } if (relativeUiVueDir) { //console.log('relativeUiVueDir', relativeUiVueDir, info.uiMjsDir, opt.uiFileName) ast.config.uiMjs = `~/${path.join(relativeUiVueDir, opt.uiFileName)}`; } return toTsd(ast); }