@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 +21 -0
- package/README.md +112 -0
- package/package.json +47 -0
- package/src/cli.js +164 -0
- package/src/commands/deploy.js +157 -0
- package/src/commands/domain.js +57 -0
- package/src/commands/env.js +120 -0
- package/src/commands/github.js +291 -0
- package/src/commands/init.js +90 -0
- package/src/commands/login.js +280 -0
- package/src/commands/logs.js +49 -0
- package/src/commands/preview.js +60 -0
- package/src/commands/scan.js +70 -0
- package/src/commands/setup.js +854 -0
- package/src/commands/start.js +26 -0
- package/src/commands/status.js +33 -0
- package/src/commands/stop.js +24 -0
- package/src/commands/terminate.js +33 -0
- package/src/commands/upgrade.js +49 -0
- package/src/commands/whoami.js +18 -0
- package/src/lib/api.js +85 -0
- package/src/lib/auth.js +56 -0
- package/src/lib/config.js +93 -0
- package/src/lib/detect.js +88 -0
- package/src/lib/prompt.js +76 -0
- package/src/lib/spinner.js +43 -0
- package/src/lib/tar.js +55 -0
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
|
+
}
|