@nometria-ai/nom 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Nometria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # nom
2
+
3
+ Deploy any project to any cloud from your terminal. One command, zero config.
4
+
5
+ ```bash
6
+ npx @nometria-ai/nom deploy
7
+ ```
8
+
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ # 1. Authenticate
13
+ npx @nometria-ai/nom login
14
+
15
+ # 2. Set up your project
16
+ npx @nometria-ai/nom init
17
+
18
+ # 3. Deploy
19
+ npx @nometria-ai/nom deploy
20
+ ```
21
+
22
+ ## Commands
23
+
24
+ | Command | Description |
25
+ |---------|-------------|
26
+ | `nom login` | Authenticate with your API key |
27
+ | `nom init` | Create a `nometria.json` config file |
28
+ | `nom deploy` | Deploy to production |
29
+ | `nom preview` | Create a staging preview |
30
+ | `nom status` | Check deployment status |
31
+ | `nom logs` | View deployment logs |
32
+ | `nom logs -f` | Stream logs in real-time |
33
+ | `nom whoami` | Show current authenticated user |
34
+
35
+ ## Configuration
36
+
37
+ `nom init` creates a `nometria.json` in your project root:
38
+
39
+ ```json
40
+ {
41
+ "name": "my-app",
42
+ "framework": "vite",
43
+ "platform": "aws",
44
+ "region": "us-east-1",
45
+ "instanceType": "4gb",
46
+ "build": {
47
+ "command": "npm run build",
48
+ "output": "dist"
49
+ },
50
+ "env": {
51
+ "DATABASE_URL": "@env:DATABASE_URL"
52
+ },
53
+ "ignore": []
54
+ }
55
+ ```
56
+
57
+ ### Fields
58
+
59
+ | Field | Description | Default |
60
+ |-------|-------------|---------|
61
+ | `name` | Project name (becomes `{name}.ownmy.app`) | from package.json |
62
+ | `framework` | `vite`, `nextjs`, `remix`, `static`, `node` | auto-detected |
63
+ | `platform` | `aws`, `gcp`, `azure`, `digitalocean`, `hetzner`, `vercel` | `aws` |
64
+ | `region` | Cloud region | `us-east-1` |
65
+ | `instanceType` | `2gb`, `4gb`, `8gb`, `16gb` | `4gb` |
66
+ | `build.command` | Build command | auto-detected |
67
+ | `build.output` | Build output directory | auto-detected |
68
+ | `env` | Environment variables. Use `@env:VAR` to read from local env | `{}` |
69
+ | `ignore` | Extra patterns to exclude from upload | `[]` |
70
+
71
+ ## Authentication
72
+
73
+ Get an API key at [ownmy.app/settings/api-keys](https://ownmy.app/settings/api-keys).
74
+
75
+ ```bash
76
+ # Option 1: Login command (stores in ~/.nometria/credentials.json)
77
+ nom login
78
+
79
+ # Option 2: Environment variable
80
+ export NOMETRIA_API_KEY=nometria_sk_...
81
+ ```
82
+
83
+ ## Environment Variables
84
+
85
+ Use the `@env:` prefix in `nometria.json` to reference local environment variables. These are resolved at deploy time and never stored in the config file:
86
+
87
+ ```json
88
+ {
89
+ "env": {
90
+ "DATABASE_URL": "@env:DATABASE_URL",
91
+ "API_SECRET": "@env:MY_API_SECRET"
92
+ }
93
+ }
94
+ ```
95
+
96
+ ## Supported Platforms
97
+
98
+ - **AWS** (EC2 + Route53)
99
+ - **Google Cloud** (Compute Engine)
100
+ - **Azure** (Virtual Machines)
101
+ - **DigitalOcean** (Droplets)
102
+ - **Hetzner** (Cloud Servers)
103
+ - **Vercel** (Serverless)
104
+
105
+ ## Requirements
106
+
107
+ - Node.js 18+
108
+ - `tar` command (available by default on macOS/Linux/WSL)
109
+
110
+ ## License
111
+
112
+ MIT
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@nometria-ai/nom",
3
+ "version": "0.1.0",
4
+ "description": "Deploy any project to any cloud from your terminal. One command, zero config.",
5
+ "type": "module",
6
+ "bin": {
7
+ "nom": "./src/cli.js"
8
+ },
9
+ "files": [
10
+ "src/",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "test": "node --test tests/*.test.js"
16
+ },
17
+ "keywords": [
18
+ "deploy",
19
+ "cloud",
20
+ "aws",
21
+ "vercel",
22
+ "gcp",
23
+ "azure",
24
+ "digitalocean",
25
+ "hetzner",
26
+ "cli",
27
+ "nometria",
28
+ "hosting",
29
+ "devops"
30
+ ],
31
+ "license": "MIT",
32
+ "author": "Nometria <hello@nometria.com>",
33
+ "homepage": "https://github.com/nometria/deploy-cli#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/nometria/deploy-cli/issues"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/nometria/deploy-cli.git"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ }
47
+ }
package/src/cli.js ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * nom — Deploy any project to any cloud from your terminal.
4
+ *
5
+ * Commands:
6
+ * nom init Create a nometria.json config file
7
+ * nom deploy Deploy to production (default)
8
+ * nom preview Deploy a staging preview
9
+ * nom status Check deployment status
10
+ * nom logs View deployment logs
11
+ * nom login Authenticate with your API key
12
+ * nom whoami Show current authenticated user
13
+ */
14
+
15
+ import { parseArgs } from 'node:util';
16
+ import { init } from './commands/init.js';
17
+ import { deploy } from './commands/deploy.js';
18
+ import { preview } from './commands/preview.js';
19
+ import { status } from './commands/status.js';
20
+ import { logs } from './commands/logs.js';
21
+ import { login } from './commands/login.js';
22
+ import { whoami } from './commands/whoami.js';
23
+ import { github } from './commands/github.js';
24
+ import { start } from './commands/start.js';
25
+ import { stop } from './commands/stop.js';
26
+ import { terminate } from './commands/terminate.js';
27
+ import { upgrade } from './commands/upgrade.js';
28
+ import { domain } from './commands/domain.js';
29
+ import { env } from './commands/env.js';
30
+ import { scan } from './commands/scan.js';
31
+ import { setup } from './commands/setup.js';
32
+
33
+ const { values, positionals } = parseArgs({
34
+ allowPositionals: true,
35
+ options: {
36
+ help: { type: 'boolean', short: 'h', default: false },
37
+ version: { type: 'boolean', short: 'v', default: false },
38
+ yes: { type: 'boolean', short: 'y', default: false },
39
+ follow: { type: 'boolean', short: 'f', default: false },
40
+ json: { type: 'boolean', default: false },
41
+ from: { type: 'string' },
42
+ 'api-key': { type: 'boolean', default: false },
43
+ token: { type: 'boolean', default: false },
44
+ message: { type: 'string', short: 'm' },
45
+ },
46
+ strict: false,
47
+ });
48
+
49
+ if (values.version) {
50
+ const { readFileSync } = await import('node:fs');
51
+ const { fileURLToPath } = await import('node:url');
52
+ const { dirname, join } = await import('node:path');
53
+ const dir = dirname(fileURLToPath(import.meta.url));
54
+ const pkg = JSON.parse(readFileSync(join(dir, '..', 'package.json'), 'utf8'));
55
+ console.log(pkg.version);
56
+ process.exit(0);
57
+ }
58
+
59
+ const command = positionals[0] || 'deploy';
60
+
61
+ if (values.help) {
62
+ printHelp();
63
+ process.exit(0);
64
+ }
65
+
66
+ const commands = {
67
+ init,
68
+ deploy,
69
+ preview,
70
+ status,
71
+ logs,
72
+ login,
73
+ whoami,
74
+ github,
75
+ start,
76
+ stop,
77
+ terminate,
78
+ upgrade,
79
+ domain,
80
+ env,
81
+ scan,
82
+ setup,
83
+ };
84
+
85
+ const handler = commands[command];
86
+ if (!handler) {
87
+ console.error(`Unknown command: ${command}`);
88
+ printHelp();
89
+ process.exit(1);
90
+ }
91
+
92
+ try {
93
+ await handler(values, positionals.slice(1));
94
+ } catch (err) {
95
+ if (err.code === 'ERR_AUTH') {
96
+ console.error(`\n Not authenticated. Run: nom login\n`);
97
+ } else if (err.code === 'ERR_CONFIG') {
98
+ console.error(`\n No nometria.json found. Run: nom init\n`);
99
+ } else {
100
+ console.error(`\n Error: ${err.message}\n`);
101
+ if (process.env.NOM_DEBUG) console.error(err.stack);
102
+ }
103
+ process.exit(1);
104
+ }
105
+
106
+ function printHelp() {
107
+ console.log(`
108
+ nom — Deploy any project to any cloud.
109
+
110
+ Usage:
111
+ nom [command] [options]
112
+
113
+ Commands:
114
+ init Create a nometria.json config file
115
+ deploy Deploy to production (default)
116
+ preview Deploy a staging preview
117
+ status Check deployment status
118
+ logs [-f] View deployment logs
119
+ login Sign in via browser (or --api-key)
120
+ whoami Show current user
121
+
122
+ github connect Connect GitHub for auto-deploy
123
+ github status Check GitHub connection
124
+ github repos List connected repos
125
+ github push [-m] Push changes to GitHub
126
+
127
+ start Start a stopped instance
128
+ stop Stop a running instance
129
+ terminate Terminate instance permanently
130
+ upgrade <size> Upgrade instance (2gb/4gb/8gb/16gb)
131
+
132
+ domain add <domain> Add custom domain
133
+ env set KEY=VALUE Set environment variables
134
+ env list List environment variables
135
+ env delete KEY Delete environment variable
136
+ scan Run AI security scan
137
+
138
+ Options:
139
+ -h, --help Show this help
140
+ -v, --version Show version
141
+ -y, --yes Skip confirmation prompts
142
+ -f, --follow Follow logs in real-time
143
+ -m, --message Commit message (for github push)
144
+ --json Output as JSON
145
+ --api-key Login with API key paste instead of browser
146
+ --from <url> Deploy from a GitHub URL
147
+
148
+ Examples:
149
+ nom login Sign in via browser
150
+ nom init Set up a new project
151
+ nom deploy Deploy current directory
152
+ nom github connect Connect GitHub for auto-deploy
153
+ nom status Check deployment status
154
+ nom logs -f Stream live logs
155
+ nom upgrade 8gb Upgrade instance to 8gb
156
+ nom env set DB_URL=postgres://...
157
+ nom scan Run AI security scan
158
+ nom setup Generate AI tool configs (Cursor, Copilot, etc.)
159
+
160
+ Environment:
161
+ NOMETRIA_API_KEY API key (overrides stored credentials)
162
+ NOM_DEBUG Enable debug output
163
+ `);
164
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * nom deploy — Build, upload, and deploy to production.
3
+ * All calls go through Deno functions at app.ownmy.app.
4
+ */
5
+ import { execSync } from 'node:child_process';
6
+ import { existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { readConfig, resolveEnv, updateConfig } from '../lib/config.js';
9
+ import { requireApiKey } from '../lib/auth.js';
10
+ import { apiRequest, uploadFile } from '../lib/api.js';
11
+ import { createTarball } from '../lib/tar.js';
12
+ import { createSpinner } from '../lib/spinner.js';
13
+ import { confirm } from '../lib/prompt.js';
14
+
15
+ export async function deploy(flags) {
16
+ const apiKey = requireApiKey();
17
+ const config = readConfig();
18
+ const envVars = resolveEnv(config.env);
19
+ const appName = config.name || config.app_id;
20
+ const isResync = !!config.app_id;
21
+
22
+ if (isResync) {
23
+ console.log(`\n Resyncing ${appName} on ${config.platform} (${config.region})\n`);
24
+ } else {
25
+ console.log(`\n Deploying ${appName} to ${config.platform} (${config.region})\n`);
26
+ }
27
+
28
+ // Step 1: Build
29
+ if (config.build?.command) {
30
+ const spinner = createSpinner('Building').start();
31
+ try {
32
+ execSync(config.build.command, {
33
+ cwd: process.cwd(),
34
+ stdio: 'pipe',
35
+ env: { ...process.env, NODE_ENV: 'production' },
36
+ });
37
+ spinner.succeed('Built successfully');
38
+ } catch (err) {
39
+ spinner.fail('Build failed');
40
+ console.error(`\n${err.stderr?.toString() || err.message}\n`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ // Step 2: Create archive
46
+ const archiveSpinner = createSpinner('Creating archive').start();
47
+ let tarball;
48
+ try {
49
+ tarball = createTarball(process.cwd(), config.ignore);
50
+ archiveSpinner.succeed(`Archive created (${tarball.sizeFormatted})`);
51
+ } catch (err) {
52
+ archiveSpinner.fail('Failed to create archive');
53
+ throw err;
54
+ }
55
+
56
+ // Step 3: Upload to Supabase storage via Deno function
57
+ const uploadSpinner = createSpinner('Uploading').start();
58
+ let uploadResult;
59
+ try {
60
+ uploadResult = await uploadFile(apiKey, tarball.buffer, `${appName}.tar.gz`);
61
+ uploadSpinner.succeed(`Uploaded (${tarball.sizeFormatted})`);
62
+ } catch (err) {
63
+ uploadSpinner.fail('Upload failed');
64
+ throw err;
65
+ }
66
+
67
+ // Step 4: Trigger deploy via Deno function
68
+ const deploySpinner = createSpinner(isResync ? 'Resyncing' : 'Deploying').start();
69
+ let deployResult;
70
+ try {
71
+ deployResult = await apiRequest('/cli/deploy', {
72
+ apiKey,
73
+ body: {
74
+ app_name: appName,
75
+ upload_url: uploadResult.upload_url,
76
+ platform: config.platform,
77
+ region: config.region,
78
+ instance_type: config.instanceType || '4gb',
79
+ env_vars: envVars,
80
+ framework: config.framework,
81
+ ...(config.app_id ? { app_id: config.app_id } : {}),
82
+ },
83
+ });
84
+ } catch (err) {
85
+ deploySpinner.fail(isResync ? 'Resync failed' : 'Deploy request failed');
86
+ throw err;
87
+ }
88
+
89
+ // Step 5: Poll for status via Deno function
90
+ const deployId = deployResult.deploy_id || appName;
91
+ let finalStatus;
92
+ while (true) {
93
+ await sleep(3000);
94
+ try {
95
+ const statusResult = await apiRequest('/checkAwsStatus', {
96
+ apiKey,
97
+ body: { app_id: deployId },
98
+ });
99
+ const deployStatus = statusResult.data?.deploymentStatus;
100
+ const instanceState = statusResult.data?.instanceState;
101
+ const topStatus = statusResult.status;
102
+ const st = deployStatus || instanceState || topStatus || 'unknown';
103
+ deploySpinner.update(`${isResync ? 'Resyncing' : 'Deploying'} — ${st}`);
104
+
105
+ // "completed"/"running" from deploymentStatus/instanceState, or "deployed" from top-level
106
+ if (st === 'completed' || st === 'running' || topStatus === 'deployed' || instanceState === 'running') {
107
+ finalStatus = statusResult;
108
+ deploySpinner.succeed(isResync ? 'Resynced successfully' : 'Deployed successfully');
109
+ break;
110
+ }
111
+ if (st === 'failed') {
112
+ deploySpinner.fail(`${isResync ? 'Resync' : 'Deploy'} failed: ${statusResult.data?.errorMessage || 'unknown error'}`);
113
+ process.exit(1);
114
+ }
115
+ } catch {
116
+ // Keep polling on transient errors
117
+ }
118
+ }
119
+
120
+ // Step 6: Write app_id and migration_id back to nometria.json
121
+ const updates = {};
122
+ if (!config.app_id && deployId) updates.app_id = deployId;
123
+ if (deployResult.migration_id && !config.migration_id) updates.migration_id = deployResult.migration_id;
124
+ if (Object.keys(updates).length > 0) {
125
+ try { updateConfig(process.cwd(), updates); } catch { /* non-fatal */ }
126
+ }
127
+
128
+ // Step 7: Print result
129
+ const url = finalStatus.data?.deployUrl || finalStatus.url || `https://${appName}.ownmy.app`;
130
+ console.log(`
131
+ Live at: ${url}
132
+ Dashboard: https://ownmy.app/apps/${appName}
133
+ `);
134
+
135
+ // Step 8: Auto-detect git repo and offer GitHub connection
136
+ if (!flags.yes) {
137
+ const hasGit = existsSync(join(process.cwd(), '.git'));
138
+ if (hasGit) {
139
+ try {
140
+ const ghStatus = await apiRequest('/getUserGithubConnection', {
141
+ apiKey,
142
+ body: {},
143
+ });
144
+ if (!ghStatus.connected) {
145
+ const connectGh = await confirm('This project is a git repo. Connect GitHub for auto-deploy?');
146
+ if (connectGh) {
147
+ console.log(' Run: nom github connect\n');
148
+ }
149
+ }
150
+ } catch { /* non-fatal — github status check failed */ }
151
+ }
152
+ }
153
+ }
154
+
155
+ function sleep(ms) {
156
+ return new Promise(resolve => setTimeout(resolve, ms));
157
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * nom domain — Manage custom domains via Deno functions.
3
+ *
4
+ * Subcommands:
5
+ * add <domain> — Add a custom domain to the app
6
+ */
7
+ import { readConfig } from '../lib/config.js';
8
+ import { requireApiKey } from '../lib/auth.js';
9
+ import { apiRequest } from '../lib/api.js';
10
+
11
+ export async function domain(flags, positionals) {
12
+ const sub = positionals[0];
13
+
14
+ switch (sub) {
15
+ case 'add':
16
+ return domainAdd(flags, positionals);
17
+ default:
18
+ console.log(`
19
+ Usage: nom domain <command>
20
+
21
+ Commands:
22
+ add <domain> Add a custom domain (e.g. nom domain add example.com)
23
+ `);
24
+ }
25
+ }
26
+
27
+ async function domainAdd(flags, positionals) {
28
+ const apiKey = requireApiKey();
29
+ const config = readConfig();
30
+ const appId = config.app_id || config.name;
31
+
32
+ const customDomain = positionals[1];
33
+ if (!customDomain) {
34
+ console.error('\n Usage: nom domain add <domain>\n');
35
+ process.exit(1);
36
+ }
37
+
38
+ const result = await apiRequest('/addCustomDomain', {
39
+ apiKey,
40
+ body: { app_id: appId, custom_domain: customDomain },
41
+ });
42
+
43
+ if (flags.json) {
44
+ console.log(JSON.stringify(result, null, 2));
45
+ return;
46
+ }
47
+
48
+ if (result.success) {
49
+ console.log(`\n Domain "${customDomain}" added.`);
50
+ if (result.cname) console.log(` CNAME: ${result.cname}`);
51
+ if (result.instructions) console.log(` ${result.instructions}`);
52
+ console.log();
53
+ } else {
54
+ console.error(`\n Failed to add domain: ${result.error || 'Unknown error'}\n`);
55
+ process.exit(1);
56
+ }
57
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * nom env — Manage environment variables via Deno functions.
3
+ *
4
+ * Subcommands:
5
+ * set KEY=VALUE [KEY=VALUE ...] Set environment variables
6
+ * list List variable names
7
+ * delete KEY [KEY ...] Delete variables
8
+ */
9
+ import { readConfig } from '../lib/config.js';
10
+ import { requireApiKey } from '../lib/auth.js';
11
+ import { apiRequest } from '../lib/api.js';
12
+
13
+ export async function env(flags, positionals) {
14
+ const sub = positionals[0];
15
+
16
+ switch (sub) {
17
+ case 'set':
18
+ return envSet(flags, positionals.slice(1));
19
+ case 'list':
20
+ return envList(flags);
21
+ case 'delete':
22
+ return envDelete(flags, positionals.slice(1));
23
+ default:
24
+ console.log(`
25
+ Usage: nom env <command>
26
+
27
+ Commands:
28
+ set KEY=VALUE [...] Set environment variables
29
+ list List variable names
30
+ delete KEY [...] Delete variables
31
+ `);
32
+ }
33
+ }
34
+
35
+ async function envSet(flags, pairs) {
36
+ const apiKey = requireApiKey();
37
+ const config = readConfig();
38
+ const appId = config.app_id || config.name;
39
+
40
+ if (!pairs.length) {
41
+ console.error('\n Usage: nom env set KEY=VALUE [KEY=VALUE ...]\n');
42
+ process.exit(1);
43
+ }
44
+
45
+ const vars = {};
46
+ for (const pair of pairs) {
47
+ const eqIndex = pair.indexOf('=');
48
+ if (eqIndex === -1) {
49
+ console.error(`\n Invalid format: "${pair}". Use KEY=VALUE.\n`);
50
+ process.exit(1);
51
+ }
52
+ const key = pair.slice(0, eqIndex);
53
+ const value = pair.slice(eqIndex + 1);
54
+ vars[key] = value;
55
+ }
56
+
57
+ const result = await apiRequest('/cli/env', {
58
+ apiKey,
59
+ body: { api_key: apiKey, app_id: appId, action: 'set', vars },
60
+ });
61
+
62
+ if (flags.json) {
63
+ console.log(JSON.stringify(result, null, 2));
64
+ return;
65
+ }
66
+
67
+ const keys = Object.keys(vars);
68
+ console.log(`\n Set ${keys.length} variable${keys.length === 1 ? '' : 's'}: ${keys.join(', ')}\n`);
69
+ }
70
+
71
+ async function envList(flags) {
72
+ const apiKey = requireApiKey();
73
+ const config = readConfig();
74
+ const appId = config.app_id || config.name;
75
+
76
+ const result = await apiRequest('/cli/env', {
77
+ apiKey,
78
+ body: { api_key: apiKey, app_id: appId, action: 'list' },
79
+ });
80
+
81
+ if (flags.json) {
82
+ console.log(JSON.stringify(result, null, 2));
83
+ return;
84
+ }
85
+
86
+ const vars = result.keys || result.vars || [];
87
+ if (!vars.length) {
88
+ console.log('\n No environment variables set.\n');
89
+ return;
90
+ }
91
+
92
+ console.log('\n Environment variables:\n');
93
+ for (const name of vars) {
94
+ console.log(` ${name}`);
95
+ }
96
+ console.log();
97
+ }
98
+
99
+ async function envDelete(flags, keys) {
100
+ const apiKey = requireApiKey();
101
+ const config = readConfig();
102
+ const appId = config.app_id || config.name;
103
+
104
+ if (!keys.length) {
105
+ console.error('\n Usage: nom env delete KEY [KEY ...]\n');
106
+ process.exit(1);
107
+ }
108
+
109
+ const result = await apiRequest('/cli/env', {
110
+ apiKey,
111
+ body: { api_key: apiKey, app_id: appId, action: 'delete', vars: keys },
112
+ });
113
+
114
+ if (flags.json) {
115
+ console.log(JSON.stringify(result, null, 2));
116
+ return;
117
+ }
118
+
119
+ console.log(`\n Deleted ${keys.length} variable${keys.length === 1 ? '' : 's'}: ${keys.join(', ')}\n`);
120
+ }