offbyt 1.0.0

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 (103) hide show
  1. package/README.md +2 -0
  2. package/cli/index.js +2 -0
  3. package/cli.js +206 -0
  4. package/core/detector/detectAxios.js +107 -0
  5. package/core/detector/detectFetch.js +148 -0
  6. package/core/detector/detectForms.js +55 -0
  7. package/core/detector/detectSocket.js +341 -0
  8. package/core/generator/generateControllers.js +17 -0
  9. package/core/generator/generateModels.js +25 -0
  10. package/core/generator/generateRoutes.js +17 -0
  11. package/core/generator/generateServer.js +18 -0
  12. package/core/generator/generateSocket.js +160 -0
  13. package/core/index.js +14 -0
  14. package/core/ir/IRTypes.js +25 -0
  15. package/core/ir/buildIR.js +83 -0
  16. package/core/parser/parseJS.js +26 -0
  17. package/core/parser/parseTS.js +27 -0
  18. package/core/rules/relationRules.js +38 -0
  19. package/core/rules/resourceRules.js +32 -0
  20. package/core/rules/schemaInference.js +26 -0
  21. package/core/scanner/scanProject.js +58 -0
  22. package/deploy/cloudflare.js +41 -0
  23. package/deploy/cloudflareWorker.js +122 -0
  24. package/deploy/connect.js +198 -0
  25. package/deploy/flyio.js +51 -0
  26. package/deploy/index.js +322 -0
  27. package/deploy/netlify.js +29 -0
  28. package/deploy/railway.js +215 -0
  29. package/deploy/render.js +195 -0
  30. package/deploy/utils.js +383 -0
  31. package/deploy/vercel.js +29 -0
  32. package/index.js +18 -0
  33. package/lib/generator/advancedCrudGenerator.js +475 -0
  34. package/lib/generator/crudCodeGenerator.js +486 -0
  35. package/lib/generator/irBasedGenerator.js +360 -0
  36. package/lib/ir-builder/index.js +16 -0
  37. package/lib/ir-builder/irBuilder.js +330 -0
  38. package/lib/ir-builder/rulesEngine.js +353 -0
  39. package/lib/ir-builder/templateEngine.js +193 -0
  40. package/lib/ir-builder/templates/index.js +14 -0
  41. package/lib/ir-builder/templates/model.template.js +47 -0
  42. package/lib/ir-builder/templates/routes-generic.template.js +66 -0
  43. package/lib/ir-builder/templates/routes-user.template.js +105 -0
  44. package/lib/ir-builder/templates/routes.template.js +102 -0
  45. package/lib/ir-builder/templates/validation.template.js +15 -0
  46. package/lib/ir-integration.js +349 -0
  47. package/lib/modes/benchmark.js +162 -0
  48. package/lib/modes/configBasedGenerator.js +2258 -0
  49. package/lib/modes/connect.js +1125 -0
  50. package/lib/modes/doctorAi.js +172 -0
  51. package/lib/modes/generateApi.js +435 -0
  52. package/lib/modes/interactiveSetup.js +548 -0
  53. package/lib/modes/offline.clean.js +14 -0
  54. package/lib/modes/offline.enhanced.js +787 -0
  55. package/lib/modes/offline.js +295 -0
  56. package/lib/modes/offline.v2.js +13 -0
  57. package/lib/modes/sync.js +629 -0
  58. package/lib/scanner/apiEndpointExtractor.js +387 -0
  59. package/lib/scanner/authPatternDetector.js +54 -0
  60. package/lib/scanner/frontendScanner.js +642 -0
  61. package/lib/utils/apiClientGenerator.js +242 -0
  62. package/lib/utils/apiScanner.js +95 -0
  63. package/lib/utils/codeInjector.js +350 -0
  64. package/lib/utils/doctor.js +381 -0
  65. package/lib/utils/envGenerator.js +36 -0
  66. package/lib/utils/loadTester.js +61 -0
  67. package/lib/utils/performanceAnalyzer.js +298 -0
  68. package/lib/utils/resourceDetector.js +281 -0
  69. package/package.json +20 -0
  70. package/templates/.env.template +31 -0
  71. package/templates/advanced.model.template.js +201 -0
  72. package/templates/advanced.route.template.js +341 -0
  73. package/templates/auth.middleware.template.js +87 -0
  74. package/templates/auth.routes.template.js +238 -0
  75. package/templates/auth.user.model.template.js +78 -0
  76. package/templates/cache.middleware.js +34 -0
  77. package/templates/chat.models.template.js +260 -0
  78. package/templates/chat.routes.template.js +478 -0
  79. package/templates/compression.middleware.js +19 -0
  80. package/templates/database.config.js +74 -0
  81. package/templates/errorHandler.middleware.js +54 -0
  82. package/templates/express/controller.ejs +26 -0
  83. package/templates/express/model.ejs +9 -0
  84. package/templates/express/route.ejs +18 -0
  85. package/templates/express/server.ejs +16 -0
  86. package/templates/frontend.env.template +14 -0
  87. package/templates/model.template.js +86 -0
  88. package/templates/package.production.json +51 -0
  89. package/templates/package.template.json +41 -0
  90. package/templates/pagination.utility.js +110 -0
  91. package/templates/production.server.template.js +233 -0
  92. package/templates/rateLimiter.middleware.js +36 -0
  93. package/templates/requestLogger.middleware.js +19 -0
  94. package/templates/response.helper.js +179 -0
  95. package/templates/route.template.js +130 -0
  96. package/templates/security.middleware.js +78 -0
  97. package/templates/server.template.js +91 -0
  98. package/templates/socket.server.template.js +433 -0
  99. package/templates/utils.helper.js +157 -0
  100. package/templates/validation.middleware.js +63 -0
  101. package/templates/validation.schema.js +128 -0
  102. package/utils/fileWriter.js +15 -0
  103. package/utils/logger.js +18 -0
