@nometria-ai/nom 0.2.2 → 0.2.4
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/package.json +1 -1
- package/src/commands/deploy.js +18 -3
- package/src/commands/init.js +13 -3
- package/src/lib/detect.js +118 -1
package/package.json
CHANGED
package/src/commands/deploy.js
CHANGED
|
@@ -6,6 +6,7 @@ import { execSync } from 'node:child_process';
|
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { readConfig, resolveEnv, updateConfig } from '../lib/config.js';
|
|
9
|
+
import { detectServices } from '../lib/detect.js';
|
|
9
10
|
import { requireApiKey } from '../lib/auth.js';
|
|
10
11
|
import { apiRequest, uploadFile } from '../lib/api.js';
|
|
11
12
|
import { createTarball } from '../lib/tar.js';
|
|
@@ -19,6 +20,17 @@ export async function deploy(flags) {
|
|
|
19
20
|
const appName = config.name || config.app_id;
|
|
20
21
|
const isResync = !!config.app_id;
|
|
21
22
|
|
|
23
|
+
// Auto-detect services if not in config (so nometria.json includes it in tarball)
|
|
24
|
+
if (!config.services) {
|
|
25
|
+
const { services, docker_compose } = detectServices(process.cwd());
|
|
26
|
+
if (services.length > 0 || docker_compose) {
|
|
27
|
+
const updates = {};
|
|
28
|
+
if (services.length > 0) updates.services = services;
|
|
29
|
+
if (docker_compose) updates.docker_compose = true;
|
|
30
|
+
try { updateConfig(process.cwd(), updates); Object.assign(config, updates); } catch { /* non-fatal */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
if (isResync) {
|
|
23
35
|
console.log(`\n Resyncing ${appName} on ${config.platform} (${config.region})\n`);
|
|
24
36
|
} else {
|
|
@@ -99,16 +111,19 @@ export async function deploy(flags) {
|
|
|
99
111
|
const deployStatus = statusResult.data?.deploymentStatus;
|
|
100
112
|
const instanceState = statusResult.data?.instanceState;
|
|
101
113
|
const topStatus = statusResult.status;
|
|
114
|
+
// Display status: prefer deploymentStatus (accurate), fall back to instanceState
|
|
102
115
|
const st = deployStatus || instanceState || topStatus || 'unknown';
|
|
103
116
|
deploySpinner.update(`${isResync ? 'Resyncing' : 'Deploying'} — ${st}`);
|
|
104
117
|
|
|
105
|
-
//
|
|
106
|
-
|
|
118
|
+
// Only trust deploymentStatus for completion — instanceState='running' just means
|
|
119
|
+
// EC2 booted, not that the deploy script finished.
|
|
120
|
+
const isDone = deployStatus === 'completed' || deployStatus === 'running' || topStatus === 'deployed';
|
|
121
|
+
if (isDone) {
|
|
107
122
|
finalStatus = statusResult;
|
|
108
123
|
deploySpinner.succeed(isResync ? 'Resynced successfully' : 'Deployed successfully');
|
|
109
124
|
break;
|
|
110
125
|
}
|
|
111
|
-
if (st === 'failed') {
|
|
126
|
+
if (deployStatus === 'failed' || st === 'failed') {
|
|
112
127
|
deploySpinner.fail(`${isResync ? 'Resync' : 'Deploy'} failed: ${statusResult.data?.errorMessage || 'unknown error'}`);
|
|
113
128
|
process.exit(1);
|
|
114
129
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { writeFileSync } from 'node:fs';
|
|
5
5
|
import { join, basename } from 'node:path';
|
|
6
|
-
import { detectFramework, detectPackageManager } from '../lib/detect.js';
|
|
6
|
+
import { detectFramework, detectPackageManager, detectServices } from '../lib/detect.js';
|
|
7
7
|
import { configExists, CONFIG_FILE, VALID_PLATFORMS } from '../lib/config.js';
|
|
8
8
|
import { ask, choose, confirm } from '../lib/prompt.js';
|
|
9
9
|
|
|
@@ -30,10 +30,18 @@ export async function init(flags) {
|
|
|
30
30
|
|
|
31
31
|
console.log('\n Setting up your project for deployment\n');
|
|
32
32
|
|
|
33
|
-
// Detect framework
|
|
33
|
+
// Detect framework and services
|
|
34
34
|
const detected = detectFramework(dir);
|
|
35
35
|
const pkgManager = detectPackageManager(dir);
|
|
36
|
-
|
|
36
|
+
const { services, docker_compose } = detectServices(dir);
|
|
37
|
+
console.log(` Detected: ${detected.framework} (${pkgManager})`);
|
|
38
|
+
if (services.length > 0) {
|
|
39
|
+
console.log(` Services: ${services.map(s => `${s.name} (${s.type})`).join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
if (docker_compose) {
|
|
42
|
+
console.log(` Docker Compose: yes`);
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
37
45
|
|
|
38
46
|
// Project name
|
|
39
47
|
const dirName = basename(dir).replace(/[^a-z0-9-]/gi, '-').toLowerCase();
|
|
@@ -73,6 +81,8 @@ export async function init(flags) {
|
|
|
73
81
|
},
|
|
74
82
|
env: {},
|
|
75
83
|
ignore: [],
|
|
84
|
+
...(services.length > 0 ? { services } : {}),
|
|
85
|
+
...(docker_compose ? { docker_compose: true } : {}),
|
|
76
86
|
};
|
|
77
87
|
|
|
78
88
|
const configPath = join(dir, CONFIG_FILE);
|
package/src/lib/detect.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-detect project framework and build settings.
|
|
3
3
|
*/
|
|
4
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join, basename } from 'node:path';
|
|
6
6
|
|
|
7
7
|
const DETECTORS = [
|
|
@@ -72,6 +72,123 @@ export function detectPackageManager(dir = process.cwd()) {
|
|
|
72
72
|
return 'npm';
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// ── Frontend/backend indicator deps ────────────────────────────────────────
|
|
76
|
+
const FRONTEND_INDICATORS = new Set([
|
|
77
|
+
'react', 'react-dom', 'vue', 'svelte', '@sveltejs/kit', 'next', 'nuxt',
|
|
78
|
+
'@angular/core', 'vite', 'solid-js', 'astro', '@remix-run/react',
|
|
79
|
+
]);
|
|
80
|
+
const FRONTEND_CONFIG_FILES = [
|
|
81
|
+
'vite.config.js', 'vite.config.ts', 'vite.config.mjs',
|
|
82
|
+
'next.config.js', 'next.config.mjs', 'next.config.ts',
|
|
83
|
+
'svelte.config.js', 'nuxt.config.ts', 'angular.json', 'astro.config.mjs',
|
|
84
|
+
];
|
|
85
|
+
const BACKEND_INDICATORS = new Set([
|
|
86
|
+
'express', 'fastify', 'hono', 'koa', '@nestjs/core', '@hapi/hapi',
|
|
87
|
+
'restify', 'polka', 'micro', 'moleculer',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Classify a directory as frontend, backend, or unknown based on its package.json
|
|
92
|
+
* and config files.
|
|
93
|
+
*/
|
|
94
|
+
function classifyServiceDir(dir) {
|
|
95
|
+
const pkgPath = join(dir, 'package.json');
|
|
96
|
+
if (!existsSync(pkgPath)) return null;
|
|
97
|
+
|
|
98
|
+
let pkg;
|
|
99
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); } catch { return null; }
|
|
100
|
+
|
|
101
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
102
|
+
const depNames = Object.keys(deps);
|
|
103
|
+
|
|
104
|
+
// Check for frontend config files
|
|
105
|
+
const hasFrontendConfig = FRONTEND_CONFIG_FILES.some(f => existsSync(join(dir, f)));
|
|
106
|
+
const hasFrontendDep = depNames.some(d => FRONTEND_INDICATORS.has(d));
|
|
107
|
+
const hasBackendDep = depNames.some(d => BACKEND_INDICATORS.has(d));
|
|
108
|
+
|
|
109
|
+
let type = 'unknown';
|
|
110
|
+
if (hasFrontendConfig || (hasFrontendDep && !hasBackendDep)) {
|
|
111
|
+
type = 'frontend';
|
|
112
|
+
} else if (hasBackendDep || (pkg.scripts?.start && !hasFrontendDep)) {
|
|
113
|
+
type = 'backend';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Detect build command
|
|
117
|
+
let build = null;
|
|
118
|
+
if (pkg.scripts?.build) {
|
|
119
|
+
const pm = detectPackageManager(dir);
|
|
120
|
+
build = `${pm} run build`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Detect start command
|
|
124
|
+
let start = null;
|
|
125
|
+
if (pkg.scripts?.start) {
|
|
126
|
+
const pm = detectPackageManager(dir);
|
|
127
|
+
start = `${pm} run start`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Try to detect port from scripts.start or common env patterns
|
|
131
|
+
let port = null;
|
|
132
|
+
const startScript = pkg.scripts?.start || '';
|
|
133
|
+
const portMatch = startScript.match(/(?:--port|PORT=|:)\s*(\d{4,5})/);
|
|
134
|
+
if (portMatch) port = parseInt(portMatch[1], 10);
|
|
135
|
+
|
|
136
|
+
return { type, build, start, port };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Detect multi-service project structure.
|
|
141
|
+
* Scans subdirs for package.json, classifies as frontend/backend,
|
|
142
|
+
* and checks for docker-compose.yml.
|
|
143
|
+
*
|
|
144
|
+
* Returns { services: [...], docker_compose: boolean }
|
|
145
|
+
* services is empty for single-root projects (existing flow handles those).
|
|
146
|
+
*/
|
|
147
|
+
export function detectServices(dir = process.cwd()) {
|
|
148
|
+
const result = { services: [], docker_compose: false };
|
|
149
|
+
|
|
150
|
+
// Check for docker-compose
|
|
151
|
+
if (existsSync(join(dir, 'docker-compose.yml')) || existsSync(join(dir, 'docker-compose.yaml'))) {
|
|
152
|
+
result.docker_compose = true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// If root has package.json AND no subdirs with package.json → single-root project
|
|
156
|
+
const hasRootPkg = existsSync(join(dir, 'package.json'));
|
|
157
|
+
|
|
158
|
+
// Scan immediate subdirs for package.json
|
|
159
|
+
let entries;
|
|
160
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return result; }
|
|
161
|
+
|
|
162
|
+
const subdirServices = [];
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
if (!entry.isDirectory()) continue;
|
|
165
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
166
|
+
|
|
167
|
+
const subdir = join(dir, entry.name);
|
|
168
|
+
const info = classifyServiceDir(subdir);
|
|
169
|
+
if (!info) continue;
|
|
170
|
+
|
|
171
|
+
subdirServices.push({
|
|
172
|
+
name: entry.name,
|
|
173
|
+
path: entry.name,
|
|
174
|
+
type: info.type,
|
|
175
|
+
...(info.build ? { build: info.build } : {}),
|
|
176
|
+
...(info.start ? { start: info.start } : {}),
|
|
177
|
+
...(info.port ? { port: info.port } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Only populate services for multi-folder projects
|
|
182
|
+
if (subdirServices.length > 0) {
|
|
183
|
+
// Sort: frontends first, then backends, then unknown
|
|
184
|
+
const order = { frontend: 0, backend: 1, unknown: 2 };
|
|
185
|
+
subdirServices.sort((a, b) => (order[a.type] ?? 2) - (order[b.type] ?? 2));
|
|
186
|
+
result.services = subdirServices;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
75
192
|
export function getProjectName(dir = process.cwd()) {
|
|
76
193
|
const pkgPath = join(dir, 'package.json');
|
|
77
194
|
if (existsSync(pkgPath)) {
|