@nometria-ai/nom 0.2.1 → 0.2.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nometria-ai/nom",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Deploy any project to any cloud from your terminal. One command, zero config.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 {
@@ -129,7 +141,7 @@ export async function deploy(flags) {
129
141
  const url = finalStatus.data?.deployUrl || finalStatus.url || `https://${appName}.nometria.com`;
130
142
  console.log(`
131
143
  Live at: ${url}
132
- Dashboard: https://nometria.com/apps/${appName}
144
+ Dashboard: https://nometria.com/AppDetails?app_id=${appName}
133
145
  `);
134
146
 
135
147
  // Step 8: Auto-detect git repo and offer GitHub connection
@@ -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
- console.log(` Detected: ${detected.framework} (${pkgManager})\n`);
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)) {