@nometria-ai/nom 0.2.10 → 0.3.1

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.
@@ -12,9 +12,20 @@ import { apiRequest, uploadFile } from '../lib/api.js';
12
12
  import { createTarball } from '../lib/tar.js';
13
13
  import { createSpinner } from '../lib/spinner.js';
14
14
  import { confirm } from '../lib/prompt.js';
15
+ import { trackCommand } from '../lib/telemetry.js';
15
16
  import { login } from './login.js';
16
17
  import { init } from './init.js';
17
18
 
19
+ const INSTANCE_COSTS = {
20
+ '2gb': { monthly: '$39', type: 't4g.small' },
21
+ '4gb': { monthly: '$49', type: 't4g.medium' },
22
+ '8gb': { monthly: '$79', type: 't4g.large' },
23
+ '16gb': { monthly: '$129', type: 't4g.xlarge' },
24
+ };
25
+
26
+ const HELP_URL = 'https://docs.nometria.com';
27
+ const DASHBOARD_URL = 'https://nometria.com';
28
+
18
29
  export async function deploy(flags) {
19
30
  // Auto-login if not authenticated
20
31
  let apiKey = getApiKey();
@@ -39,6 +50,13 @@ export async function deploy(flags) {
39
50
  const envVars = resolveEnv(config.env);
40
51
  const appName = config.name || config.app_id;
41
52
  const isResync = !!config.app_id;
53
+ const isDryRun = flags['dry-run'] || flags.dryRun;
54
+
55
+ const telemetry = trackCommand('deploy', { framework: config.framework, platform: config.platform, isResync });
56
+
57
+ if (isDryRun) {
58
+ return dryRun(config, apiKey, appName);
59
+ }
42
60
 
43
61
  // Auto-detect services if not in config (so nometria.json includes it in tarball)
44
62
  if (!config.services) {
@@ -51,15 +69,26 @@ export async function deploy(flags) {
51
69
  }
52
70
  }
53
71
 
72
+ const instanceSize = config.instanceType || '4gb';
73
+ const cost = INSTANCE_COSTS[instanceSize];
74
+
54
75
  if (isResync) {
55
76
  console.log(`\n Resyncing ${appName} on ${config.platform} (${config.region})\n`);
56
77
  } else {
57
- console.log(`\n Deploying ${appName} to ${config.platform} (${config.region})\n`);
78
+ console.log(`\n Deploying ${appName} to ${config.platform} (${config.region})`);
79
+ if (cost) {
80
+ console.log(` Instance: ${instanceSize} (${cost.type}) — ${cost.monthly}/mo`);
81
+ }
82
+ console.log();
58
83
  }
59
84
 
85
+ const totalSteps = config.build?.command ? 5 : 4;
86
+ let step = 0;
87
+ const stepLabel = (label) => `[${++step}/${totalSteps}] ${label}`;
88
+
60
89
  // Step 1: Build
61
90
  if (config.build?.command) {
62
- const spinner = createSpinner('Building').start();
91
+ const spinner = createSpinner(stepLabel('Building')).start();
63
92
  try {
64
93
  execSync(config.build.command, {
65
94
  cwd: process.cwd(),
@@ -75,7 +104,7 @@ export async function deploy(flags) {
75
104
  }
76
105
 
77
106
  // Step 2: Create archive
78
- const archiveSpinner = createSpinner('Creating archive').start();
107
+ const archiveSpinner = createSpinner(stepLabel('Creating archive')).start();
79
108
  let tarball;
80
109
  try {
81
110
  tarball = createTarball(process.cwd(), config.ignore);
@@ -85,8 +114,8 @@ export async function deploy(flags) {
85
114
  throw err;
86
115
  }
87
116
 
88
- // Step 3: Upload to Supabase storage via Deno function
89
- const uploadSpinner = createSpinner('Uploading').start();
117
+ // Step 3: Upload
118
+ const uploadSpinner = createSpinner(stepLabel('Uploading')).start();
90
119
  let uploadResult;
91
120
  try {
92
121
  uploadResult = await uploadFile(apiKey, tarball.buffer, `${appName}.tar.gz`);
@@ -96,8 +125,8 @@ export async function deploy(flags) {
96
125
  throw err;
97
126
  }
98
127
 
99
- // Step 4: Trigger deploy via Deno function
100
- const deploySpinner = createSpinner(isResync ? 'Resyncing' : 'Deploying').start();
128
+ // Step 4: Trigger deploy
129
+ const deploySpinner = createSpinner(stepLabel(isResync ? 'Resyncing' : 'Deploying')).start();
101
130
  let deployResult;
102
131
  try {
103
132
  deployResult = await apiRequest('/cli/deploy', {
@@ -118,13 +147,14 @@ export async function deploy(flags) {
118
147
  throw err;
119
148
  }
120
149
 
121
- // Step 5: Poll for status via Deno function
150
+ // Step 5: Poll for status
122
151
  const deployId = deployResult.deploy_id || appName;
123
- const dashboardUrl = `https://nometria.com/AppDetails?app_id=${deployId}`;
152
+ const dashboardUrl = `${DASHBOARD_URL}/AppDetails?app_id=${deployId}`;
124
153
  console.log(`\n Dashboard: ${dashboardUrl}`);
125
154
  console.log(` You can close this terminal — check the dashboard for progress.\n`);
126
155
 
127
156
  let finalStatus;
157
+ let consecutiveErrors = 0;
128
158
  const pollStart = Date.now();
129
159
  const POLL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
130
160
  while (true) {
@@ -134,39 +164,56 @@ export async function deploy(flags) {
134
164
  apiKey,
135
165
  body: { app_id: deployId },
136
166
  });
137
- // statusResult is already unwrapped by apiRequest (raw.data),
138
- // so fields are at top level, not nested under .data
167
+ consecutiveErrors = 0; // reset on success
168
+
169
+ // statusResult may be unwrapped (top-level fields) or nested under .data
139
170
  const deployStatus = statusResult.deploymentStatus || statusResult.data?.deploymentStatus;
140
171
  const instanceState = statusResult.instanceState || statusResult.data?.instanceState;
141
172
  const topStatus = statusResult.status;
142
- // Display status: prefer deploymentStatus (accurate), fall back to instanceState
143
173
  const st = deployStatus || instanceState || topStatus || 'unknown';
144
174
  deploySpinner.update(`${isResync ? 'Resyncing' : 'Deploying'} — ${st}`);
145
175
 
146
- // Only trust deploymentStatus for completion — instanceState='running' just means
147
- // EC2 booted, and topStatus='deployed' just means the record exists in DB.
148
- // The sync script calls report_status("running") only after everything finishes.
149
176
  const isDone = deployStatus === 'completed' || deployStatus === 'running';
150
177
  if (isDone) {
151
178
  finalStatus = statusResult;
152
179
  deploySpinner.succeed(isResync ? 'Resynced successfully' : 'Deployed successfully');
180
+ telemetry.success({ instanceType: instanceSize });
153
181
  break;
154
182
  }
155
183
  if (deployStatus === 'failed' || st === 'failed') {
156
184
  deploySpinner.fail(`${isResync ? 'Resync' : 'Deploy'} failed: ${statusResult.errorMessage || statusResult.data?.errorMessage || 'unknown error'}`);
185
+ console.error(`\n View logs: nom logs`);
186
+ console.error(` Dashboard: ${dashboardUrl}`);
187
+ console.error(` Help: ${HELP_URL}/deploy/overview\n`);
157
188
  process.exit(1);
158
189
  }
159
190
 
160
- // Timeout: if instance is running but status stuck at deploying, show URL and exit
191
+ // Timeout: if instance is running but status stuck at deploying, show URL and exit gracefully
161
192
  if (Date.now() - pollStart > POLL_TIMEOUT_MS && instanceState === 'running') {
162
193
  finalStatus = statusResult;
163
194
  deploySpinner.succeed('Deploy in progress — check dashboard for final status');
164
195
  break;
165
196
  }
166
- } catch {
167
- // Keep polling on transient errors
197
+ } catch (err) {
198
+ consecutiveErrors++;
199
+ if (consecutiveErrors >= 5) {
200
+ deploySpinner.fail(`Lost connection while polling (${consecutiveErrors} consecutive errors)`);
201
+ console.error(`\n Last error: ${err.message}`);
202
+ console.error(` Check status manually: nom status`);
203
+ console.error(` Dashboard: ${dashboardUrl}`);
204
+ console.error(` Help: ${HELP_URL}/deploy/overview\n`);
205
+ process.exit(1);
206
+ }
207
+ deploySpinner.update(`${isResync ? 'Resyncing' : 'Deploying'} — retrying (${consecutiveErrors}/5)...`);
168
208
  }
169
209
  }
210
+ if (!finalStatus) {
211
+ deploySpinner.fail('Timed out waiting for deployment');
212
+ console.error(`\n The deploy may still be in progress. Check status:`);
213
+ console.error(` nom status`);
214
+ console.error(` Dashboard: ${dashboardUrl}\n`);
215
+ process.exit(1);
216
+ }
170
217
 
171
218
  // Step 6: Write app_id and migration_id back to nometria.json
172
219
  const updates = {};
@@ -178,11 +225,26 @@ export async function deploy(flags) {
178
225
 
179
226
  // Step 7: Print result
180
227
  const url = finalStatus.deployUrl || finalStatus.data?.deployUrl || finalStatus.url || `https://${deployId}.ownmy.app`;
228
+ const instanceInfo = finalStatus.instanceType || finalStatus.data?.instanceType || instanceSize;
181
229
  console.log(`
182
- Live at: ${url}
183
- Dashboard: https://nometria.com/AppDetails?app_id=${deployId}
230
+ ${isResync ? 'Resynced' : 'Deployed'} successfully!
231
+
232
+ URL: ${url}
233
+ Instance: ${instanceInfo}${cost ? ` (${cost.monthly}/mo)` : ''}
234
+ Dashboard: ${dashboardUrl}
184
235
  `);
185
236
 
237
+ // First deploy — show next steps
238
+ if (!isResync) {
239
+ console.log(` Next steps:`);
240
+ console.log(` nom github connect Connect GitHub for auto-deploy`);
241
+ console.log(` nom domain add <url> Add a custom domain`);
242
+ console.log(` nom env set KEY=VAL Set environment variables`);
243
+ console.log(` nom logs -f Watch live logs`);
244
+ console.log(` nom scan Run security scan`);
245
+ console.log();
246
+ }
247
+
186
248
  // Step 8: Auto-detect git repo and offer GitHub connection
187
249
  if (!flags.yes) {
188
250
  const hasGit = existsSync(join(process.cwd(), '.git'));
@@ -206,3 +268,71 @@ export async function deploy(flags) {
206
268
  function sleep(ms) {
207
269
  return new Promise(resolve => setTimeout(resolve, ms));
208
270
  }
271
+
272
+ async function dryRun(config, apiKey, appName) {
273
+ const instanceSize = config.instanceType || '4gb';
274
+ const cost = INSTANCE_COSTS[instanceSize];
275
+
276
+ console.log(`\n Dry run for: ${appName}`);
277
+ console.log(` ─────────────────────────────────\n`);
278
+
279
+ // 1. Config check
280
+ console.log(` Config: nometria.json`);
281
+ console.log(` Framework: ${config.framework || 'unknown'}`);
282
+ console.log(` Platform: ${config.platform || 'aws'} (${config.region || 'us-east-1'})`);
283
+ console.log(` Instance: ${instanceSize}${cost ? ` (${cost.type}) — ${cost.monthly}/mo` : ''}`);
284
+ console.log(` App ID: ${config.app_id || '(new — will be created)'}`);
285
+ console.log();
286
+
287
+ // 2. Auth check
288
+ console.log(` Checking auth...`);
289
+ try {
290
+ const authResult = await apiRequest('/cli/auth', { body: { api_key: apiKey } });
291
+ console.log(` Auth: OK (${authResult.email || 'authenticated'})`);
292
+ } catch (err) {
293
+ console.log(` Auth: FAILED — ${err.message}`);
294
+ console.log(` Get a key: https://nometria.com/settings/api-keys\n`);
295
+ process.exit(1);
296
+ }
297
+
298
+ // 3. Build check
299
+ if (config.build?.command) {
300
+ console.log(` Testing build: ${config.build.command}`);
301
+ try {
302
+ execSync(config.build.command, {
303
+ cwd: process.cwd(),
304
+ stdio: 'pipe',
305
+ env: { ...process.env, NODE_ENV: 'production' },
306
+ });
307
+ console.log(` Build: PASSED`);
308
+ } catch (err) {
309
+ console.log(` Build: FAILED`);
310
+ console.error(`\n${err.stderr?.toString().slice(0, 500) || err.message}\n`);
311
+ process.exit(1);
312
+ }
313
+ } else {
314
+ console.log(` Build: (no build command)`);
315
+ }
316
+
317
+ // 4. Archive size estimate
318
+ try {
319
+ const tarball = createTarball(process.cwd(), config.ignore);
320
+ console.log(` Archive: ${tarball.sizeFormatted}`);
321
+ } catch {
322
+ console.log(` Archive: (could not estimate)`);
323
+ }
324
+
325
+ // 5. Status check
326
+ if (config.app_id) {
327
+ try {
328
+ const status = await apiRequest('/checkAwsStatus', { apiKey, body: { app_id: config.app_id } });
329
+ const state = status.instanceState || status.data?.instanceState || status.status || 'unknown';
330
+ console.log(` Instance: ${state}`);
331
+ } catch {
332
+ console.log(` Instance: (could not check)`);
333
+ }
334
+ }
335
+
336
+ console.log(`\n Dry run complete. Ready to deploy.`);
337
+ console.log(` Run: nom deploy\n`);
338
+ }
@@ -5,11 +5,20 @@
5
5
  * set KEY=VALUE [KEY=VALUE ...] Set environment variables
6
6
  * list List variable names
7
7
  * delete KEY [KEY ...] Delete variables
8
+ *
9
+ * Scope flags:
10
+ * --preview Target preview/staging environment
11
+ * --production Target production environment (default)
8
12
  */
9
13
  import { readConfig } from '../lib/config.js';
10
14
  import { requireApiKey } from '../lib/auth.js';
11
15
  import { apiRequest } from '../lib/api.js';
12
16
 
17
+ function getScope(flags) {
18
+ if (flags.preview) return 'preview';
19
+ return 'production'; // default
20
+ }
21
+
13
22
  export async function env(flags, positionals) {
14
23
  const sub = positionals[0];
15
24
 
@@ -20,25 +29,47 @@ export async function env(flags, positionals) {
20
29
  return envList(flags);
21
30
  case 'delete':
22
31
  return envDelete(flags, positionals.slice(1));
32
+ case 'compare':
33
+ return envCompare(flags);
34
+ case 'validate':
35
+ return envValidate(flags);
23
36
  default:
24
37
  console.log(`
25
- Usage: nom env <command>
38
+ Usage: nom env <command> [options]
26
39
 
27
40
  Commands:
28
41
  set KEY=VALUE [...] Set environment variables
29
42
  list List variable names
30
43
  delete KEY [...] Delete variables
44
+ compare Compare preview vs production env vars
45
+ validate Check if required env vars are set
46
+
47
+ Options:
48
+ --preview Target preview/staging environment
49
+ --production Target production environment (default)
31
50
  `);
32
51
  }
33
52
  }
34
53
 
54
+ const SECRET_PATTERNS = [
55
+ /^sk[-_]/i, /^pk[-_]/i, /secret/i, /password/i, /token/i,
56
+ /api[-_]?key/i, /private[-_]?key/i, /^ghp_/, /^gho_/, /^nometria_sk_/,
57
+ ];
58
+
59
+ function looksLikeSecret(key, value) {
60
+ if (SECRET_PATTERNS.some(p => p.test(key))) return true;
61
+ if (value && value.length > 20 && /^[A-Za-z0-9+/=_-]+$/.test(value)) return true;
62
+ return false;
63
+ }
64
+
35
65
  async function envSet(flags, pairs) {
36
66
  const apiKey = requireApiKey();
37
67
  const config = readConfig();
38
68
  const appId = config.app_id || config.name;
69
+ const scope = getScope(flags);
39
70
 
40
71
  if (!pairs.length) {
41
- console.error('\n Usage: nom env set KEY=VALUE [KEY=VALUE ...]\n');
72
+ console.error('\n Usage: nom env set KEY=VALUE [KEY=VALUE ...] [--preview|--production]\n');
42
73
  process.exit(1);
43
74
  }
44
75
 
@@ -51,12 +82,17 @@ async function envSet(flags, pairs) {
51
82
  }
52
83
  const key = pair.slice(0, eqIndex);
53
84
  const value = pair.slice(eqIndex + 1);
85
+
86
+ // Warn about secrets
87
+ if (looksLikeSecret(key, value)) {
88
+ console.log(` Warning: "${key}" looks like a secret. Make sure it's not in nometria.json or git.`);
89
+ }
54
90
  vars[key] = value;
55
91
  }
56
92
 
57
93
  const result = await apiRequest('/cli/env', {
58
94
  apiKey,
59
- body: { api_key: apiKey, app_id: appId, action: 'set', vars },
95
+ body: { api_key: apiKey, app_id: appId, action: 'set', vars, scope },
60
96
  });
61
97
 
62
98
  if (flags.json) {
@@ -65,17 +101,18 @@ async function envSet(flags, pairs) {
65
101
  }
66
102
 
67
103
  const keys = Object.keys(vars);
68
- console.log(`\n Set ${keys.length} variable${keys.length === 1 ? '' : 's'}: ${keys.join(', ')}\n`);
104
+ console.log(`\n Set ${keys.length} variable${keys.length === 1 ? '' : 's'} (${scope}): ${keys.join(', ')}\n`);
69
105
  }
70
106
 
71
107
  async function envList(flags) {
72
108
  const apiKey = requireApiKey();
73
109
  const config = readConfig();
74
110
  const appId = config.app_id || config.name;
111
+ const scope = getScope(flags);
75
112
 
76
113
  const result = await apiRequest('/cli/env', {
77
114
  apiKey,
78
- body: { api_key: apiKey, app_id: appId, action: 'list' },
115
+ body: { api_key: apiKey, app_id: appId, action: 'list', scope },
79
116
  });
80
117
 
81
118
  if (flags.json) {
@@ -85,11 +122,11 @@ async function envList(flags) {
85
122
 
86
123
  const vars = result.keys || result.vars || [];
87
124
  if (!vars.length) {
88
- console.log('\n No environment variables set.\n');
125
+ console.log(`\n No environment variables set (${scope}).\n`);
89
126
  return;
90
127
  }
91
128
 
92
- console.log('\n Environment variables:\n');
129
+ console.log(`\n Environment variables (${scope}):\n`);
93
130
  for (const name of vars) {
94
131
  console.log(` ${name}`);
95
132
  }
@@ -100,15 +137,16 @@ async function envDelete(flags, keys) {
100
137
  const apiKey = requireApiKey();
101
138
  const config = readConfig();
102
139
  const appId = config.app_id || config.name;
140
+ const scope = getScope(flags);
103
141
 
104
142
  if (!keys.length) {
105
- console.error('\n Usage: nom env delete KEY [KEY ...]\n');
143
+ console.error('\n Usage: nom env delete KEY [KEY ...] [--preview|--production]\n');
106
144
  process.exit(1);
107
145
  }
108
146
 
109
147
  const result = await apiRequest('/cli/env', {
110
148
  apiKey,
111
- body: { api_key: apiKey, app_id: appId, action: 'delete', vars: keys },
149
+ body: { api_key: apiKey, app_id: appId, action: 'delete', vars: keys, scope },
112
150
  });
113
151
 
114
152
  if (flags.json) {
@@ -116,5 +154,116 @@ async function envDelete(flags, keys) {
116
154
  return;
117
155
  }
118
156
 
119
- console.log(`\n Deleted ${keys.length} variable${keys.length === 1 ? '' : 's'}: ${keys.join(', ')}\n`);
157
+ console.log(`\n Deleted ${keys.length} variable${keys.length === 1 ? '' : 's'} (${scope}): ${keys.join(', ')}\n`);
158
+ }
159
+
160
+ async function envCompare(flags) {
161
+ const apiKey = requireApiKey();
162
+ const config = readConfig();
163
+ const appId = config.app_id || config.name;
164
+
165
+ const [prodResult, prevResult] = await Promise.all([
166
+ apiRequest('/cli/env', { apiKey, body: { api_key: apiKey, app_id: appId, action: 'list', scope: 'production' } }),
167
+ apiRequest('/cli/env', { apiKey, body: { api_key: apiKey, app_id: appId, action: 'list', scope: 'preview' } }),
168
+ ]);
169
+
170
+ if (flags.json) {
171
+ console.log(JSON.stringify({ production: prodResult, preview: prevResult }, null, 2));
172
+ return;
173
+ }
174
+
175
+ const prodVars = new Set(prodResult.keys || prodResult.vars || []);
176
+ const prevVars = new Set(prevResult.keys || prevResult.vars || []);
177
+ const allKeys = new Set([...prodVars, ...prevVars]);
178
+
179
+ if (!allKeys.size) {
180
+ console.log('\n No environment variables set in either environment.\n');
181
+ return;
182
+ }
183
+
184
+ console.log('\n Environment variable comparison:\n');
185
+ console.log(' Key Production Preview');
186
+ console.log(' ───────────────────────── ────────── ───────');
187
+ for (const key of [...allKeys].sort()) {
188
+ const inProd = prodVars.has(key) ? 'set' : '—';
189
+ const inPrev = prevVars.has(key) ? 'set' : '—';
190
+ const marker = inProd !== inPrev ? ' *' : '';
191
+ console.log(` ${key.padEnd(27)} ${inProd.padEnd(12)}${inPrev}${marker}`);
192
+ }
193
+ console.log('\n * = different between environments\n');
194
+ }
195
+
196
+ async function envValidate(flags) {
197
+ const apiKey = requireApiKey();
198
+ const config = readConfig();
199
+ const appId = config.app_id || config.name;
200
+ const scope = getScope(flags);
201
+
202
+ // Collect expected vars from local .env and nometria.json
203
+ const { existsSync, readFileSync } = await import('node:fs');
204
+ const { join } = await import('node:path');
205
+
206
+ const expectedKeys = new Set();
207
+
208
+ // From local .env
209
+ const envPath = join(process.cwd(), '.env');
210
+ if (existsSync(envPath)) {
211
+ const lines = readFileSync(envPath, 'utf8').split('\n');
212
+ for (const line of lines) {
213
+ const match = line.match(/^([A-Z_][A-Z0-9_]*)=/);
214
+ if (match) expectedKeys.add(match[1]);
215
+ }
216
+ }
217
+
218
+ // From nometria.json env section
219
+ if (config.env && typeof config.env === 'object') {
220
+ for (const key of Object.keys(config.env)) {
221
+ expectedKeys.add(key);
222
+ }
223
+ }
224
+
225
+ if (!expectedKeys.size) {
226
+ console.log('\n No expected environment variables found in .env or nometria.json.\n');
227
+ return;
228
+ }
229
+
230
+ // Get deployed vars
231
+ const result = await apiRequest('/cli/env', {
232
+ apiKey,
233
+ body: { api_key: apiKey, app_id: appId, action: 'list', scope },
234
+ });
235
+ const deployedKeys = new Set(result.keys || result.vars || []);
236
+
237
+ // Compare
238
+ const missing = [...expectedKeys].filter(k => !deployedKeys.has(k));
239
+ const extra = [...deployedKeys].filter(k => !expectedKeys.has(k));
240
+
241
+ console.log(`\n Environment validation (${scope}):\n`);
242
+ console.log(` Local vars: ${expectedKeys.size}`);
243
+ console.log(` Deployed vars: ${deployedKeys.size}`);
244
+
245
+ if (missing.length) {
246
+ console.log(`\n Missing on ${scope} (${missing.length}):`);
247
+ for (const key of missing) {
248
+ console.log(` - ${key}`);
249
+ }
250
+ }
251
+
252
+ if (extra.length) {
253
+ console.log(`\n Extra on ${scope} (not in local .env):`);
254
+ for (const key of extra) {
255
+ console.log(` + ${key}`);
256
+ }
257
+ }
258
+
259
+ if (!missing.length && !extra.length) {
260
+ console.log('\n All environment variables match.');
261
+ }
262
+
263
+ console.log();
264
+
265
+ if (missing.length) {
266
+ console.log(` Fix: nom env set ${missing.map(k => `${k}=<value>`).join(' ')}\n`);
267
+ process.exit(1);
268
+ }
120
269
  }
@@ -2,8 +2,9 @@
2
2
  * nom init — Create nometria.json config interactively
3
3
  */
4
4
  import { writeFileSync } from 'node:fs';
5
+ import { execSync } from 'node:child_process';
5
6
  import { join, basename } from 'node:path';
6
- import { detectFramework, detectPackageManager, detectServices } from '../lib/detect.js';
7
+ import { detectFramework, detectPackageManager, detectServices, detectMonorepo } from '../lib/detect.js';
7
8
  import { configExists, CONFIG_FILE, VALID_PLATFORMS } from '../lib/config.js';
8
9
  import { ask, choose, confirm } from '../lib/prompt.js';
9
10
 
@@ -30,10 +31,42 @@ export async function init(flags) {
30
31
 
31
32
  console.log('\n Setting up your project for deployment\n');
32
33
 
34
+ // Detect monorepo
35
+ const mono = detectMonorepo(dir);
36
+ if (mono.isMonorepo && mono.packages.length > 0 && !flags.yes) {
37
+ console.log(` Monorepo detected (${mono.tool}). Deployable packages:`);
38
+ for (let i = 0; i < mono.packages.length; i++) {
39
+ console.log(` ${i + 1}. ${mono.packages[i]}`);
40
+ }
41
+ console.log();
42
+ }
43
+
33
44
  // Detect framework and services
34
- const detected = detectFramework(dir);
45
+ let detected = detectFramework(dir);
35
46
  const pkgManager = detectPackageManager(dir);
36
47
  const { services, docker_compose } = detectServices(dir);
48
+
49
+ // If detection is uncertain, warn and let user override
50
+ if (detected.uncertain && !flags.yes) {
51
+ console.log(` Could not confidently detect framework (defaulting to "static").`);
52
+ const frameworks = ['static', 'vite', 'nextjs', 'remix', 'astro', 'sveltekit', 'nuxt', 'node'];
53
+ const choice = await choose('What framework is this project?', frameworks, 0);
54
+ if (choice !== 'static') {
55
+ detected = detectFramework(dir); // re-detect won't help, set manually
56
+ detected.framework = choice;
57
+ detected.uncertain = false;
58
+ if (['vite', 'astro', 'sveltekit'].includes(choice)) {
59
+ detected.build = { command: 'npm run build', output: choice === 'sveltekit' ? 'build' : 'dist' };
60
+ } else if (choice === 'nextjs') {
61
+ detected.build = { command: 'npm run build', output: '.next' };
62
+ } else if (choice === 'nuxt') {
63
+ detected.build = { command: 'npm run build', output: '.output' };
64
+ } else if (choice === 'remix') {
65
+ detected.build = { command: 'npm run build', output: 'build' };
66
+ }
67
+ }
68
+ }
69
+
37
70
  console.log(` Detected: ${detected.framework} (${pkgManager})`);
38
71
  if (services.length > 0) {
39
72
  console.log(` Services: ${services.map(s => `${s.name} (${s.type})`).join(', ')}`);
@@ -90,6 +123,21 @@ export async function init(flags) {
90
123
 
91
124
  console.log(`\n Created ${CONFIG_FILE}`);
92
125
 
126
+ // Validate build if a build command exists
127
+ if (buildCmd && !flags.yes) {
128
+ const shouldValidate = await confirm('Test the build command now?', true);
129
+ if (shouldValidate) {
130
+ console.log(`\n Running: ${buildCmd}`);
131
+ try {
132
+ execSync(buildCmd, { cwd: dir, stdio: 'inherit', env: { ...process.env, NODE_ENV: 'production' } });
133
+ console.log(`\n Build passed.\n`);
134
+ } catch {
135
+ console.log(`\n Build failed. Fix the errors above, then run: nom deploy`);
136
+ console.log(` Help: https://docs.nometria.com/deploy/overview\n`);
137
+ }
138
+ }
139
+ }
140
+
93
141
  // Auto-generate AI tool configs
94
142
  const { setup } = await import('./setup.js');
95
143
  await setup({ yes: true });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * nom list — List all your deployed apps.
3
+ */
4
+ import { requireApiKey } from '../lib/auth.js';
5
+ import { apiRequest } from '../lib/api.js';
6
+ import { getCached, setCache } from '../lib/cache.js';
7
+
8
+ export async function list(flags) {
9
+ const apiKey = requireApiKey();
10
+
11
+ // Check cache first (1 minute TTL), skip with --no-cache
12
+ const noCache = flags['no-cache'] || flags.noCache;
13
+ if (!noCache) {
14
+ const cached = getCached('list_apps');
15
+ if (cached) {
16
+ const migrations = cached.migrations || cached || [];
17
+ printApps(migrations, flags);
18
+ console.log(' (cached — use --no-cache to refresh)\n');
19
+ return;
20
+ }
21
+ }
22
+
23
+ const result = await apiRequest('/listUserMigrations', { apiKey, body: {} });
24
+ const migrations = result.migrations || result || [];
25
+ setCache('list_apps', result);
26
+
27
+ printApps(migrations, flags);
28
+ }
29
+
30
+ function printApps(migrations, flags) {
31
+ if (!migrations.length) {
32
+ console.log('\n No apps found. Deploy your first app: nom deploy\n');
33
+ return;
34
+ }
35
+
36
+ console.log(`\n Your apps (${migrations.length}):\n`);
37
+
38
+ if (flags.json) {
39
+ console.log(JSON.stringify(migrations, null, 2));
40
+ return;
41
+ }
42
+
43
+ for (const m of migrations) {
44
+ const status = m.delivery_type === 'hosting' ? (m.payment_status === 'paid' ? 'hosting' : 'unpaid') : m.delivery_type || 'download';
45
+ const platform = m.platform || '—';
46
+ const url = m.hosted_url || m.deploy_url || '—';
47
+ console.log(` ${m.app_name || m.app_id}`);
48
+ console.log(` Platform: ${platform} Status: ${status} URL: ${url}`);
49
+ console.log();
50
+ }
51
+ }