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.
- package/README.md +2 -0
- package/cli/index.js +2 -0
- package/cli.js +206 -0
- package/core/detector/detectAxios.js +107 -0
- package/core/detector/detectFetch.js +148 -0
- package/core/detector/detectForms.js +55 -0
- package/core/detector/detectSocket.js +341 -0
- package/core/generator/generateControllers.js +17 -0
- package/core/generator/generateModels.js +25 -0
- package/core/generator/generateRoutes.js +17 -0
- package/core/generator/generateServer.js +18 -0
- package/core/generator/generateSocket.js +160 -0
- package/core/index.js +14 -0
- package/core/ir/IRTypes.js +25 -0
- package/core/ir/buildIR.js +83 -0
- package/core/parser/parseJS.js +26 -0
- package/core/parser/parseTS.js +27 -0
- package/core/rules/relationRules.js +38 -0
- package/core/rules/resourceRules.js +32 -0
- package/core/rules/schemaInference.js +26 -0
- package/core/scanner/scanProject.js +58 -0
- package/deploy/cloudflare.js +41 -0
- package/deploy/cloudflareWorker.js +122 -0
- package/deploy/connect.js +198 -0
- package/deploy/flyio.js +51 -0
- package/deploy/index.js +322 -0
- package/deploy/netlify.js +29 -0
- package/deploy/railway.js +215 -0
- package/deploy/render.js +195 -0
- package/deploy/utils.js +383 -0
- package/deploy/vercel.js +29 -0
- package/index.js +18 -0
- package/lib/generator/advancedCrudGenerator.js +475 -0
- package/lib/generator/crudCodeGenerator.js +486 -0
- package/lib/generator/irBasedGenerator.js +360 -0
- package/lib/ir-builder/index.js +16 -0
- package/lib/ir-builder/irBuilder.js +330 -0
- package/lib/ir-builder/rulesEngine.js +353 -0
- package/lib/ir-builder/templateEngine.js +193 -0
- package/lib/ir-builder/templates/index.js +14 -0
- package/lib/ir-builder/templates/model.template.js +47 -0
- package/lib/ir-builder/templates/routes-generic.template.js +66 -0
- package/lib/ir-builder/templates/routes-user.template.js +105 -0
- package/lib/ir-builder/templates/routes.template.js +102 -0
- package/lib/ir-builder/templates/validation.template.js +15 -0
- package/lib/ir-integration.js +349 -0
- package/lib/modes/benchmark.js +162 -0
- package/lib/modes/configBasedGenerator.js +2258 -0
- package/lib/modes/connect.js +1125 -0
- package/lib/modes/doctorAi.js +172 -0
- package/lib/modes/generateApi.js +435 -0
- package/lib/modes/interactiveSetup.js +548 -0
- package/lib/modes/offline.clean.js +14 -0
- package/lib/modes/offline.enhanced.js +787 -0
- package/lib/modes/offline.js +295 -0
- package/lib/modes/offline.v2.js +13 -0
- package/lib/modes/sync.js +629 -0
- package/lib/scanner/apiEndpointExtractor.js +387 -0
- package/lib/scanner/authPatternDetector.js +54 -0
- package/lib/scanner/frontendScanner.js +642 -0
- package/lib/utils/apiClientGenerator.js +242 -0
- package/lib/utils/apiScanner.js +95 -0
- package/lib/utils/codeInjector.js +350 -0
- package/lib/utils/doctor.js +381 -0
- package/lib/utils/envGenerator.js +36 -0
- package/lib/utils/loadTester.js +61 -0
- package/lib/utils/performanceAnalyzer.js +298 -0
- package/lib/utils/resourceDetector.js +281 -0
- package/package.json +20 -0
- package/templates/.env.template +31 -0
- package/templates/advanced.model.template.js +201 -0
- package/templates/advanced.route.template.js +341 -0
- package/templates/auth.middleware.template.js +87 -0
- package/templates/auth.routes.template.js +238 -0
- package/templates/auth.user.model.template.js +78 -0
- package/templates/cache.middleware.js +34 -0
- package/templates/chat.models.template.js +260 -0
- package/templates/chat.routes.template.js +478 -0
- package/templates/compression.middleware.js +19 -0
- package/templates/database.config.js +74 -0
- package/templates/errorHandler.middleware.js +54 -0
- package/templates/express/controller.ejs +26 -0
- package/templates/express/model.ejs +9 -0
- package/templates/express/route.ejs +18 -0
- package/templates/express/server.ejs +16 -0
- package/templates/frontend.env.template +14 -0
- package/templates/model.template.js +86 -0
- package/templates/package.production.json +51 -0
- package/templates/package.template.json +41 -0
- package/templates/pagination.utility.js +110 -0
- package/templates/production.server.template.js +233 -0
- package/templates/rateLimiter.middleware.js +36 -0
- package/templates/requestLogger.middleware.js +19 -0
- package/templates/response.helper.js +179 -0
- package/templates/route.template.js +130 -0
- package/templates/security.middleware.js +78 -0
- package/templates/server.template.js +91 -0
- package/templates/socket.server.template.js +433 -0
- package/templates/utils.helper.js +157 -0
- package/templates/validation.middleware.js +63 -0
- package/templates/validation.schema.js +128 -0
- package/utils/fileWriter.js +15 -0
- package/utils/logger.js +18 -0
package/deploy/render.js
ADDED
|
@@ -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
|
+
|
package/deploy/utils.js
ADDED
|
@@ -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
|
+
}
|
package/deploy/vercel.js
ADDED
|
@@ -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';
|