@@ -0,0 +1,195 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { deployWithCommand, isCommandAvailable, runCommandCapture } from './utils.js';
5
+
6
+ function resolveRenderAssetSuffix() {
7
+ const platform = process.platform;
8
+ const arch = process.arch;
9
+
10
+ if (platform === 'win32') {
11
+ if (arch === 'arm64') return 'windows_arm64.zip';
12
+ if (arch === 'ia32') return 'windows_386.zip';
13
+ return 'windows_amd64.zip';
14
+ }
15
+
16
+ if (platform === 'linux') {
17
+ if (arch === 'arm64') return 'linux_arm64.zip';
18
+ if (arch === 'arm') return 'linux_arm.zip';
19
+ if (arch === 'ia32') return 'linux_386.zip';
20
+ return 'linux_amd64.zip';
21
+ }
22
+
23
+ if (platform === 'darwin') {
24
+ return arch === 'arm64' ? 'darwin_arm64.zip' : 'darwin_amd64.zip';
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ async function fetchLatestRenderRelease() {
31
+ const response = await fetch('https://api.github.com/repos/render-oss/cli/releases/latest', {
32
+ headers: {
33
+ 'User-Agent': 'offbyt-cli'
34
+ }
35
+ });
36
+
37
+ if (!response.ok) {
38
+ throw new Error(`Could not fetch Render CLI release metadata (HTTP ${response.status})`);
39
+ }
40
+
41
+ return response.json();
42
+ }
43
+
44
+ function getRenderInstallPaths() {
45
+ const binDir = path.join(os.homedir(), '.offbyt', 'bin');
46
+ const executableName = process.platform === 'win32' ? 'render.exe' : 'render';
47
+
48
+ return {
49
+ binDir,
50
+ executablePath: path.join(binDir, executableName)
51
+ };
52
+ }
53
+
54
+ function prependBinToPath(binDir) {
55
+ const currentPath = process.env.PATH || '';
56
+ const pathParts = currentPath.split(path.delimiter);
57
+
58
+ if (!pathParts.includes(binDir)) {
59
+ process.env.PATH = `${binDir}${path.delimiter}${currentPath}`;
60
+ }
61
+ }
62
+
63
+ async function installRenderCliFromGithub() {
64
+ const assetSuffix = resolveRenderAssetSuffix();
65
+ if (!assetSuffix) {
66
+ throw new Error(`Render CLI auto-install is not supported on this platform: ${process.platform}/${process.arch}`);
67
+ }
68
+
69
+ const release = await fetchLatestRenderRelease();
70
+ const assets = release.assets || [];
71
+ const targetAsset = assets.find((asset) =>
72
+ String(asset.name || '').toLowerCase().endsWith(assetSuffix)
73
+ );
74
+
75
+ if (!targetAsset || !targetAsset.browser_download_url) {
76
+ throw new Error(`No Render CLI release asset found for ${assetSuffix}`);
77
+ }
78
+
79
+ const { binDir, executablePath } = getRenderInstallPaths();
80
+ const tempRoot = path.join(os.tmpdir(), 'offbyt-render-cli');
81
+ const zipPath = path.join(tempRoot, targetAsset.name);
82
+ const extractDir = path.join(tempRoot, `extract-${Date.now()}`);
83
+
84
+ fs.mkdirSync(tempRoot, { recursive: true });
85
+ fs.mkdirSync(extractDir, { recursive: true });
86
+ fs.mkdirSync(binDir, { recursive: true });
87
+
88
+ const archiveResponse = await fetch(targetAsset.browser_download_url, {
89
+ headers: {
90
+ 'User-Agent': 'offbyt-cli'
91
+ }
92
+ });
93
+
94
+ if (!archiveResponse.ok) {
95
+ throw new Error(`Failed to download Render CLI archive (HTTP ${archiveResponse.status})`);
96
+ }
97
+
98
+ const archiveBuffer = Buffer.from(await archiveResponse.arrayBuffer());
99
+ fs.writeFileSync(zipPath, archiveBuffer);
100
+
101
+ if (process.platform === 'win32') {
102
+ await runCommandCapture({
103
+ command: 'powershell',
104
+ args: ['-NoProfile', '-Command', `Expand-Archive -Path \"${zipPath}\" -DestinationPath \"${extractDir}\" -Force`],
105
+ cwd: process.cwd(),
106
+ streamOutput: false
107
+ });
108
+ } else {
109
+ await runCommandCapture({
110
+ command: 'unzip',
111
+ args: ['-o', zipPath, '-d', extractDir],
112
+ cwd: process.cwd(),
113
+ streamOutput: false
114
+ });
115
+ }
116
+
117
+ const extractedFiles = fs.readdirSync(extractDir);
118
+ const binaryFile = extractedFiles.find((fileName) => {
119
+ const lower = fileName.toLowerCase();
120
+ if (process.platform === 'win32') {
121
+ return lower.startsWith('cli_v') && lower.endsWith('.exe');
122
+ }
123
+ return lower.startsWith('cli_v');
124
+ });
125
+
126
+ if (!binaryFile) {
127
+ throw new Error('Downloaded Render CLI archive did not contain expected binary');
128
+ }
129
+
130
+ const sourceBinary = path.join(extractDir, binaryFile);
131
+ fs.copyFileSync(sourceBinary, executablePath);
132
+
133
+ if (process.platform !== 'win32') {
134
+ fs.chmodSync(executablePath, 0o755);
135
+ }
136
+
137
+ prependBinToPath(binDir);
138
+ }
139
+
140
+ async function ensureRenderCommandAvailable() {
141
+ const { binDir, executablePath } = getRenderInstallPaths();
142
+ prependBinToPath(binDir);
143
+
144
+ if (fs.existsSync(executablePath)) {
145
+ return;
146
+ }
147
+
148
+ if (await isCommandAvailable('render')) {
149
+ return;
150
+ }
151
+
152
+ await installRenderCliFromGithub();
153
+
154
+ if (!(await isCommandAvailable('render'))) {
155
+ throw new Error('Render CLI installation completed but command is still unavailable in PATH');
156
+ }
157
+ }
158
+
159
+ async function checkRenderLogin() {
160
+ try {
161
+ const result = await runCommandCapture({
162
+ command: 'render',
163
+ args: ['workspaces', '--output', 'text', '--confirm'],
164
+ cwd: process.cwd(),
165
+ streamOutput: false
166
+ });
167
+ const output = `${result.stdout}\n${result.stderr}`.toLowerCase();
168
+ return !output.includes('authentication') && !output.includes('login') && !output.includes('unauthorized');
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+
174
+ export async function deployToRender(backendPath, options = {}) {
175
+ await ensureRenderCommandAvailable();
176
+
177
+ const serviceId = String(options.serviceId || process.env.RENDER_SERVICE_ID || '').trim();
178
+ if (!serviceId) {
179
+ throw new Error('Render service ID is required. Pass --backend-service-id <SERVICE_ID> or set RENDER_SERVICE_ID.');
180
+ }
181
+
182
+ return deployWithCommand({
183
+ providerName: 'Render',
184
+ command: 'render',
185
+ packageName: null,
186
+ installHint: 'Render CLI auto-install failed. Download from https://github.com/render-oss/cli/releases and ensure `render` is in PATH.',
187
+ args: ['deploys', 'create', serviceId],
188
+ cwd: backendPath,
189
+ urlHints: ['onrender.com', 'dashboard.render.com'],
190
+ loginCheck: checkRenderLogin,
191
+ loginCommand: { command: 'render', args: ['login'] },
192
+ successLabel: 'Backend deployed on Render'
193
+ });
194
+ }
195
+
@@ -0,0 +1,383 @@
1
+ import { spawn } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+
7
+ const URL_REGEX = /https?:\/\/[^\s"'`<>]+/gi;
8
+
9
+ export function normalizeProviderKey(value = '') {
10
+ return String(value)
11
+ .trim()
12
+ .toLowerCase()
13
+ .replace(/\s+/g, '')
14
+ .replace(/\./g, '')
15
+ .replace(/-/g, '')
16
+ .replace(/\/+/g, '');
17
+ }
18
+
19
+ export async function isCommandAvailable(command) {
20
+ const checks = [
21
+ ['--version'],
22
+ ['version'],
23
+ ['-v']
24
+ ];
25
+
26
+ for (const args of checks) {
27
+ try {
28
+ await runCommandCapture({
29
+ command,
30
+ args,
31
+ cwd: process.cwd(),
32
+ streamOutput: false
33
+ });
34
+ return true;
35
+ } catch {
36
+ // Try next version flag
37
+ }
38
+ }
39
+
40
+ return false;
41
+ }
42
+
43
+ export async function autoInstallCLI(packageName, command) {
44
+ console.log(chalk.cyan(`\n📦 Installing ${command} CLI...\n`));
45
+
46
+ const spinner = ora(`Installing ${packageName}...`).start();
47
+
48
+ try {
49
+ await runCommandCapture({
50
+ command: 'npm',
51
+ args: ['install', '-g', packageName],
52
+ cwd: process.cwd(),
53
+ streamOutput: true
54
+ });
55
+
56
+ spinner.succeed(`${command} CLI installed successfully`);
57
+ return true;
58
+ } catch (error) {
59
+ spinner.fail(`Failed to install ${command} CLI`);
60
+ throw new Error(`Could not install ${packageName}. Please install manually: npm install -g ${packageName}`);
61
+ }
62
+ }
63
+
64
+ export async function ensureCommandAvailable(command, packageName, installHint) {
65
+ const isAvailable = await isCommandAvailable(command);
66
+
67
+ if (!isAvailable) {
68
+ console.log(chalk.yellow(`⚠️ ${command} CLI not found`));
69
+
70
+ if (!packageName) {
71
+ const hint = installHint
72
+ ? ` ${installHint}`
73
+ : ` Please install "${command}" and ensure it is available in PATH.`;
74
+ throw new Error(`${command} CLI not found.${hint}`);
75
+ }
76
+
77
+ // Auto-install
78
+ await autoInstallCLI(packageName, command);
79
+
80
+ // Verify installation
81
+ const stillNotAvailable = !(await isCommandAvailable(command));
82
+ if (stillNotAvailable) {
83
+ throw new Error(`${command} CLI installation failed. Please install manually.`);
84
+ }
85
+ }
86
+ }
87
+
88
+ export async function deployWithCommand({
89
+ providerName,
90
+ command,
91
+ args,
92
+ cwd,
93
+ urlHints = [],
94
+ packageName,
95
+ installHint,
96
+ loginCheck,
97
+ loginCommand,
98
+ preflight,
99
+ postDeploy,
100
+ successLabel,
101
+ commandNeedsTty = false
102
+ }) {
103
+ // Step 1: Ensure CLI is installed (auto-install if needed)
104
+ await ensureCommandAvailable(command, packageName, installHint);
105
+
106
+ // Step 2: Check login status and prompt if needed
107
+ if (typeof loginCheck === 'function') {
108
+ const isLoggedIn = await loginCheck();
109
+ if (!isLoggedIn && loginCommand) {
110
+ console.log(chalk.yellow(`\n⚠️ Not logged in to ${providerName}`));
111
+ console.log(chalk.cyan(`🔐 Please login to continue...\n`));
112
+
113
+ try {
114
+ await runCommandCapture({
115
+ command: loginCommand.command,
116
+ args: loginCommand.args || [],
117
+ cwd: process.cwd(),
118
+ streamOutput: true,
119
+ interactive: true
120
+ });
121
+ console.log(chalk.green(`\n✅ Successfully logged in to ${providerName}\n`));
122
+ } catch (error) {
123
+ throw new Error(`Login to ${providerName} failed. Please try again.`);
124
+ }
125
+ }
126
+ }
127
+
128
+ // Step 3: Run preflight checks
129
+ if (typeof preflight === 'function') {
130
+ await preflight();
131
+ }
132
+
133
+ const spinner = ora(`Deploying to ${providerName}...`).start();
134
+
135
+ try {
136
+ const resolvedArgs = typeof args === 'function' ? await args() : args;
137
+ spinner.stop();
138
+ const result = await runCommandCapture({
139
+ command,
140
+ args: resolvedArgs,
141
+ cwd,
142
+ streamOutput: true,
143
+ interactive: commandNeedsTty
144
+ });
145
+
146
+ const output = `${result.stdout}\n${result.stderr}`;
147
+ let deployedUrl = null;
148
+
149
+ if (typeof postDeploy === 'function') {
150
+ try {
151
+ deployedUrl = await postDeploy({ cwd, output, command, args: resolvedArgs, providerName });
152
+ } catch {
153
+ // Fall back to parsing command output when post-deploy URL lookup fails.
154
+ }
155
+ }
156
+
157
+ if (!deployedUrl) {
158
+ deployedUrl = extractUrl(output, urlHints);
159
+ }
160
+
161
+ if (!deployedUrl) {
162
+ throw new Error(
163
+ `Deployment completed but no URL was detected in ${providerName} output.`
164
+ );
165
+ }
166
+
167
+ spinner.succeed(successLabel || `${providerName} deployment complete`);
168
+
169
+ return {
170
+ provider: providerName,
171
+ url: deployedUrl,
172
+ output
173
+ };
174
+ } catch (error) {
175
+ spinner.fail(`${providerName} deployment failed`);
176
+ throw error;
177
+ }
178
+ }
179
+
180
+ export async function runCommandCapture({ command, args = [], cwd, streamOutput = true, interactive = false }) {
181
+ return new Promise((resolve, reject) => {
182
+ const stdio = interactive ? 'inherit' : ['inherit', 'pipe', 'pipe'];
183
+
184
+ // When using shell: true, properly quote arguments that contain spaces
185
+ const quotedArgs = args.map((arg) => {
186
+ if (typeof arg !== 'string') return String(arg);
187
+ if (arg.includes(' ')) {
188
+ return `"${arg.replace(/"/g, '\\"')}"`;
189
+ }
190
+ return arg;
191
+ });
192
+
193
+ const child = spawn(command, quotedArgs, {
194
+ cwd,
195
+ shell: true,
196
+ env: process.env,
197
+ windowsHide: false,
198
+ stdio
199
+ });
200
+
201
+ let stdout = '';
202
+ let stderr = '';
203
+
204
+ if (child.stdout) {
205
+ child.stdout.on('data', (chunk) => {
206
+ const text = chunk.toString();
207
+ stdout += text;
208
+ if (streamOutput) {
209
+ process.stdout.write(chalk.gray(text));
210
+ }
211
+ });
212
+ }
213
+
214
+ if (child.stderr) {
215
+ child.stderr.on('data', (chunk) => {
216
+ const text = chunk.toString();
217
+ stderr += text;
218
+ if (streamOutput) {
219
+ process.stderr.write(chalk.gray(text));
220
+ }
221
+ });
222
+ }
223
+
224
+ child.on('error', (error) => {
225
+ reject(new Error(`Failed to run command "${command}": ${error.message}`));
226
+ });
227
+
228
+ child.on('close', (code) => {
229
+ if (code === 0) {
230
+ resolve({ stdout, stderr, code });
231
+ return;
232
+ }
233
+
234
+ const combined = `${stdout}\n${stderr}`.trim();
235
+ const preview = combined
236
+ ? combined.split('\n').slice(-12).join('\n')
237
+ : 'No captured output (interactive command mode).';
238
+
239
+ reject(
240
+ new Error(
241
+ `Command failed (${command} ${args.join(' ')}). Exit code: ${code}.\n${preview}`
242
+ )
243
+ );
244
+ });
245
+ });
246
+ }
247
+
248
+ export function extractUrl(output = '', urlHints = []) {
249
+ const matches = [...new Set(String(output).match(URL_REGEX) || [])]
250
+ .map(cleanDetectedUrl)
251
+ .filter(Boolean);
252
+
253
+ if (matches.length === 0) {
254
+ return null;
255
+ }
256
+
257
+ const normalizedHints = urlHints
258
+ .map((hint) => String(hint).trim().toLowerCase())
259
+ .filter(Boolean);
260
+
261
+ const hintedMatches = normalizedHints.length
262
+ ? matches.filter((url) => normalizedHints.some((hint) => url.toLowerCase().includes(hint)))
263
+ : matches;
264
+
265
+ const candidates = hintedMatches.length > 0 ? hintedMatches : matches;
266
+
267
+ const httpsCandidates = candidates.filter((url) => url.startsWith('https://'));
268
+ const rankingPool = httpsCandidates.length > 0 ? httpsCandidates : candidates;
269
+
270
+ return rankingPool[rankingPool.length - 1] || null;
271
+ }
272
+
273
+ function cleanDetectedUrl(value) {
274
+ return String(value)
275
+ .trim()
276
+ .replace(/[),.;]+$/g, '');
277
+ }
278
+
279
+ export function detectBuildOutputDirectory(frontendPath) {
280
+ const candidates = ['dist', 'build', 'out', '.next'];
281
+
282
+ for (const candidate of candidates) {
283
+ const fullPath = path.join(frontendPath, candidate);
284
+ if (fs.existsSync(fullPath)) {
285
+ return candidate;
286
+ }
287
+ }
288
+
289
+ return null;
290
+ }
291
+
292
+ export function detectFrontendPath(projectPath) {
293
+ const candidates = [
294
+ projectPath,
295
+ path.join(projectPath, 'frontend'),
296
+ path.join(projectPath, 'client'),
297
+ path.join(projectPath, 'web')
298
+ ];
299
+
300
+ for (const candidate of candidates) {
301
+ if (!fs.existsSync(candidate)) continue;
302
+
303
+ const hasPackage = fs.existsSync(path.join(candidate, 'package.json'));
304
+ const hasSource = ['src', 'app', 'pages'].some((folder) =>
305
+ fs.existsSync(path.join(candidate, folder))
306
+ );
307
+
308
+ if (hasPackage || hasSource) {
309
+ return candidate;
310
+ }
311
+ }
312
+
313
+ return projectPath;
314
+ }
315
+
316
+ export function detectBackendPath(projectPath) {
317
+ const candidates = [
318
+ path.join(projectPath, 'backend'),
319
+ path.join(projectPath, 'api'),
320
+ projectPath
321
+ ];
322
+
323
+ for (const candidate of candidates) {
324
+ const hasPackage = fs.existsSync(path.join(candidate, 'package.json'));
325
+ const hasServer = ['server.js', 'index.js', 'app.js', 'main.ts'].some((file) =>
326
+ fs.existsSync(path.join(candidate, file))
327
+ );
328
+
329
+ if (hasPackage || hasServer) {
330
+ return candidate;
331
+ }
332
+ }
333
+
334
+ return projectPath;
335
+ }
336
+
337
+ export function detectLocalBackendUrl(projectPath) {
338
+ const backendPath = detectBackendPath(projectPath);
339
+ const candidateFiles = [
340
+ path.join(backendPath, 'server.js'),
341
+ path.join(backendPath, 'index.js'),
342
+ path.join(backendPath, 'app.js'),
343
+ path.join(backendPath, 'main.ts')
344
+ ];
345
+
346
+ let port = 5000;
347
+
348
+ for (const filePath of candidateFiles) {
349
+ if (!fs.existsSync(filePath)) continue;
350
+
351
+ try {
352
+ const content = fs.readFileSync(filePath, 'utf8');
353
+ const envPortMatch = content.match(/process\.env\.PORT\s*\|\|\s*(\d+)/);
354
+ if (envPortMatch) {
355
+ port = Number(envPortMatch[1]);
356
+ break;
357
+ }
358
+
359
+ const directMatch = content.match(/PORT\s*=\s*['"]?(\d+)['"]?/);
360
+ if (directMatch) {
361
+ port = Number(directMatch[1]);
362
+ break;
363
+ }
364
+ } catch {
365
+ // Ignore unreadable files and continue scanning.
366
+ }
367
+ }
368
+
369
+ return `http://localhost:${port}`;
370
+ }
371
+
372
+ export function readPackageJsonSafe(targetPath) {
373
+ const packagePath = path.join(targetPath, 'package.json');
374
+ if (!fs.existsSync(packagePath)) {
375
+ return null;
376
+ }
377
+
378
+ try {
379
+ return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
@@ -0,0 +1,29 @@
1
+ import { deployWithCommand, runCommandCapture } from './utils.js';
2
+
3
+ async function checkVercelLogin() {
4
+ try {
5
+ const result = await runCommandCapture({
6
+ command: 'vercel',
7
+ args: ['whoami'],
8
+ cwd: process.cwd(),
9
+ streamOutput: false
10
+ });
11
+ return !result.stderr.toLowerCase().includes('not');
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export async function deployToVercel(frontendPath) {
18
+ return deployWithCommand({
19
+ providerName: 'Vercel',
20
+ command: 'vercel',
21
+ packageName: 'vercel',
22
+ args: ['--prod', '--yes'],
23
+ cwd: frontendPath,
24
+ urlHints: ['vercel.app'],
25
+ loginCheck: checkVercelLogin,
26
+ loginCommand: { command: 'vercel', args: ['login'] },
27
+ successLabel: 'Frontend deployed on Vercel'
28
+ });
29
+ }
package/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Offbyt - Hybrid Backend Generator
3
+ *
4
+ * Main entry point for programmatic usage
5
+ *
6
+ * Usage:
7
+ * import { offlineMode, runDoctor } from 'offbyt';
8
+ *
9
+ * await offlineMode('/path/to/project');
10
+ * await runDoctor();
11
+ */
12
+
13
+ export { offlineMode } from './lib/modes/offline.js';
14
+ export { runDoctor } from './lib/utils/doctor.js';
15
+ export { scanFrontendCode, generateRoutesFromAPICalls, buildHybridIR } from './lib/scanner/frontendScanner.js';
16
+ export { detectSocket } from './core/detector/detectSocket.js';
17
+ export { generateSocketBackend, generateServerWithSocket } from './core/generator/generateSocket.js';
18
+ export * from './core/index.js';