@joytreesite/joytree 1.0.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/README.md +169 -0
- package/bin/joytree.js +277 -0
- package/commands/account.js +46 -0
- package/commands/auth.js +65 -0
- package/commands/db.js +106 -0
- package/commands/deploy.js +123 -0
- package/commands/domains.js +91 -0
- package/commands/env.js +101 -0
- package/commands/extras.js +101 -0
- package/commands/github.js +55 -0
- package/commands/logs.js +68 -0
- package/commands/projects.js +96 -0
- package/commands/ssh.js +77 -0
- package/commands/webhook.js +38 -0
- package/lib/api.js +104 -0
- package/lib/config.js +43 -0
- package/lib/ui.js +90 -0
- package/package.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# 🌳 Joytree CLI
|
|
2
|
+
|
|
3
|
+
Deploy and manage your Joytree-hosted sites from the terminal.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @joytreeapp/joytree
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run without installing:
|
|
12
|
+
```bash
|
|
13
|
+
npx @joytreeapp/joytree login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Authenticate
|
|
22
|
+
joytree login
|
|
23
|
+
|
|
24
|
+
# 2. Deploy a GitHub repo
|
|
25
|
+
joytree deploy --repo https://github.com/you/my-site --name my-site
|
|
26
|
+
|
|
27
|
+
# 3. View your projects
|
|
28
|
+
joytree projects
|
|
29
|
+
|
|
30
|
+
# 4. Stream logs
|
|
31
|
+
joytree logs my-site --follow
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
### Account
|
|
39
|
+
|
|
40
|
+
| Command | Description |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `joytree login [--api-key <key>]` | Authenticate with your Joytree API key |
|
|
43
|
+
| `joytree logout` | Remove saved credentials |
|
|
44
|
+
| `joytree whoami` | Show current account info |
|
|
45
|
+
| `joytree status` | Show account status and project list |
|
|
46
|
+
|
|
47
|
+
> Find your API key at: **your-joytree-dashboard → Settings → API Key**
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Deploy
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `joytree deploy` | Deploy a GitHub repo (interactive) |
|
|
56
|
+
| `joytree deploy --repo <url> --name <name>` | Deploy with flags |
|
|
57
|
+
| `joytree deploy --static` | Deploy as a static site |
|
|
58
|
+
| `joytree redeploy <project-id>` | Trigger a fresh redeployment |
|
|
59
|
+
| `joytree deployments [project-id]` | Show recent deployments |
|
|
60
|
+
| `joytree open <project-id>` | Open the live URL in your browser |
|
|
61
|
+
|
|
62
|
+
**Deploy flags:**
|
|
63
|
+
```
|
|
64
|
+
-r, --repo <url> GitHub repository URL
|
|
65
|
+
-b, --branch <branch> Branch to deploy (default: main)
|
|
66
|
+
-n, --name <name> Project name / subdomain
|
|
67
|
+
--build <cmd> Build command, e.g. "npm run build"
|
|
68
|
+
--start <cmd> Start command, e.g. "node server.js"
|
|
69
|
+
--static Mark as a static site
|
|
70
|
+
-m, --message <msg> Deployment message
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Projects
|
|
76
|
+
|
|
77
|
+
| Command | Description |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `joytree projects` | List all projects |
|
|
80
|
+
| `joytree projects --json` | Output raw JSON |
|
|
81
|
+
| `joytree inspect <project-id>` | Show full project details |
|
|
82
|
+
| `joytree delete <project-id>` | Delete a project |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### Logs
|
|
87
|
+
|
|
88
|
+
| Command | Description |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `joytree logs <project-id>` | Fetch recent runtime logs |
|
|
91
|
+
| `joytree logs <project-id> --lines 100` | Fetch last 100 lines |
|
|
92
|
+
| `joytree logs <project-id> --follow` | Stream live logs (poll every 3s) |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Environment Variables
|
|
97
|
+
|
|
98
|
+
| Command | Description |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `joytree env list <project-id>` | List all env var keys |
|
|
101
|
+
| `joytree env set <project-id> KEY=VALUE` | Set one or more env vars |
|
|
102
|
+
| `joytree env delete <project-id> KEY` | Delete an env var |
|
|
103
|
+
| `joytree env push <project-id>` | Push a local `.env` file |
|
|
104
|
+
| `joytree env push <project-id> --file prod.env --force` | Push & overwrite all |
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Set multiple at once
|
|
108
|
+
joytree env set my-site DATABASE_URL=postgres://... SECRET_KEY=abc123
|
|
109
|
+
|
|
110
|
+
# Push your .env file
|
|
111
|
+
joytree env push my-site
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Domains
|
|
117
|
+
|
|
118
|
+
| Command | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `joytree domains list` | List your custom domains |
|
|
121
|
+
| `joytree domains attach <domain> <project-id>` | Attach a domain to a project |
|
|
122
|
+
| `joytree domains verify <domain>` | Trigger DNS verification |
|
|
123
|
+
| `joytree domains remove <domain>` | Remove a custom domain |
|
|
124
|
+
| `joytree domains check <domain>` | Check domain availability |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### Databases
|
|
129
|
+
|
|
130
|
+
| Command | Description |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `joytree db list` | List all databases |
|
|
133
|
+
| `joytree db create --type postgres --name mydb` | Create a database |
|
|
134
|
+
| `joytree db start <db-id>` | Start a stopped database |
|
|
135
|
+
| `joytree db stop <db-id>` | Stop a running database |
|
|
136
|
+
| `joytree db restart <db-id>` | Restart a database |
|
|
137
|
+
| `joytree db logs <db-id>` | Fetch recent database logs |
|
|
138
|
+
| `joytree db delete <db-id>` | Delete a database |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
Credentials are stored at `~/.joytree/credentials.json` (mode 600).
|
|
145
|
+
|
|
146
|
+
You can also use environment variables:
|
|
147
|
+
```bash
|
|
148
|
+
export JOYTREE_API_KEY=jtk_your_key_here
|
|
149
|
+
export JOYTREE_BASE_URL=https://joytree.app
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Publishing to npm
|
|
155
|
+
|
|
156
|
+
To publish this CLI so users can `npm install -g @joytreeapp/joytree`:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
cd joytree-cli
|
|
160
|
+
npm login # login to npm as @joytreeapp
|
|
161
|
+
npm publish --access public
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Then users install with:
|
|
165
|
+
```bash
|
|
166
|
+
npm install -g @joytreeapp/joytree
|
|
167
|
+
# or
|
|
168
|
+
npx @joytreeapp/joytree login
|
|
169
|
+
```
|
package/bin/joytree.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { program } = require('commander');
|
|
5
|
+
const pkg = require('../package.json');
|
|
6
|
+
|
|
7
|
+
// Commands
|
|
8
|
+
const auth = require('../commands/auth');
|
|
9
|
+
const deploy = require('../commands/deploy');
|
|
10
|
+
const projects= require('../commands/projects');
|
|
11
|
+
const envCmd = require('../commands/env');
|
|
12
|
+
const logs = require('../commands/logs');
|
|
13
|
+
const domains = require('../commands/domains');
|
|
14
|
+
const db = require('../commands/db');
|
|
15
|
+
const account = require('../commands/account');
|
|
16
|
+
const github = require('../commands/github');
|
|
17
|
+
const webhook = require('../commands/webhook');
|
|
18
|
+
const ssh = require('../commands/ssh');
|
|
19
|
+
const extras = require('../commands/extras');
|
|
20
|
+
const ui = require('../lib/ui');
|
|
21
|
+
|
|
22
|
+
// ── Custom help screen ────────────────────────────────────────────────
|
|
23
|
+
function showHelp() {
|
|
24
|
+
const c = ui.c;
|
|
25
|
+
ui.logo();
|
|
26
|
+
|
|
27
|
+
console.log(`${c.bold}USAGE${c.reset}`);
|
|
28
|
+
console.log(` joytree ${c.cyan}<command>${c.reset} ${c.dim}[options]${c.reset}\n`);
|
|
29
|
+
|
|
30
|
+
const section = (title, icon) => {
|
|
31
|
+
console.log(`${c.bold}${c.white}${icon} ${title}${c.reset}`);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const cmd = (name, args, desc) => {
|
|
35
|
+
const nameCol = `${c.cyan} joytree ${c.bold}${name}${c.reset}`;
|
|
36
|
+
const argsCol = args ? ` ${c.dim}${args}${c.reset}` : '';
|
|
37
|
+
const pad = Math.max(0, 42 - name.length - (args ? args.length + 1 : 0));
|
|
38
|
+
console.log(`${nameCol}${argsCol}${' '.repeat(pad)}${c.dim}${desc}${c.reset}`);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── ACCOUNT ──
|
|
42
|
+
section('Account', 'â—†');
|
|
43
|
+
cmd('login', '--api-key <key>', 'Validate and save your Joytree API key');
|
|
44
|
+
cmd('logout', '', 'Remove local credentials');
|
|
45
|
+
cmd('whoami', '', 'Show the active account and API key scope');
|
|
46
|
+
cmd('status', '', 'Show account status and project overview');
|
|
47
|
+
console.log();
|
|
48
|
+
|
|
49
|
+
// ── API KEY ──
|
|
50
|
+
section('API Key', 'â—†');
|
|
51
|
+
cmd('apikey show', '', 'View your current API key and usage stats');
|
|
52
|
+
cmd('apikey rotate', '', 'Revoke old key and generate a new one');
|
|
53
|
+
console.log();
|
|
54
|
+
|
|
55
|
+
// ── DEPLOY ──
|
|
56
|
+
section('Deploy', 'â—†');
|
|
57
|
+
cmd('deploy', '-r <repo> -n <name>', 'Deploy a GitHub repo to Joytree');
|
|
58
|
+
cmd('redeploy', '<project-id>', 'Trigger a fresh redeployment');
|
|
59
|
+
cmd('stop', '<deploy-id>', 'Cancel a currently running deployment');
|
|
60
|
+
cmd('autodeploy', '<project-id> --enable', 'Toggle GitHub push auto-deploy on/off');
|
|
61
|
+
cmd('deployments', '[project-id]', 'Show recent deployment history');
|
|
62
|
+
cmd('open', '<project-id>', 'Open the live URL in your browser');
|
|
63
|
+
console.log();
|
|
64
|
+
|
|
65
|
+
// ── PROJECTS ──
|
|
66
|
+
section('Projects', 'â—†');
|
|
67
|
+
cmd('projects', '', 'List all your projects');
|
|
68
|
+
cmd('inspect', '<project-id>', 'Show full project details');
|
|
69
|
+
cmd('delete', '<project-id>', 'Delete a project (irreversible)');
|
|
70
|
+
console.log();
|
|
71
|
+
|
|
72
|
+
// ── LOGS ──
|
|
73
|
+
section('Logs', 'â—†');
|
|
74
|
+
cmd('logs', '<project-id>', 'Fetch recent runtime logs');
|
|
75
|
+
cmd('logs', '<project-id> --follow', 'Stream live logs in real time');
|
|
76
|
+
console.log();
|
|
77
|
+
|
|
78
|
+
// ── ENV VARS ──
|
|
79
|
+
section('Environment Variables', 'â—†');
|
|
80
|
+
cmd('env list', '<project-id>', 'List all env var keys');
|
|
81
|
+
cmd('env set', '<project-id> KEY=VALUE', 'Set one or more env vars');
|
|
82
|
+
cmd('env delete', '<project-id> <KEY>', 'Delete an env var');
|
|
83
|
+
cmd('env push', '<project-id>', 'Push a local .env file to a project');
|
|
84
|
+
console.log();
|
|
85
|
+
|
|
86
|
+
// ── GITHUB ──
|
|
87
|
+
section('GitHub', 'â—†');
|
|
88
|
+
cmd('pull repos', '', 'List your linked GitHub repositories');
|
|
89
|
+
cmd('pull branches', '<repo-url>', 'List branches for a repository');
|
|
90
|
+
console.log();
|
|
91
|
+
|
|
92
|
+
// ── DOMAINS ──
|
|
93
|
+
section('Domains', 'â—†');
|
|
94
|
+
cmd('domains list', '', 'List your custom domains');
|
|
95
|
+
cmd('domains attach', '<domain> <project-id>', 'Attach a domain to a project');
|
|
96
|
+
cmd('domains verify', '<domain>', 'Trigger DNS verification');
|
|
97
|
+
cmd('domains remove', '<domain>', 'Remove a custom domain');
|
|
98
|
+
cmd('domains check', '<domain>', 'Check domain availability');
|
|
99
|
+
console.log();
|
|
100
|
+
|
|
101
|
+
// ── DATABASES ──
|
|
102
|
+
section('Databases', 'â—†');
|
|
103
|
+
cmd('db list', '', 'List all databases');
|
|
104
|
+
cmd('db create', '--type', 'Create a new database (postgres/mysql/redis/mongo)');
|
|
105
|
+
cmd('db start', '<db-id>', 'Start a stopped database');
|
|
106
|
+
cmd('db stop', '<db-id>', 'Stop a running database');
|
|
107
|
+
cmd('db restart', '<db-id>', 'Restart a database');
|
|
108
|
+
cmd('db logs', '<db-id>', 'Fetch recent database logs');
|
|
109
|
+
cmd('db delete', '<db-id>', 'Delete a database (irreversible)');
|
|
110
|
+
console.log();
|
|
111
|
+
|
|
112
|
+
// ── WEBHOOK ──
|
|
113
|
+
section('Webhooks', 'â—†');
|
|
114
|
+
cmd('webhook secret', '', 'Show your global webhook secret');
|
|
115
|
+
cmd('webhook rotate', '', 'Regenerate your webhook secret');
|
|
116
|
+
console.log();
|
|
117
|
+
|
|
118
|
+
// ── SSH ──
|
|
119
|
+
section('SSH Keys', 'â—†');
|
|
120
|
+
cmd('ssh list', '', 'List all SSH keys');
|
|
121
|
+
cmd('ssh generate', '--name', 'Generate a new SSH key pair');
|
|
122
|
+
cmd('ssh delete', '<key-id>', 'Delete an SSH key');
|
|
123
|
+
console.log();
|
|
124
|
+
|
|
125
|
+
// ── ACTIVITY ──
|
|
126
|
+
section('Activity', 'â—†');
|
|
127
|
+
cmd('activity', '--limit <n>', 'Show recent platform activity feed');
|
|
128
|
+
console.log();
|
|
129
|
+
|
|
130
|
+
// ── FOOTER ──
|
|
131
|
+
console.log(`${c.dim} Run ${c.cyan}joytree <command> --help${c.dim} for detailed options on any command.${c.reset}`);
|
|
132
|
+
console.log(`${c.dim} Docs: ${c.cyan}https://docs.joytree.app${c.reset}\n`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
program
|
|
136
|
+
.name('joytree')
|
|
137
|
+
.description('Joytree CLI — deploy and manage your projects from the terminal')
|
|
138
|
+
.version(pkg.version)
|
|
139
|
+
.helpOption(false)
|
|
140
|
+
.addHelpCommand(false)
|
|
141
|
+
.option('-h, --help', 'Show this help screen')
|
|
142
|
+
.hook('preAction', (thisCommand) => {
|
|
143
|
+
if (thisCommand.opts().help) { showHelp(); process.exit(0); }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Override default --help
|
|
147
|
+
program.on('--help', showHelp);
|
|
148
|
+
|
|
149
|
+
// Show custom help when no args given
|
|
150
|
+
if (process.argv.length === 2) {
|
|
151
|
+
showHelp();
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Auth ──────────────────────────────────────────────────────────────
|
|
156
|
+
program
|
|
157
|
+
.command('login')
|
|
158
|
+
.description('Authenticate with your Joytree API key')
|
|
159
|
+
.option('--api-key <key>', 'Provide API key directly (skips prompt)')
|
|
160
|
+
.action(auth.login);
|
|
161
|
+
|
|
162
|
+
program
|
|
163
|
+
.command('logout')
|
|
164
|
+
.description('Remove saved credentials')
|
|
165
|
+
.action(auth.logout);
|
|
166
|
+
|
|
167
|
+
program
|
|
168
|
+
.command('whoami')
|
|
169
|
+
.description('Show the active account and API key scope')
|
|
170
|
+
.action(auth.whoami);
|
|
171
|
+
|
|
172
|
+
program
|
|
173
|
+
.command('status')
|
|
174
|
+
.description('Show account status, plan, and project count')
|
|
175
|
+
.action(account.status);
|
|
176
|
+
|
|
177
|
+
// ── API Key ───────────────────────────────────────────────────────────
|
|
178
|
+
const apikeyGroup = program
|
|
179
|
+
.command('apikey')
|
|
180
|
+
.description('Manage your Joytree API key');
|
|
181
|
+
|
|
182
|
+
apikeyGroup.command('show').description('Show your current API key and usage info').action(extras.apiKey);
|
|
183
|
+
apikeyGroup.command('rotate').description('Generate a new API key and revoke the old one').action(extras.rotateApiKey);
|
|
184
|
+
|
|
185
|
+
// ── Activity ──────────────────────────────────────────────────────────
|
|
186
|
+
program
|
|
187
|
+
.command('activity')
|
|
188
|
+
.description('Show recent platform activity')
|
|
189
|
+
.option('--limit <n>', 'Number of events to show', '20')
|
|
190
|
+
.action(extras.activity);
|
|
191
|
+
|
|
192
|
+
// ── GitHub ────────────────────────────────────────────────────────────
|
|
193
|
+
const pullGroup = program.command('pull').description('Browse connected GitHub repos and branches');
|
|
194
|
+
pullGroup.command('repos').description('List your linked GitHub repositories').action(github.repos);
|
|
195
|
+
pullGroup.command('branches <repo-url>').description('List branches for a repository').action(github.branches);
|
|
196
|
+
|
|
197
|
+
// ── Deploy ────────────────────────────────────────────────────────────
|
|
198
|
+
program
|
|
199
|
+
.command('deploy')
|
|
200
|
+
.description('Deploy a GitHub repository to Joytree')
|
|
201
|
+
.option('-r, --repo <url>', 'GitHub repository URL')
|
|
202
|
+
.option('-b, --branch <branch>', 'Branch to deploy', 'main')
|
|
203
|
+
.option('-n, --name <name>', 'Project name / subdomain')
|
|
204
|
+
.option('--build <cmd>', 'Build command')
|
|
205
|
+
.option('--start <cmd>', 'Start command')
|
|
206
|
+
.option('--static', 'Mark as a static site')
|
|
207
|
+
.option('-m, --message <msg>', 'Deployment message')
|
|
208
|
+
.action(deploy.deployGit);
|
|
209
|
+
|
|
210
|
+
program.command('redeploy <project-id>').description('Trigger a fresh deployment').action(deploy.redeploy);
|
|
211
|
+
program.command('stop <deploy-id>').description('Stop a currently running deployment').action(extras.stopDeploy);
|
|
212
|
+
program
|
|
213
|
+
.command('autodeploy <project-id>')
|
|
214
|
+
.description('Toggle GitHub push auto-deploy')
|
|
215
|
+
.option('--enable', 'Enable auto-deploy')
|
|
216
|
+
.option('--disable', 'Disable auto-deploy')
|
|
217
|
+
.action(extras.autodeploy);
|
|
218
|
+
program.command('open [project-id]').description('Open the live URL in your browser').action(deploy.open);
|
|
219
|
+
program
|
|
220
|
+
.command('deployments [project-id]')
|
|
221
|
+
.description('Show recent deployments')
|
|
222
|
+
.option('--limit <n>', 'Number to show', '10')
|
|
223
|
+
.action(deploy.listDeployments);
|
|
224
|
+
|
|
225
|
+
// ── Projects ──────────────────────────────────────────────────────────
|
|
226
|
+
program.command('projects').description('List all your projects').option('--json', 'Output raw JSON').action(projects.list);
|
|
227
|
+
program.command('inspect <project-id>').description('Show full project details').action(projects.inspect);
|
|
228
|
+
program.command('delete <project-id>').description('Delete a project (irreversible)').option('-y, --yes', 'Skip confirmation').action(projects.deleteProject);
|
|
229
|
+
|
|
230
|
+
// ── Logs ──────────────────────────────────────────────────────────────
|
|
231
|
+
program
|
|
232
|
+
.command('logs <project-id>')
|
|
233
|
+
.description('Fetch runtime logs for a project')
|
|
234
|
+
.option('--lines <n>', 'Number of lines', '50')
|
|
235
|
+
.option('-f, --follow', 'Stream live logs')
|
|
236
|
+
.action(logs.fetchLogs);
|
|
237
|
+
|
|
238
|
+
// ── Env ───────────────────────────────────────────────────────────────
|
|
239
|
+
const envGroup = program.command('env').description('Manage environment variables');
|
|
240
|
+
envGroup.command('list <project-id>').description('List all env var keys').action(envCmd.list);
|
|
241
|
+
envGroup.command('set <project-id> <KEY=VALUE...>').description('Set one or more env vars').action(envCmd.set);
|
|
242
|
+
envGroup.command('delete <project-id> <KEY>').description('Delete an env var').action(envCmd.del);
|
|
243
|
+
envGroup.command('push <project-id>').description('Push a .env file').option('--file <path>', 'Path to .env file', '.env').option('--force', 'Overwrite all existing vars').action(envCmd.push);
|
|
244
|
+
|
|
245
|
+
// ── Domains ───────────────────────────────────────────────────────────
|
|
246
|
+
const domainGroup = program.command('domains').description('Manage custom domains');
|
|
247
|
+
domainGroup.command('list').description('List your custom domains').action(domains.list);
|
|
248
|
+
domainGroup.command('attach <domain> <project-id>').description('Attach a domain to a project').action(domains.attach);
|
|
249
|
+
domainGroup.command('verify <domain>').description('Trigger DNS verification').action(domains.verify);
|
|
250
|
+
domainGroup.command('remove <domain>').description('Remove a custom domain').action(domains.remove);
|
|
251
|
+
domainGroup.command('check <domain>').description('Check domain availability').action(domains.check);
|
|
252
|
+
|
|
253
|
+
// ── Databases ─────────────────────────────────────────────────────────
|
|
254
|
+
const dbGroup = program.command('db').description('Manage databases');
|
|
255
|
+
dbGroup.command('list').description('List all databases').action(db.list);
|
|
256
|
+
dbGroup.command('create').description('Create a new database').option('--type <type>', 'Database type', 'postgres').option('--name <name>', 'Database name').action(db.create);
|
|
257
|
+
dbGroup.command('start <db-id>').description('Start a stopped database').action(db.start);
|
|
258
|
+
dbGroup.command('stop <db-id>').description('Stop a running database').action(db.stop);
|
|
259
|
+
dbGroup.command('restart <db-id>').description('Restart a database').action(db.restart);
|
|
260
|
+
dbGroup.command('logs <db-id>').description('Fetch database logs').action(db.fetchLogs);
|
|
261
|
+
dbGroup.command('delete <db-id>').description('Delete a database').option('-y, --yes', 'Skip confirmation').action(db.del);
|
|
262
|
+
|
|
263
|
+
// ── Webhook ───────────────────────────────────────────────────────────
|
|
264
|
+
const webhookGroup = program.command('webhook').description('Manage GitHub webhook secrets');
|
|
265
|
+
webhookGroup.command('secret').description('Show your global webhook secret').action(webhook.getSecret);
|
|
266
|
+
webhookGroup.command('rotate').description('Regenerate your webhook secret').action(webhook.rotateSecret);
|
|
267
|
+
|
|
268
|
+
// ── SSH ───────────────────────────────────────────────────────────────
|
|
269
|
+
const sshGroup = program.command('ssh').description('Manage SSH keys');
|
|
270
|
+
sshGroup.command('list').description('List all SSH keys').action(ssh.list);
|
|
271
|
+
sshGroup.command('generate').description('Generate a new SSH key pair').option('--name <name>', 'Key name/label').action(ssh.generate);
|
|
272
|
+
sshGroup.command('delete <key-id>').description('Delete an SSH key').option('-y, --yes', 'Skip confirmation').action(ssh.del);
|
|
273
|
+
|
|
274
|
+
program.parseAsync(process.argv).catch(err => {
|
|
275
|
+
console.error('Error:', err.message);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const config = require('../lib/config');
|
|
4
|
+
const { validateApiKey } = require('../lib/api');
|
|
5
|
+
const ui = require('../lib/ui');
|
|
6
|
+
|
|
7
|
+
async function status() {
|
|
8
|
+
const apiKey = config.getApiKey();
|
|
9
|
+
const baseUrl = config.getBaseUrl();
|
|
10
|
+
if (!apiKey) { ui.warn('Not logged in. Run: joytree login'); return; }
|
|
11
|
+
|
|
12
|
+
const spin = ui.spinner('Loading account status');
|
|
13
|
+
try {
|
|
14
|
+
const data = await validateApiKey(apiKey, baseUrl);
|
|
15
|
+
spin.stop();
|
|
16
|
+
|
|
17
|
+
const projects = Array.isArray(data.projects) ? data.projects : [];
|
|
18
|
+
const email = config.load().email || '—';
|
|
19
|
+
|
|
20
|
+
ui.header('Account Status');
|
|
21
|
+
ui.divider();
|
|
22
|
+
ui.label('Email', email);
|
|
23
|
+
ui.label('Host', baseUrl);
|
|
24
|
+
ui.label('Projects', String(projects.length));
|
|
25
|
+
ui.label('API Key', apiKey.slice(0, 8) + '…' + apiKey.slice(-4));
|
|
26
|
+
console.log();
|
|
27
|
+
|
|
28
|
+
if (projects.length) {
|
|
29
|
+
ui.header('Projects');
|
|
30
|
+
ui.divider();
|
|
31
|
+
const base = baseUrl.replace(/^https?:\/\//, '');
|
|
32
|
+
projects.slice(0, 10).forEach(p => {
|
|
33
|
+
const sub = p.subdomain || p.name || p.id;
|
|
34
|
+
console.log(` ${ui.c.bold}${sub}${ui.c.reset} ${ui.c.dim}https://${sub}.${base}${ui.c.reset}`);
|
|
35
|
+
});
|
|
36
|
+
if (projects.length > 10) ui.info(`… and ${projects.length - 10} more. Run: joytree projects`);
|
|
37
|
+
console.log();
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spin.stop();
|
|
41
|
+
ui.error(`Failed: ${err.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { status };
|
package/commands/auth.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const config = require('../lib/config');
|
|
5
|
+
const { validateApiKey } = require('../lib/api');
|
|
6
|
+
const ui = require('../lib/ui');
|
|
7
|
+
|
|
8
|
+
async function prompt(question) {
|
|
9
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function login(opts) {
|
|
14
|
+
ui.logo();
|
|
15
|
+
|
|
16
|
+
let apiKey = opts.apiKey;
|
|
17
|
+
let baseUrl = config.getBaseUrl();
|
|
18
|
+
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
console.log(`${ui.c.dim}Find your API key at: ${baseUrl}/dashboard → Settings → API Key${ui.c.reset}\n`);
|
|
21
|
+
apiKey = await prompt(`${ui.c.bold}Paste your Joytree API key:${ui.c.reset} `);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!apiKey || !apiKey.startsWith('jtk_')) {
|
|
25
|
+
ui.error('Invalid API key format. Keys start with jtk_');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spin = ui.spinner('Validating API key');
|
|
30
|
+
try {
|
|
31
|
+
const data = await validateApiKey(apiKey, baseUrl);
|
|
32
|
+
spin.stop();
|
|
33
|
+
const email = (data.projects && data.email) || (Array.isArray(data) ? '' : data.email) || '';
|
|
34
|
+
const projects = Array.isArray(data.projects) ? data.projects.length : (data.projectCount || 0);
|
|
35
|
+
config.setCredentials({ apiKey, baseUrl, email });
|
|
36
|
+
ui.success(`Logged in${email ? ' as ' + ui.c.bold + email + ui.c.reset : ''}`);
|
|
37
|
+
ui.info(`${projects} project(s) in your workspace`);
|
|
38
|
+
console.log(`\n${ui.c.dim}Run ${ui.c.cyan}joytree projects${ui.c.reset}${ui.c.dim} to see them.${ui.c.reset}\n`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spin.stop();
|
|
41
|
+
ui.error(`Authentication failed: ${err.message}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function logout() {
|
|
47
|
+
config.clearCredentials();
|
|
48
|
+
ui.success('Logged out. Credentials removed.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function whoami() {
|
|
52
|
+
const creds = config.load();
|
|
53
|
+
if (!creds.apiKey) {
|
|
54
|
+
ui.warn('Not logged in. Run: joytree login');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
ui.header('Current Session');
|
|
58
|
+
if (creds.email) ui.label('Email', creds.email);
|
|
59
|
+
ui.label('API Key', creds.apiKey.slice(0, 8) + '…' + creds.apiKey.slice(-4));
|
|
60
|
+
ui.label('Host', creds.baseUrl || 'https://joytree.app');
|
|
61
|
+
ui.label('Config', config.CONFIG_FILE);
|
|
62
|
+
console.log();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { login, logout, whoami };
|
package/commands/db.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { api } = require('../lib/api');
|
|
5
|
+
const ui = require('../lib/ui');
|
|
6
|
+
|
|
7
|
+
async function prompt(q) {
|
|
8
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
9
|
+
return new Promise(r => rl.question(q, a => { rl.close(); r(a.trim()); }));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function list() {
|
|
13
|
+
const spin = ui.spinner('Fetching databases');
|
|
14
|
+
try {
|
|
15
|
+
const data = await api.get('/api/databases');
|
|
16
|
+
spin.stop();
|
|
17
|
+
const items = Array.isArray(data) ? data : (data.databases || []);
|
|
18
|
+
if (!items.length) { ui.info('No databases yet. Create one: joytree db create'); return; }
|
|
19
|
+
ui.header(`Databases (${items.length})`);
|
|
20
|
+
ui.divider();
|
|
21
|
+
items.forEach(d => {
|
|
22
|
+
console.log(` ${ui.statusBadge(d.status)} ${ui.c.bold}${d.name}${ui.c.reset} ${ui.c.dim}[${d.type || 'db'}] ${d.id}${ui.c.reset}`);
|
|
23
|
+
if (d.host) console.log(` ${ui.c.dim}host: ${d.host}:${d.port || '—'}${ui.c.reset}`);
|
|
24
|
+
});
|
|
25
|
+
console.log();
|
|
26
|
+
} catch (err) {
|
|
27
|
+
spin.stop();
|
|
28
|
+
ui.error(`Failed: ${err.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function create(opts) {
|
|
34
|
+
let { type, name } = opts;
|
|
35
|
+
if (!name) {
|
|
36
|
+
name = await prompt(`${ui.c.bold}Database name:${ui.c.reset} `);
|
|
37
|
+
if (!name) { ui.error('Name is required.'); process.exit(1); }
|
|
38
|
+
}
|
|
39
|
+
const spin = ui.spinner(`Creating ${type} database "${name}"`);
|
|
40
|
+
try {
|
|
41
|
+
const data = await api.post('/api/databases', { type: type || 'postgres', name });
|
|
42
|
+
spin.stop(`Database ${ui.c.bold}${name}${ui.c.reset} created!`);
|
|
43
|
+
if (data.id) ui.label('ID', data.id);
|
|
44
|
+
if (data.host) ui.label('Host', `${data.host}:${data.port || '—'}`);
|
|
45
|
+
if (data.connectionString) ui.label('DSN', data.connectionString);
|
|
46
|
+
console.log();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
spin.stop();
|
|
49
|
+
ui.error(`Failed: ${err.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function dbAction(dbId, action) {
|
|
55
|
+
const spin = ui.spinner(`${action} database ${dbId}`);
|
|
56
|
+
try {
|
|
57
|
+
await api.post(`/api/databases/${encodeURIComponent(dbId)}/${action}`, {});
|
|
58
|
+
spin.stop(`Database ${ui.c.bold}${dbId}${ui.c.reset} ${action}ped.`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
spin.stop();
|
|
61
|
+
ui.error(`Failed: ${err.message}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const start = dbId => dbAction(dbId, 'start');
|
|
67
|
+
const stop = dbId => dbAction(dbId, 'stop');
|
|
68
|
+
const restart = dbId => dbAction(dbId, 'restart');
|
|
69
|
+
|
|
70
|
+
async function fetchLogs(dbId) {
|
|
71
|
+
const spin = ui.spinner(`Fetching logs for ${dbId}`);
|
|
72
|
+
try {
|
|
73
|
+
const data = await api.get(`/api/databases/${encodeURIComponent(dbId)}/logs`);
|
|
74
|
+
spin.stop();
|
|
75
|
+
const lines = Array.isArray(data) ? data : (data.logs || data.lines || []);
|
|
76
|
+
ui.header(`DB Logs — ${dbId}`);
|
|
77
|
+
ui.divider();
|
|
78
|
+
lines.forEach(l => {
|
|
79
|
+
const msg = typeof l === 'string' ? l : (l.message || l.text || JSON.stringify(l));
|
|
80
|
+
console.log(` ${ui.c.dim}${msg}${ui.c.reset}`);
|
|
81
|
+
});
|
|
82
|
+
console.log();
|
|
83
|
+
} catch (err) {
|
|
84
|
+
spin.stop();
|
|
85
|
+
ui.error(`Failed: ${err.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function del(dbId, opts) {
|
|
91
|
+
if (!opts.yes) {
|
|
92
|
+
const ans = await prompt(`${ui.c.yellow}Delete database "${dbId}"? This is irreversible. Type yes to confirm: ${ui.c.reset}`);
|
|
93
|
+
if (ans.toLowerCase() !== 'yes') { ui.info('Cancelled.'); return; }
|
|
94
|
+
}
|
|
95
|
+
const spin = ui.spinner(`Deleting ${dbId}`);
|
|
96
|
+
try {
|
|
97
|
+
await api.post(`/api/databases/${encodeURIComponent(dbId)}/delete`, {});
|
|
98
|
+
spin.stop(`Database ${ui.c.bold}${dbId}${ui.c.reset} deleted.`);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
spin.stop();
|
|
101
|
+
ui.error(`Failed: ${err.message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { list, create, start, stop, restart, fetchLogs, del };
|