diff --git a/CHANGELOG.md b/CHANGELOG.md
index c178b02..d89fdec 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,10 @@
-## [1.0.11](https://github.com/niledatabase/cli/compare/v1.0.10...v1.0.11) (2025-02-07)
+# [1.1.0-alpha.1](https://github.com/niledatabase/cli/compare/v1.0.11-alpha.1...v1.1.0-alpha.1) (2025-02-22)
-### Bug Fixes
+### Features
-* fix package-lock ([436303b](https://github.com/niledatabase/cli/commit/436303b5eee3ae33a6733d5801978356ddaad922))
-* table format, stack trace on debug mode ([b5cb0d4](https://github.com/niledatabase/cli/commit/b5cb0d4b01afed59ffb562c9a673e99404d2edba))
-* try creating new ([6dd6b9b](https://github.com/niledatabase/cli/commit/6dd6b9b5a105e14d96da635fa4471777d745efee))
-* try fixing test failure ([123e2b6](https://github.com/niledatabase/cli/commit/123e2b6161078417847553baf937d9b1f5320d74))
-* update test ([20551ec](https://github.com/niledatabase/cli/commit/20551ec2f246e3fea3247b2c23e27cedfaa6fabe))
+* new features for local dev ([f542d3d](https://github.com/niledatabase/cli/commit/f542d3dfeccfaef051f2f9652ef8c986794c8356))
+* new features for local dev ([5758a5f](https://github.com/niledatabase/cli/commit/5758a5f2fcbbd49dcfd9bd3a0778ce223729d0bf))
## [1.0.11-alpha.1](https://github.com/niledatabase/cli/compare/v1.0.10...v1.0.11-alpha.1) (2025-02-07)
diff --git a/package-lock.json b/package-lock.json
index c936d54..2799900 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "niledatabase",
- "version": "1.0.11",
+ "version": "1.1.0-alpha.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "niledatabase",
- "version": "1.0.11",
+ "version": "1.1.0-alpha.1",
"license": "MIT",
"dependencies": {
"@types/pg": "^8.11.10",
@@ -15,6 +15,7 @@
"cli-table3": "^0.6.5",
"commander": "^11.0.0",
"open": "^8.4.2",
+ "ora": "^5.4.1",
"pg": "^8.13.1"
},
"bin": {
@@ -2679,6 +2680,26 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/before-after-hook": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
@@ -2686,6 +2707,31 @@
"dev": true,
"license": "Apache-2.0"
},
+ "node_modules/bl": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+ "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer": "^5.5.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/bottleneck": {
"version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@@ -2772,6 +2818,30 @@
"node-int64": "^0.4.0"
}
},
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -2879,6 +2949,18 @@
"node": ">=6"
}
},
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
+ "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/cli-highlight": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz",
@@ -2942,6 +3024,18 @@
"node": ">=10"
}
},
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/cli-table3": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
@@ -2972,6 +3066,15 @@
"node": ">=12"
}
},
+ "node_modules/clone": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+ "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -3308,6 +3411,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/defaults": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
+ "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "clone": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/define-lazy-prop": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
@@ -4570,6 +4685,26 @@
"node": ">=10.17.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4691,7 +4826,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/ini": {
@@ -4798,6 +4932,15 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-interactive": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
+ "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -5928,6 +6071,34 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6120,7 +6291,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -9452,7 +9622,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"mimic-fn": "^2.1.0"
@@ -9499,6 +9668,41 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/ora": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
+ "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bl": "^4.1.0",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-spinners": "^2.5.0",
+ "is-interactive": "^1.0.0",
+ "is-unicode-supported": "^0.1.0",
+ "log-symbols": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "wcwidth": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/p-each-series": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz",
@@ -10501,6 +10705,19 @@
"node": ">=10"
}
},
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
+ "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -10557,7 +10774,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
- "dev": true,
"license": "MIT"
},
"node_modules/semantic-release": {
@@ -10914,7 +11130,6 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
- "dev": true,
"license": "ISC"
},
"node_modules/signale": {
@@ -11171,7 +11386,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
@@ -11809,7 +12023,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "dev": true,
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {
@@ -11855,6 +12068,15 @@
"makeerror": "1.0.12"
}
},
+ "node_modules/wcwidth": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
+ "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
+ "license": "MIT",
+ "dependencies": {
+ "defaults": "^1.0.3"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 73e095d..839d2fb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "niledatabase",
- "version": "1.0.11",
+ "version": "1.1.0-alpha.1",
"description": "Command line interface for Nile databases",
"main": "dist/index.js",
"bin": {
@@ -64,6 +64,7 @@
"cli-table3": "^0.6.5",
"commander": "^11.0.0",
"open": "^8.4.2",
+ "ora": "^5.4.1",
"pg": "^8.13.1"
},
"devDependencies": {
diff --git a/readme.md b/readme.md
index 5763d23..9c133dc 100644
--- a/readme.md
+++ b/readme.md
@@ -1,7 +1,27 @@
+
+
+
Nile CLI
+
+
+
+
+ Learn more ↗️
+
+
+ Discord
+ 🔵
+ Website
+ 🔵
+ Issues
+
+
+
# Nile CLI
Command line interface for managing Nile databases. Easily create, manage, and monitor your Nile databases from the terminal.
+For detailed documentation, visit our [CLI Documentation](https://thenile.dev/docs/cli/introduction).
+
## Installation
### Using npm
@@ -78,20 +98,76 @@ nile --version
## Usage
```bash
-# Show help
+# Show help and version
nile --help
+nile --version
+
+# Authentication
+nile connect login # Login using browser-based authentication
+nile connect status # Check connection status
+nile connect logout # Clear stored credentials
+
+# Workspace Management
+nile workspace list # List all workspaces
+nile workspace show # Show current workspace details
+nile config --workspace # Set default workspace
+
+# Database Management
+nile db list # List all databases
+nile db show # Show database details
+nile db create --name --region # Create a new database
+nile db delete # Delete a database
+nile db psql # Connect using PostgreSQL CLI
+
+# Tenant Management
+nile tenants list # List all tenants
+nile tenants create --name "Name" # Create a tenant
+nile tenants update --id --new_name "Name" # Update tenant
+nile tenants delete --id # Delete tenant
+
+# User Management
+nile users create # Create a new user
+nile users list # List users
+nile users delete # Delete a user
+
+# Local Development
+nile local start # Start local development environment
+nile local stop # Stop local environment
+nile local info # Show connection information
+
+# Configuration
+nile config # Show current configuration
+nile config --api-key # Set API key
+nile config --workspace # Set workspace
+nile config --db # Set database
+nile config reset # Reset configuration
+```
-# List available commands
-nile help
+### Global Options
+
+These options work with all commands:
+
+```bash
+--api-key # API key for authentication
+--workspace # Workspace to use
+--db # Database to use
+--format # Output format: human (default), json, or csv
+--color # Enable colored output (default: true)
+--no-color # Disable colored output
+--debug # Enable debug output
+```
-# Configure your workspace
-nile config --workspace
+### Output Formats
-# List databases
+```bash
+# Human-readable (default)
nile db list
-# Create a new database
-nile db create --name --region
+# JSON format
+nile --format json db list
+
+# CSV format
+nile --format csv db list
```
## Development Versions
diff --git a/src/commands/local.ts b/src/commands/local.ts
new file mode 100644
index 0000000..46ba4ca
--- /dev/null
+++ b/src/commands/local.ts
@@ -0,0 +1,318 @@
+import { Command } from 'commander';
+import { theme, formatCommand } from '../lib/colors';
+import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions';
+import { spawn, exec } from 'child_process';
+import readline from 'readline';
+import ora from 'ora';
+import { promisify } from 'util';
+
+const execAsync = promisify(exec);
+
+type GetOptions = () => GlobalOptions;
+
+async function waitForPostgres(getOptions: GetOptions, retries = 30, interval = 1000): Promise {
+ const command = 'docker exec nile-local pg_isready -U 00000000-0000-0000-0000-000000000000 -p 5432 -d test -h localhost';
+
+ for (let i = 0; i < retries; i++) {
+ try {
+ const { stdout } = await execAsync(command);
+ if (stdout.includes('accepting connections')) {
+ return true;
+ }
+ } catch (error: any) {
+ const options = getOptions();
+ if (options.debug) {
+ console.log(theme.dim(`Waiting for PostgreSQL (attempt ${i + 1}/${retries})...`));
+ if (error.stdout) console.log(theme.dim('stdout:'), error.stdout);
+ if (error.stderr) console.log(theme.dim('stderr:'), error.stderr);
+ }
+ // Wait before next attempt
+ await new Promise(resolve => setTimeout(resolve, interval));
+ continue;
+ }
+ }
+ return false;
+}
+
+export function createLocalCommand(getOptions: GetOptions): Command {
+ const local = new Command('local')
+ .description('Manage local development environment')
+ .addHelpText('after', `
+Examples:
+ ${formatCommand('nile local start')} Start local development environment
+ ${formatCommand('nile local start', '--no-prompt')} Start without prompting for psql connection
+ ${formatCommand('nile local stop')} Stop local development environment
+ ${formatCommand('nile local info')} Show connection information
+
+${getGlobalOptionsHelp()}`);
+
+ local
+ .command('info')
+ .description('Show connection information for local environment')
+ .action(async () => {
+ try {
+ // Check if container is running
+ const { stdout } = await execAsync('docker ps --filter name=nile-local --format {{.Names}}');
+ if (!stdout.includes('nile-local')) {
+ console.error(theme.error('\nNo Nile local environment is currently running.'));
+ console.log(theme.dim('Start it with: nile local start'));
+ process.exit(1);
+ }
+
+ // Display connection information
+ console.log('\nConnection Information:');
+ console.log(theme.info('Host: ') + 'localhost');
+ console.log(theme.info('Port: ') + '5432');
+ console.log(theme.info('Database: ') + 'test');
+ console.log(theme.info('Username: ') + '00000000-0000-0000-0000-000000000000');
+ console.log(theme.info('Password: ') + 'password');
+
+ // Show direct connection command in debug mode
+ const options = getOptions();
+ if (options.debug) {
+ console.log(theme.dim('\nDirect connection command:'));
+ console.log(theme.dim('PGPASSWORD=password psql -h localhost -p 5432 -U 00000000-0000-0000-0000-000000000000 -d test'));
+ }
+ } catch (error: any) {
+ const options = getOptions();
+ if (options.debug) {
+ console.error(theme.error('\nFailed to check environment status:'), error);
+ } else {
+ console.error(theme.error('\nFailed to check environment status:'), error.message || 'Unknown error');
+ }
+ process.exit(1);
+ }
+ });
+
+ local
+ .command('stop')
+ .description('Stop local development environment')
+ .action(async () => {
+ try {
+ // Check if container is running
+ const { stdout } = await execAsync('docker ps --filter name=nile-local --format {{.Names}}');
+ if (!stdout.includes('nile-local')) {
+ console.error(theme.error('\nNo Nile local environment is currently running.'));
+ process.exit(1);
+ }
+
+ // Start spinner for stopping container
+ const stopSpinner = ora({
+ text: 'Stopping local development environment...',
+ color: 'cyan'
+ }).start();
+
+ try {
+ await execAsync('docker stop nile-local && docker rm nile-local');
+ stopSpinner.succeed('Local environment stopped successfully');
+ } catch (error: any) {
+ stopSpinner.fail('Failed to stop local environment');
+ if (getOptions().debug) {
+ console.error(theme.error('Error details:'), error.message);
+ }
+ process.exit(1);
+ }
+ } catch (error: any) {
+ const options = getOptions();
+ if (options.debug) {
+ console.error(theme.error('Failed to check container status:'), error);
+ } else {
+ console.error(theme.error('Failed to check container status:'), error.message || 'Unknown error');
+ }
+ process.exit(1);
+ }
+ });
+
+ local
+ .command('start')
+ .description('Start local development environment')
+ .option('--no-prompt', 'Start without prompting for psql connection')
+ .action(async (cmdOptions) => {
+ try {
+ // Check if container is already running
+ try {
+ const { stdout } = await execAsync('docker ps --filter name=nile-local --format {{.Names}}');
+ if (stdout.includes('nile-local')) {
+ console.error(theme.error('\nA Nile local environment is already running.'));
+ console.log(theme.dim('To stop it, use: docker stop nile-local'));
+ process.exit(1);
+ }
+ } catch (error) {
+ // Ignore error, means docker ps failed which is fine
+ }
+
+ // Start spinner for Docker pull
+ const pullSpinner = ora({
+ text: 'Pulling latest Nile testing container...',
+ color: 'cyan'
+ }).start();
+
+ try {
+ await execAsync('docker pull ghcr.io/niledatabase/testingcontainer:latest');
+ pullSpinner.succeed('Latest Nile testing container pulled successfully');
+ } catch (error: any) {
+ pullSpinner.fail('Failed to pull latest container');
+ if (getOptions().debug) {
+ console.error(theme.error('Error details:'), error.message);
+ }
+ process.exit(1);
+ }
+
+ // Start spinner for container launch
+ const startSpinner = ora({
+ text: 'Starting local development environment...',
+ color: 'cyan'
+ }).start();
+
+ // Start Docker container in background
+ const docker = spawn('docker', [
+ 'run',
+ '--name', 'nile-local',
+ '-d', // Run in background
+ '-p', '5432:5432',
+ 'ghcr.io/niledatabase/testingcontainer:latest'
+ ]);
+
+ // Collect any error output
+ let errorOutput = '';
+ docker.stderr?.on('data', (data) => {
+ errorOutput += data.toString();
+ });
+
+ // Wait for container to start
+ await new Promise((resolve, reject) => {
+ docker.on('close', (code) => {
+ if (code !== 0) {
+ reject(new Error(`Docker failed to start: ${errorOutput}`));
+ } else {
+ resolve(undefined);
+ }
+ });
+ });
+
+ // Wait for PostgreSQL to be ready
+ const readySpinner = ora({
+ text: 'Waiting for database to be ready...',
+ color: 'cyan'
+ }).start();
+
+ const isReady = await waitForPostgres(getOptions);
+ if (!isReady) {
+ readySpinner.fail('Database failed to start within timeout period');
+ console.log(theme.dim('\nStopping container...'));
+ try {
+ await execAsync('docker stop nile-local && docker rm nile-local');
+ } catch (error) {
+ // Ignore cleanup errors
+ }
+ process.exit(1);
+ }
+
+ readySpinner.succeed('Database is ready');
+ startSpinner.succeed('Local development environment started successfully');
+
+ // Display connection information
+ console.log('\nConnection Information:');
+ console.log(theme.info('Host: ') + 'localhost');
+ console.log(theme.info('Port: ') + '5432');
+ console.log(theme.info('Database: ') + 'test');
+ console.log(theme.info('Username: ') + '00000000-0000-0000-0000-000000000000');
+ console.log(theme.info('Password: ') + 'password');
+
+ if (cmdOptions.prompt) {
+ // Create readline interface
+ const rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+
+ // Ask if user wants to connect with psql
+ rl.question('\nWould you like to connect using psql? (y/N) ', async (answer) => {
+ rl.close();
+ if (answer.toLowerCase() === 'y') {
+ const connectSpinner = ora({
+ text: 'Connecting...',
+ color: 'cyan'
+ }).start();
+
+ // Add a longer delay to ensure the database is fully ready for connections
+ await new Promise(resolve => setTimeout(resolve, 3000));
+
+ // Connect using psql with individual parameters
+ const psql = spawn('psql', [
+ '-h', 'localhost',
+ '-p', '5432',
+ '-U', '00000000-0000-0000-0000-000000000000',
+ '-d', 'test',
+ '-w' // Never prompt for password
+ ], {
+ stdio: 'inherit',
+ env: {
+ ...process.env,
+ PGPASSWORD: 'password' // Set password via environment variable
+ }
+ });
+
+ // Stop the spinner immediately as psql will take over the terminal
+ connectSpinner.stop();
+
+ // Handle psql exit
+ psql.on('exit', (code) => {
+ if (code !== 0) {
+ console.error(theme.error('\nFailed to connect using psql. Please check if psql is installed.'));
+ if (getOptions().debug) {
+ console.error(theme.dim('Try connecting directly with:'));
+ console.error(theme.dim('PGPASSWORD=password psql -h localhost -p 5432 -U 00000000-0000-0000-0000-000000000000 -d test'));
+ }
+ }
+ // Note: Don't exit here as the Docker container should keep running
+ });
+
+ // Handle psql error
+ psql.on('error', (error) => {
+ connectSpinner.fail('Failed to launch psql');
+ console.error(theme.error('\nError launching psql:'), error.message);
+ if (error.message.includes('ENOENT')) {
+ console.error(theme.error('Please make sure psql is installed and available in your PATH'));
+ }
+ });
+ } else {
+ console.log(theme.dim('\nYou can connect to the database using your preferred client with the connection information above.'));
+ console.log(theme.dim('To stop the environment, use: nile local stop'));
+ }
+ });
+ } else {
+ console.log(theme.dim('\nTo stop the environment, use: nile local stop'));
+ }
+
+ // Handle process termination
+ process.on('SIGINT', async () => {
+ console.log(theme.dim('\nStopping local development environment...'));
+ try {
+ await execAsync('docker stop nile-local && docker rm nile-local');
+ console.log(theme.success('Local environment stopped successfully'));
+ } catch (error) {
+ console.error(theme.error('Failed to stop local environment cleanly'));
+ }
+ process.exit(0);
+ });
+
+ } catch (error: any) {
+ const options = getOptions();
+ if (options.debug) {
+ console.error(theme.error('Failed to start local environment:'), error);
+ } else {
+ console.error(theme.error('Failed to start local environment:'), error.message || 'Unknown error');
+ }
+ // Cleanup on error
+ try {
+ await execAsync('docker stop nile-local && docker rm nile-local');
+ } catch (cleanupError) {
+ // Ignore cleanup errors
+ }
+ process.exit(1);
+ }
+ });
+
+ return local;
+}
\ No newline at end of file
diff --git a/src/commands/user.ts b/src/commands/user.ts
new file mode 100644
index 0000000..daade46
--- /dev/null
+++ b/src/commands/user.ts
@@ -0,0 +1,495 @@
+import { Command } from 'commander';
+import { Client } from 'pg';
+import { ConfigManager } from '../lib/config';
+import { NileAPI } from '../lib/api';
+import { theme, formatCommand } from '../lib/colors';
+import { GlobalOptions, getGlobalOptionsHelp } from '../lib/globalOptions';
+import Table from 'cli-table3';
+
+async function getWorkspaceAndDatabase(options: GlobalOptions): Promise<{ workspaceSlug: string; databaseName: string }> {
+ const configManager = new ConfigManager(options);
+ const workspaceSlug = configManager.getWorkspace();
+ if (!workspaceSlug) {
+ throw new Error('No workspace specified. Use one of:\n' +
+ '1. --workspace flag\n' +
+ '2. nile config --workspace \n' +
+ '3. NILE_WORKSPACE environment variable');
+ }
+
+ const databaseName = configManager.getDatabase();
+ if (!databaseName) {
+ throw new Error('No database specified. Use one of:\n' +
+ '1. --db flag\n' +
+ '2. nile config --db \n' +
+ '3. NILE_DB environment variable');
+ }
+
+ return { workspaceSlug, databaseName };
+}
+
+async function getPostgresClient(api: NileAPI, workspaceSlug: string, databaseName: string, options: GlobalOptions): Promise {
+ // Get database credentials from control plane
+ console.log(theme.dim('\nFetching database credentials...'));
+ const credentials = await api.createDatabaseCredentials(workspaceSlug, databaseName);
+
+ if (!credentials.id || !credentials.password) {
+ throw new Error('Invalid credentials received from server');
+ }
+
+ // Create postgres connection URL
+ const region = credentials.database.region.toLowerCase();
+ const regionParts = region.split('_');
+ const regionPrefix = `${regionParts[1]}-${regionParts[2]}-${regionParts[3]}`; // e.g., us-west-2
+
+ // Use custom host if provided, otherwise use default with region prefix
+ const dbHost = options.dbHost ?
+ `${regionPrefix}.${options.dbHost}` :
+ `${regionPrefix}.db.thenile.dev`;
+
+ // Create postgres client
+ const client = new Client({
+ host: dbHost,
+ port: 5432,
+ database: databaseName,
+ user: credentials.id,
+ password: credentials.password,
+ ssl: {
+ rejectUnauthorized: false
+ }
+ });
+
+ if (options.debug) {
+ console.log(theme.dim('\nConnecting to PostgreSQL:'));
+ console.log(theme.dim('Host:'), dbHost);
+ console.log(theme.dim('Database:'), databaseName);
+ console.log(theme.dim('User:'), credentials.id);
+ console.log();
+ }
+
+ // Connect to the database
+ await client.connect();
+ return client;
+}
+
+export class UsersCommand {
+ constructor(program: Command, getGlobalOptions: () => GlobalOptions) {
+ const users = program
+ .command('users')
+ .description('Manage users in your database')
+ .addHelpText('after', `
+Examples:
+ # Create users
+ ${formatCommand('nile users create', '--email "user@example.com" --password "securepass123"')} Create a basic user
+ ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --name "John Doe"')} Create user with name
+ ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --given-name "John" --family-name "Doe"')} Create user with full name
+ ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --tenant-id "tenant123"')} Create user in specific tenant
+ ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --new-tenant-name "New Corp"')} Create user with new tenant
+
+ # List user tenants
+ ${formatCommand('nile users tenants', '--user-id user123')} List all tenants for a user
+
+ # Update users
+ ${formatCommand('nile users update', '--user-id user123 --name "Updated Name"')} Update user name
+ ${formatCommand('nile users update', '--user-id user123 --given-name "John" --family-name "Doe"')} Update user full name
+
+ # Remove user from tenant
+ ${formatCommand('nile users remove-tenant', '--user-id user123 --tenant-id tenant123')} Remove user from tenant
+
+${getGlobalOptionsHelp()}`);
+
+ const createCmd = new Command('create')
+ .description('Create a new user')
+ .requiredOption('--email ', 'Email address for the user')
+ .requiredOption('--password ', 'Password for the user')
+ .option('--name ', 'Full name of the user')
+ .option('--given-name ', 'Given (first) name of the user')
+ .option('--family-name ', 'Family (last) name of the user')
+ .option('--picture ', 'URL of the user\'s profile picture')
+ .option('--tenant-id ', 'ID of the tenant to add the user to')
+ .option('--new-tenant-name ', 'Name of a new tenant to create and add the user to')
+ .option('--roles ', 'Roles to assign to the user in the tenant')
+ .addHelpText('after', `
+Examples:
+ ${formatCommand('nile users create', '--email "user@example.com" --password "securepass123"')}
+ ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --name "John Doe"')}
+ ${formatCommand('nile users create', '--email "user@example.com" --password "pass123" --tenant-id "tenant123"')}
+ `)
+ .action(async (cmdOptions) => {
+ let client: Client | undefined;
+ try {
+ const options = getGlobalOptions();
+ const configManager = new ConfigManager(options);
+ const api = new NileAPI({
+ token: configManager.getToken(),
+ dbHost: configManager.getDbHost(),
+ controlPlaneUrl: configManager.getGlobalHost(),
+ debug: options.debug
+ });
+
+ const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options);
+ client = await getPostgresClient(api, workspaceSlug, databaseName, options);
+
+ // Begin transaction
+ await client.query('BEGIN');
+
+ try {
+ // Insert into users schema
+ console.log(theme.dim('\nCreating user...'));
+ const result = await client.query(
+ `INSERT INTO users.users (
+ email,
+ name,
+ given_name,
+ family_name,
+ picture
+ ) VALUES ($1, $2, $3, $4, $5) RETURNING *`,
+ [
+ cmdOptions.email,
+ cmdOptions.name || null,
+ cmdOptions.givenName || null,
+ cmdOptions.familyName || null,
+ cmdOptions.picture || null
+ ]
+ );
+
+ const user = result.rows[0];
+
+ // Create auth credentials (this would typically be handled by auth service)
+ await client.query(
+ `INSERT INTO auth.credentials (
+ user_id,
+ identifier,
+ password,
+ type
+ ) VALUES ($1, $2, crypt($3, gen_salt('bf')), 'password')`,
+ [user.id, cmdOptions.email, cmdOptions.password]
+ );
+
+ // If tenant ID is provided, create user-tenant relationship
+ if (cmdOptions.tenantId) {
+ await client.query(
+ `INSERT INTO users.tenant_users (
+ tenant_id,
+ user_id,
+ roles,
+ email
+ ) VALUES ($1, $2, $3, $4)`,
+ [cmdOptions.tenantId, user.id, cmdOptions.roles || null, cmdOptions.email]
+ );
+ }
+
+ // If new tenant name is provided, create tenant and relationship
+ if (cmdOptions.newTenantName) {
+ const tenantResult = await client.query(
+ 'INSERT INTO tenants (name) VALUES ($1) RETURNING id',
+ [cmdOptions.newTenantName]
+ );
+ const tenantId = tenantResult.rows[0].id;
+ await client.query(
+ `INSERT INTO users.tenant_users (
+ tenant_id,
+ user_id,
+ roles,
+ email
+ ) VALUES ($1, $2, $3, $4)`,
+ [tenantId, user.id, cmdOptions.roles || null, cmdOptions.email]
+ );
+ }
+
+ // Commit transaction
+ await client.query('COMMIT');
+
+ if (options.format === 'json') {
+ console.log(JSON.stringify(user, null, 2));
+ return;
+ }
+
+ if (options.format === 'csv') {
+ console.log('ID,EMAIL,NAME');
+ console.log(`${user.id},${user.email},${user.name || ''}`);
+ return;
+ }
+
+ // Create a nicely formatted table
+ const table = new Table({
+ head: ['Field', 'Value'].map(h => theme.primary(h)),
+ style: { head: [], border: [] },
+ chars: {
+ 'top': '─',
+ 'top-mid': '┬',
+ 'top-left': '┌',
+ 'top-right': '┐',
+ 'bottom': '─',
+ 'bottom-mid': '┴',
+ 'bottom-left': '└',
+ 'bottom-right': '┘',
+ 'left': '│',
+ 'left-mid': '├',
+ 'mid': '─',
+ 'mid-mid': '┼',
+ 'right': '│',
+ 'right-mid': '┤',
+ 'middle': '│'
+ }
+ });
+
+ table.push(
+ ['ID', theme.info(user.id)],
+ ['Email', theme.info(user.email)],
+ ['Name', theme.info(user.name || '')],
+ ['Given Name', theme.info(user.given_name || '')],
+ ['Family Name', theme.info(user.family_name || '')],
+ ['Picture', theme.info(user.picture || '')]
+ );
+
+ console.log('\nUser created successfully:');
+ console.log(table.toString());
+
+ if (cmdOptions.tenantId) {
+ console.log(theme.success(`\nUser added to tenant: ${cmdOptions.tenantId}`));
+ } else if (cmdOptions.newTenantName) {
+ console.log(theme.success(`\nUser added to new tenant: ${cmdOptions.newTenantName}`));
+ }
+ } catch (error) {
+ // Rollback transaction on error
+ await client.query('ROLLBACK');
+ throw error;
+ }
+ } catch (error: any) {
+ const options = getGlobalOptions();
+ if (options.debug) {
+ console.error(theme.error('Failed to create user:'), error);
+ } else {
+ console.error(theme.error('Failed to create user:'), error.message || 'Unknown error');
+ }
+ process.exit(1);
+ } finally {
+ if (client) {
+ await client.end();
+ }
+ }
+ });
+
+ const tenantsCmd = new Command('tenants')
+ .description('List tenants for a user')
+ .requiredOption('--user-id ', 'ID of the user')
+ .addHelpText('after', `
+Examples:
+ ${formatCommand('nile users tenants', '--user-id user123')} List all tenants for a user
+ `)
+ .action(async (cmdOptions) => {
+ try {
+ const options = getGlobalOptions();
+ const configManager = new ConfigManager(options);
+ const api = new NileAPI({
+ token: configManager.getToken(),
+ dbHost: configManager.getDbHost(),
+ controlPlaneUrl: configManager.getGlobalHost(),
+ debug: options.debug
+ });
+
+ const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options);
+ const databaseId = await api.getDatabaseId(workspaceSlug, databaseName);
+
+ const tenants = await api.getUserTenants(databaseId, cmdOptions.userId);
+
+ if (options.format === 'json') {
+ console.log(JSON.stringify(tenants, null, 2));
+ return;
+ }
+
+ if (options.format === 'csv') {
+ console.log('ID,NAME');
+ tenants.forEach(tenant => {
+ console.log(`${tenant.id},${tenant.name || ''}`);
+ });
+ return;
+ }
+
+ if (tenants.length === 0) {
+ console.log(theme.warning('\nNo tenants found for this user.'));
+ return;
+ }
+
+ console.log('\nUser Tenants:');
+ const table = new Table({
+ head: [
+ theme.header('ID'),
+ theme.header('NAME')
+ ],
+ style: { head: [], border: [] },
+ chars: {
+ 'top': '─',
+ 'top-mid': '┬',
+ 'top-left': '┌',
+ 'top-right': '┐',
+ 'bottom': '─',
+ 'bottom-mid': '┴',
+ 'bottom-left': '└',
+ 'bottom-right': '┘',
+ 'left': '│',
+ 'left-mid': '├',
+ 'mid': '─',
+ 'mid-mid': '┼',
+ 'right': '│',
+ 'right-mid': '┤',
+ 'middle': '│'
+ }
+ });
+
+ tenants.forEach(tenant => {
+ table.push([
+ theme.primary(tenant.id),
+ theme.info(tenant.name || '(unnamed)')
+ ]);
+ });
+
+ console.log(table.toString());
+ } catch (error: any) {
+ const options = getGlobalOptions();
+ if (options.debug) {
+ console.error(theme.error('Failed to list user tenants:'), error);
+ } else {
+ console.error(theme.error('Failed to list user tenants:'), error.message || 'Unknown error');
+ }
+ process.exit(1);
+ }
+ });
+
+ const updateCmd = new Command('update')
+ .description('Update user details')
+ .requiredOption('--user-id ', 'ID of the user to update')
+ .option('--name ', 'Full name of the user')
+ .option('--given-name ', 'Given (first) name of the user')
+ .option('--family-name ', 'Family (last) name of the user')
+ .option('--picture ', 'URL of the user\'s profile picture')
+ .addHelpText('after', `
+Examples:
+ ${formatCommand('nile users update', '--user-id user123 --name "Updated Name"')}
+ ${formatCommand('nile users update', '--user-id user123 --given-name "John" --family-name "Doe"')}
+ `)
+ .action(async (cmdOptions) => {
+ try {
+ const options = getGlobalOptions();
+ const configManager = new ConfigManager(options);
+ const api = new NileAPI({
+ token: configManager.getToken(),
+ dbHost: configManager.getDbHost(),
+ controlPlaneUrl: configManager.getGlobalHost(),
+ debug: options.debug
+ });
+
+ const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options);
+ const databaseId = await api.getDatabaseId(workspaceSlug, databaseName);
+
+ const updates = {
+ name: cmdOptions.name,
+ givenName: cmdOptions.givenName,
+ familyName: cmdOptions.familyName,
+ picture: cmdOptions.picture
+ };
+
+ const user = await api.updateUser(databaseId, cmdOptions.userId, updates);
+
+ if (options.format === 'json') {
+ console.log(JSON.stringify(user, null, 2));
+ return;
+ }
+
+ if (options.format === 'csv') {
+ console.log('ID,EMAIL,NAME');
+ console.log(`${user.id},${user.email},${user.name || ''}`);
+ return;
+ }
+
+ console.log('\nUser updated successfully:');
+ const table = new Table({
+ head: ['Field', 'Value'].map(h => theme.primary(h)),
+ style: { head: [], border: [] },
+ chars: {
+ 'top': '─',
+ 'top-mid': '┬',
+ 'top-left': '┌',
+ 'top-right': '┐',
+ 'bottom': '─',
+ 'bottom-mid': '┴',
+ 'bottom-left': '└',
+ 'bottom-right': '┘',
+ 'left': '│',
+ 'left-mid': '├',
+ 'mid': '─',
+ 'mid-mid': '┼',
+ 'right': '│',
+ 'right-mid': '┤',
+ 'middle': '│'
+ }
+ });
+
+ table.push(
+ ['ID', theme.info(user.id)],
+ ['Email', theme.info(user.email)],
+ ['Name', theme.info(user.name || '')],
+ ['Given Name', theme.info(user.givenName || '')],
+ ['Family Name', theme.info(user.familyName || '')],
+ ['Picture', theme.info(user.picture || '')]
+ );
+
+ console.log(table.toString());
+ } catch (error: any) {
+ const options = getGlobalOptions();
+ if (options.debug) {
+ console.error(theme.error('Failed to update user:'), error);
+ } else {
+ console.error(theme.error('Failed to update user:'), error.message || 'Unknown error');
+ }
+ process.exit(1);
+ }
+ });
+
+ const removeTenantCmd = new Command('remove-tenant')
+ .description('Remove a user from a tenant')
+ .requiredOption('--user-id ', 'ID of the user')
+ .requiredOption('--tenant-id ', 'ID of the tenant')
+ .addHelpText('after', `
+Examples:
+ ${formatCommand('nile users remove-tenant', '--user-id user123 --tenant-id tenant123')}
+ `)
+ .action(async (cmdOptions) => {
+ try {
+ const options = getGlobalOptions();
+ const configManager = new ConfigManager(options);
+ const api = new NileAPI({
+ token: configManager.getToken(),
+ dbHost: configManager.getDbHost(),
+ controlPlaneUrl: configManager.getGlobalHost(),
+ debug: options.debug
+ });
+
+ const { workspaceSlug, databaseName } = await getWorkspaceAndDatabase(options);
+ const databaseId = await api.getDatabaseId(workspaceSlug, databaseName);
+
+ await api.removeUserFromTenant(databaseId, cmdOptions.userId, cmdOptions.tenantId);
+ console.log(theme.success(`\nUser '${cmdOptions.userId}' removed from tenant '${cmdOptions.tenantId}'`));
+ } catch (error: any) {
+ const options = getGlobalOptions();
+ if (options.debug) {
+ console.error(theme.error('Failed to remove user from tenant:'), error);
+ } else {
+ console.error(theme.error('Failed to remove user from tenant:'), error.message || 'Unknown error');
+ }
+ process.exit(1);
+ }
+ });
+
+ users.addCommand(createCmd);
+ users.addCommand(tenantsCmd);
+ users.addCommand(updateCmd);
+ users.addCommand(removeTenantCmd);
+ }
+}
+
+export function createUsersCommand(getGlobalOptions: () => GlobalOptions): Command {
+ const program = new Command();
+ new UsersCommand(program, getGlobalOptions);
+ return program.commands[0];
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index cbfe341..58fa8e2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,10 +6,13 @@ import { createWorkspaceCommand } from './commands/workspace';
import { createDbCommand } from './commands/db';
import { createTenantsCommand } from './commands/tenants';
import { configCommand } from './commands/config';
+import { createUsersCommand } from './commands/user';
+import { createLocalCommand } from './commands/local';
import { addGlobalOptions, updateChalkConfig } from './lib/globalOptions';
+import { version } from '../package.json';
const cli = new Command()
- .version('0.1.0')
+ .version(version)
.description('Nile CLI')
.addHelpText('after', `
Examples:
@@ -17,6 +20,8 @@ Examples:
$ nile --no-color db show my-database
$ nile tenants list List tenants in selected database
$ nile config --api-key Set API key in config
+ $ nile users create Create a new user
+ $ nile local start Start local development environment
`);
// Add global options
@@ -34,5 +39,7 @@ cli.addCommand(createWorkspaceCommand(() => cli.opts()));
cli.addCommand(createDbCommand(() => cli.opts()));
cli.addCommand(createTenantsCommand(() => cli.opts()));
cli.addCommand(configCommand());
+cli.addCommand(createUsersCommand(() => cli.opts()));
+cli.addCommand(createLocalCommand(() => cli.opts()));
cli.parse(process.argv);
\ No newline at end of file
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 1ed87e1..c89cc2f 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -34,12 +34,44 @@ export interface Workspace {
created?: string;
}
+export interface CreateUserRequest {
+ email: string;
+ password: string;
+ name?: string;
+ givenName?: string;
+ familyName?: string;
+ picture?: string;
+}
+
+export interface User {
+ id: string;
+ email: string;
+ name?: string;
+ givenName?: string;
+ familyName?: string;
+ picture?: string;
+ emailVerified?: string;
+ created: string;
+ updated: string;
+ tenants?: string[];
+}
+
+export interface UpdateUserRequest {
+ name?: string;
+ givenName?: string;
+ familyName?: string;
+ picture?: string;
+}
+
export class NileAPI {
private controlPlaneClient: AxiosInstance;
+ private userClient: AxiosInstance;
private static DEFAULT_CONTROL_PLANE_URL = 'https://global.thenile.dev';
+ private static DEFAULT_USER_URL = 'https://us-west-2.api.thenile.dev';
private debug: boolean;
private token: string;
private controlPlaneUrl: string;
+ private userUrl: string;
private dbHost?: string;
constructor(options: NileAPIOptions) {
@@ -59,9 +91,13 @@ export class NileAPI {
this.controlPlaneUrl = NileAPI.DEFAULT_CONTROL_PLANE_URL;
}
+ // Set user URL
+ this.userUrl = NileAPI.DEFAULT_USER_URL;
+
if (this.debug) {
console.log(theme.dim('\nAPI Configuration:'));
console.log(theme.dim('Control Plane URL:'), this.controlPlaneUrl);
+ console.log(theme.dim('User API URL:'), this.userUrl);
console.log();
}
@@ -74,8 +110,18 @@ export class NileAPI {
},
});
+ // Create user client for user operations
+ this.userClient = axios.create({
+ baseURL: this.userUrl,
+ headers: {
+ Authorization: `Bearer ${this.token}`,
+ 'Content-Type': 'application/json'
+ },
+ });
+
// Add debug logging
this.addDebugLogging(this.controlPlaneClient);
+ this.addDebugLogging(this.userClient);
this.dbHost = options.dbHost;
}
@@ -207,4 +253,45 @@ export class NileAPI {
const response = await this.controlPlaneClient.get(`/workspaces/${workspaceSlug}`);
return response.data;
}
+
+ async getDatabaseId(workspaceSlug: string, databaseName: string): Promise {
+ const database = await this.getDatabase(workspaceSlug, databaseName);
+ if (!database.id) {
+ throw new Error(`Could not find database ID for database '${databaseName}'`);
+ }
+ return database.id;
+ }
+
+ async createUser(databaseId: string, user: CreateUserRequest, options?: { tenantId?: string; newTenantName?: string }): Promise {
+ const response = await this.userClient.post(
+ `/v2/databases/${databaseId}/users`,
+ user,
+ {
+ params: {
+ tenantId: options?.tenantId,
+ newTenantName: options?.newTenantName
+ }
+ }
+ );
+ return response.data;
+ }
+
+ async getUserTenants(databaseId: string, userId: string): Promise {
+ const response = await this.userClient.get(`/v2/databases/${databaseId}/users/${userId}/tenants`);
+ return response.data;
+ }
+
+ async updateUser(databaseId: string, userId: string, updates: UpdateUserRequest): Promise {
+ const response = await this.userClient.patch(
+ `/v2/databases/${databaseId}/users/${userId}`,
+ updates
+ );
+ return response.data;
+ }
+
+ async removeUserFromTenant(databaseId: string, userId: string, tenantId: string): Promise {
+ await this.userClient.delete(
+ `/v2/databases/${databaseId}/users/${userId}/tenants/${tenantId}`
+ );
+ }
}
\ No newline at end of file
diff --git a/src/lib/types.ts b/src/lib/types.ts
index ecbbf52..a9dc761 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -6,6 +6,7 @@ export interface Workspace {
}
export interface Database {
+ id: string;
name: string;
region: string;
status: string;